You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo-issue.js 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import $ from 'jquery';
  2. import {htmlEscape} from 'escape-goat';
  3. import {showTemporaryTooltip, createTippy} from '../modules/tippy.js';
  4. import {hideElem, showElem, toggleElem} from '../utils/dom.js';
  5. import {setFileFolding} from './file-fold.js';
  6. import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
  7. import {toAbsoluteUrl} from '../utils.js';
  8. import {initDropzone} from './common-global.js';
  9. import {POST, GET} from '../modules/fetch.js';
  10. const {appSubUrl} = window.config;
  11. export function initRepoIssueTimeTracking() {
  12. $(document).on('click', '.issue-add-time', () => {
  13. $('.issue-start-time-modal').modal({
  14. duration: 200,
  15. onApprove() {
  16. $('#add_time_manual_form').trigger('submit');
  17. },
  18. }).modal('show');
  19. $('.issue-start-time-modal input').on('keydown', (e) => {
  20. if ((e.keyCode || e.key) === 13) {
  21. $('#add_time_manual_form').trigger('submit');
  22. }
  23. });
  24. });
  25. $(document).on('click', '.issue-start-time, .issue-stop-time', () => {
  26. $('#toggle_stopwatch_form').trigger('submit');
  27. });
  28. $(document).on('click', '.issue-cancel-time', () => {
  29. $('#cancel_stopwatch_form').trigger('submit');
  30. });
  31. $(document).on('click', 'button.issue-delete-time', function () {
  32. const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`;
  33. $(sel).modal({
  34. duration: 200,
  35. onApprove() {
  36. $(`${sel} form`).trigger('submit');
  37. },
  38. }).modal('show');
  39. });
  40. }
  41. async function updateDeadline(deadlineString) {
  42. hideElem('#deadline-err-invalid-date');
  43. document.getElementById('deadline-loader')?.classList.add('is-loading');
  44. let realDeadline = null;
  45. if (deadlineString !== '') {
  46. const newDate = Date.parse(deadlineString);
  47. if (Number.isNaN(newDate)) {
  48. document.getElementById('deadline-loader')?.classList.remove('is-loading');
  49. showElem('#deadline-err-invalid-date');
  50. return false;
  51. }
  52. realDeadline = new Date(newDate);
  53. }
  54. try {
  55. const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
  56. data: {due_date: realDeadline},
  57. });
  58. if (response.ok) {
  59. window.location.reload();
  60. } else {
  61. throw new Error('Invalid response');
  62. }
  63. } catch (error) {
  64. console.error(error);
  65. document.getElementById('deadline-loader').classList.remove('is-loading');
  66. showElem('#deadline-err-invalid-date');
  67. }
  68. }
  69. export function initRepoIssueDue() {
  70. $(document).on('click', '.issue-due-edit', () => {
  71. toggleElem('#deadlineForm');
  72. });
  73. $(document).on('click', '.issue-due-remove', () => {
  74. updateDeadline('');
  75. });
  76. $(document).on('submit', '.issue-due-form', () => {
  77. updateDeadline($('#deadlineDate').val());
  78. return false;
  79. });
  80. }
  81. /**
  82. * @param {HTMLElement} item
  83. */
  84. function excludeLabel(item) {
  85. const href = item.getAttribute('href');
  86. const id = item.getAttribute('data-label-id');
  87. const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
  88. const newStr = 'labels=$1-$2$3&';
  89. window.location = href.replace(new RegExp(regStr), newStr);
  90. }
  91. export function initRepoIssueSidebarList() {
  92. const repolink = $('#repolink').val();
  93. const repoId = $('#repoId').val();
  94. const crossRepoSearch = $('#crossRepoSearch').val();
  95. const tp = $('#type').val();
  96. let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
  97. if (crossRepoSearch === 'true') {
  98. issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
  99. }
  100. $('#new-dependency-drop-list')
  101. .dropdown({
  102. apiSettings: {
  103. url: issueSearchUrl,
  104. onResponse(response) {
  105. const filteredResponse = {success: true, results: []};
  106. const currIssueId = $('#new-dependency-drop-list').data('issue-id');
  107. // Parse the response from the api to work with our dropdown
  108. $.each(response, (_i, issue) => {
  109. // Don't list current issue in the dependency list.
  110. if (issue.id === currIssueId) {
  111. return;
  112. }
  113. filteredResponse.results.push({
  114. name: `#${issue.number} ${htmlEscape(issue.title)
  115. }<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`,
  116. value: issue.id,
  117. });
  118. });
  119. return filteredResponse;
  120. },
  121. cache: false,
  122. },
  123. fullTextSearch: true,
  124. });
  125. $('.menu a.label-filter-item').each(function () {
  126. $(this).on('click', function (e) {
  127. if (e.altKey) {
  128. e.preventDefault();
  129. excludeLabel(this);
  130. }
  131. });
  132. });
  133. $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
  134. if (e.altKey && e.keyCode === 13) {
  135. const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
  136. if (selectedItem) {
  137. excludeLabel(selectedItem);
  138. }
  139. }
  140. });
  141. $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
  142. }
  143. export function initRepoIssueCommentDelete() {
  144. // Delete comment
  145. document.addEventListener('click', async (e) => {
  146. if (!e.target.matches('.delete-comment')) return;
  147. e.preventDefault();
  148. const deleteButton = e.target;
  149. if (window.confirm(deleteButton.getAttribute('data-locale'))) {
  150. try {
  151. const response = await POST(deleteButton.getAttribute('data-url'));
  152. if (!response.ok) throw new Error('Failed to delete comment');
  153. const conversationHolder = deleteButton.closest('.conversation-holder');
  154. const parentTimelineItem = deleteButton.closest('.timeline-item');
  155. const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
  156. // Check if this was a pending comment.
  157. if (conversationHolder?.querySelector('.pending-label')) {
  158. const counter = document.querySelector('#review-box .review-comments-counter');
  159. let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
  160. num = Math.max(num, 0);
  161. counter.setAttribute('data-pending-comment-number', num);
  162. counter.textContent = String(num);
  163. }
  164. document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
  165. if (conversationHolder && !conversationHolder.querySelector('.comment')) {
  166. const path = conversationHolder.getAttribute('data-path');
  167. const side = conversationHolder.getAttribute('data-side');
  168. const idx = conversationHolder.getAttribute('data-idx');
  169. const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
  170. if (lineType === 'same') {
  171. document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
  172. } else {
  173. document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
  174. }
  175. conversationHolder.remove();
  176. }
  177. // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
  178. if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
  179. const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
  180. timelineAvatar?.classList.remove('timeline-avatar-offset');
  181. }
  182. } catch (error) {
  183. console.error(error);
  184. }
  185. }
  186. });
  187. }
  188. export function initRepoIssueDependencyDelete() {
  189. // Delete Issue dependency
  190. $(document).on('click', '.delete-dependency-button', (e) => {
  191. const id = e.currentTarget.getAttribute('data-id');
  192. const type = e.currentTarget.getAttribute('data-type');
  193. $('.remove-dependency').modal({
  194. closable: false,
  195. duration: 200,
  196. onApprove: () => {
  197. $('#removeDependencyID').val(id);
  198. $('#dependencyType').val(type);
  199. $('#removeDependencyForm').trigger('submit');
  200. },
  201. }).modal('show');
  202. });
  203. }
  204. export function initRepoIssueCodeCommentCancel() {
  205. // Cancel inline code comment
  206. document.addEventListener('click', (e) => {
  207. if (!e.target.matches('.cancel-code-comment')) return;
  208. const form = e.target.closest('form');
  209. if (form?.classList.contains('comment-form')) {
  210. hideElem(form);
  211. showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
  212. } else {
  213. form.closest('.comment-code-cloud')?.remove();
  214. }
  215. });
  216. }
  217. export function initRepoPullRequestUpdate() {
  218. // Pull Request update button
  219. const pullUpdateButton = document.querySelector('.update-button > button');
  220. if (!pullUpdateButton) return;
  221. pullUpdateButton.addEventListener('click', async function (e) {
  222. e.preventDefault();
  223. const redirect = this.getAttribute('data-redirect');
  224. this.classList.add('is-loading');
  225. let response;
  226. try {
  227. response = await POST(this.getAttribute('data-do'));
  228. } catch (error) {
  229. console.error(error);
  230. } finally {
  231. this.classList.remove('is-loading');
  232. }
  233. let data;
  234. try {
  235. data = await response?.json(); // the response is probably not a JSON
  236. } catch (error) {
  237. console.error(error);
  238. }
  239. if (data?.redirect) {
  240. window.location.href = data.redirect;
  241. } else if (redirect) {
  242. window.location.href = redirect;
  243. } else {
  244. window.location.reload();
  245. }
  246. });
  247. $('.update-button > .dropdown').dropdown({
  248. onChange(_text, _value, $choice) {
  249. const url = $choice[0].getAttribute('data-do');
  250. if (url) {
  251. const buttonText = pullUpdateButton.querySelector('.button-text');
  252. if (buttonText) {
  253. buttonText.textContent = $choice.text();
  254. }
  255. pullUpdateButton.setAttribute('data-do', url);
  256. }
  257. },
  258. });
  259. }
  260. export function initRepoPullRequestMergeInstruction() {
  261. $('.show-instruction').on('click', () => {
  262. toggleElem($('.instruct-content'));
  263. });
  264. }
  265. export function initRepoPullRequestAllowMaintainerEdit() {
  266. const wrapper = document.getElementById('allow-edits-from-maintainers');
  267. if (!wrapper) return;
  268. wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => {
  269. const checked = e.target.checked;
  270. const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
  271. wrapper.classList.add('is-loading');
  272. e.target.disabled = true;
  273. try {
  274. const response = await POST(url, {data: {allow_maintainer_edit: checked}});
  275. if (!response.ok) {
  276. throw new Error('Failed to update maintainer edit permission');
  277. }
  278. } catch (error) {
  279. console.error(error);
  280. showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
  281. } finally {
  282. wrapper.classList.remove('is-loading');
  283. e.target.disabled = false;
  284. }
  285. });
  286. }
  287. export function initRepoIssueReferenceRepositorySearch() {
  288. $('.issue_reference_repository_search')
  289. .dropdown({
  290. apiSettings: {
  291. url: `${appSubUrl}/repo/search?q={query}&limit=20`,
  292. onResponse(response) {
  293. const filteredResponse = {success: true, results: []};
  294. $.each(response.data, (_r, repo) => {
  295. filteredResponse.results.push({
  296. name: htmlEscape(repo.repository.full_name),
  297. value: repo.repository.full_name,
  298. });
  299. });
  300. return filteredResponse;
  301. },
  302. cache: false,
  303. },
  304. onChange(_value, _text, $choice) {
  305. const $form = $choice.closest('form');
  306. if (!$form.length) return;
  307. $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
  308. },
  309. fullTextSearch: true,
  310. });
  311. }
  312. export function initRepoIssueWipTitle() {
  313. $('.title_wip_desc > a').on('click', (e) => {
  314. e.preventDefault();
  315. const $issueTitle = $('#issue_title');
  316. $issueTitle.trigger('focus');
  317. const value = $issueTitle.val().trim().toUpperCase();
  318. const wipPrefixes = $('.title_wip_desc').data('wip-prefixes');
  319. for (const prefix of wipPrefixes) {
  320. if (value.startsWith(prefix.toUpperCase())) {
  321. return;
  322. }
  323. }
  324. $issueTitle.val(`${wipPrefixes[0]} ${$issueTitle.val()}`);
  325. });
  326. }
  327. export async function updateIssuesMeta(url, action, issue_ids, id) {
  328. try {
  329. const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
  330. if (!response.ok) {
  331. throw new Error('Failed to update issues meta');
  332. }
  333. } catch (error) {
  334. console.error(error);
  335. }
  336. }
  337. export function initRepoIssueComments() {
  338. if (!$('.repository.view.issue .timeline').length) return;
  339. $('.re-request-review').on('click', async function (e) {
  340. e.preventDefault();
  341. const url = this.getAttribute('data-update-url');
  342. const issueId = this.getAttribute('data-issue-id');
  343. const id = this.getAttribute('data-id');
  344. const isChecked = this.classList.contains('checked');
  345. await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
  346. window.location.reload();
  347. });
  348. document.addEventListener('click', (e) => {
  349. const urlTarget = document.querySelector(':target');
  350. if (!urlTarget) return;
  351. const urlTargetId = urlTarget.id;
  352. if (!urlTargetId) return;
  353. if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
  354. if (!e.target.closest(`#${urlTargetId}`)) {
  355. const scrollPosition = $(window).scrollTop();
  356. window.location.hash = '';
  357. $(window).scrollTop(scrollPosition);
  358. window.history.pushState(null, null, ' ');
  359. }
  360. });
  361. }
  362. export async function handleReply($el) {
  363. hideElem($el);
  364. const $form = $el.closest('.comment-code-cloud').find('.comment-form');
  365. showElem($form);
  366. const $textarea = $form.find('textarea');
  367. let editor = getComboMarkdownEditor($textarea);
  368. if (!editor) {
  369. // FIXME: the initialization of the dropzone is not consistent.
  370. // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
  371. // When the form is submitted and partially reload, none of them is initialized.
  372. const dropzone = $form.find('.dropzone')[0];
  373. if (!dropzone.dropzone) initDropzone(dropzone);
  374. editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
  375. }
  376. editor.focus();
  377. return editor;
  378. }
  379. export function initRepoPullRequestReview() {
  380. if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) {
  381. // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
  382. if (window.history.scrollRestoration !== 'manual') {
  383. window.history.scrollRestoration = 'manual';
  384. }
  385. const commentDiv = document.querySelector(window.location.hash);
  386. if (commentDiv) {
  387. // get the name of the parent id
  388. const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
  389. if (groupID && groupID.startsWith('code-comments-')) {
  390. const id = groupID.slice(14);
  391. const ancestorDiffBox = commentDiv.closest('.diff-file-box');
  392. // on pages like conversation, there is no diff header
  393. const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
  394. // offset is for scrolling
  395. let offset = 30;
  396. if (diffHeader) {
  397. offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
  398. }
  399. hideElem(`#show-outdated-${id}`);
  400. showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
  401. // if the comment box is folded, expand it
  402. if (ancestorDiffBox?.getAttribute('data-folded') === 'true') {
  403. setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
  404. }
  405. window.scrollTo({
  406. top: $(commentDiv).offset().top - offset,
  407. behavior: 'instant',
  408. });
  409. }
  410. }
  411. }
  412. $(document).on('click', '.show-outdated', function (e) {
  413. e.preventDefault();
  414. const id = this.getAttribute('data-comment');
  415. hideElem(this);
  416. showElem(`#code-comments-${id}`);
  417. showElem(`#code-preview-${id}`);
  418. showElem(`#hide-outdated-${id}`);
  419. });
  420. $(document).on('click', '.hide-outdated', function (e) {
  421. e.preventDefault();
  422. const id = this.getAttribute('data-comment');
  423. hideElem(this);
  424. hideElem(`#code-comments-${id}`);
  425. hideElem(`#code-preview-${id}`);
  426. showElem(`#show-outdated-${id}`);
  427. });
  428. $(document).on('click', 'button.comment-form-reply', async function (e) {
  429. e.preventDefault();
  430. await handleReply($(this));
  431. });
  432. const $reviewBox = $('.review-box-panel');
  433. if ($reviewBox.length === 1) {
  434. const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
  435. }
  436. // The following part is only for diff views
  437. if (!$('.repository.pull.diff').length) return;
  438. const $reviewBtn = $('.js-btn-review');
  439. const $panel = $reviewBtn.parent().find('.review-box-panel');
  440. const $closeBtn = $panel.find('.close');
  441. if ($reviewBtn.length && $panel.length) {
  442. const tippy = createTippy($reviewBtn[0], {
  443. content: $panel[0],
  444. placement: 'bottom',
  445. trigger: 'click',
  446. maxWidth: 'none',
  447. interactive: true,
  448. hideOnClick: true,
  449. });
  450. $closeBtn.on('click', (e) => {
  451. e.preventDefault();
  452. tippy.hide();
  453. });
  454. }
  455. $(document).on('click', '.add-code-comment', async function (e) {
  456. if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
  457. e.preventDefault();
  458. const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
  459. const side = this.getAttribute('data-side');
  460. const idx = this.getAttribute('data-idx');
  461. const path = this.closest('[data-path]')?.getAttribute('data-path');
  462. const tr = this.closest('tr');
  463. const lineType = tr.getAttribute('data-line-type');
  464. const ntr = tr.nextElementSibling;
  465. let $ntr = $(ntr);
  466. if (!ntr?.classList.contains('add-comment')) {
  467. $ntr = $(`
  468. <tr class="add-comment" data-line-type="${lineType}">
  469. ${isSplit ? `
  470. <td class="add-comment-left" colspan="4"></td>
  471. <td class="add-comment-right" colspan="4"></td>
  472. ` : `
  473. <td class="add-comment-left add-comment-right" colspan="5"></td>
  474. `}
  475. </tr>`);
  476. $(tr).after($ntr);
  477. }
  478. const $td = $ntr.find(`.add-comment-${side}`);
  479. const $commentCloud = $td.find('.comment-code-cloud');
  480. if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
  481. try {
  482. const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
  483. const html = await response.text();
  484. $td.html(html);
  485. $td.find("input[name='line']").val(idx);
  486. $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
  487. $td.find("input[name='path']").val(path);
  488. initDropzone($td.find('.dropzone')[0]);
  489. const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
  490. editor.focus();
  491. } catch (error) {
  492. console.error(error);
  493. }
  494. }
  495. });
  496. }
  497. export function initRepoIssueReferenceIssue() {
  498. // Reference issue
  499. $(document).on('click', '.reference-issue', function (event) {
  500. const $this = $(this);
  501. const content = $(`#${$this.data('target')}`).text();
  502. const poster = $this.data('poster-username');
  503. const reference = toAbsoluteUrl($this.data('reference'));
  504. const $modal = $($this.data('modal'));
  505. $modal.find('textarea[name="content"]').val(`${content}\n\n_Originally posted by @${poster} in ${reference}_`);
  506. $modal.modal('show');
  507. event.preventDefault();
  508. });
  509. }
  510. export function initRepoIssueWipToggle() {
  511. // Toggle WIP
  512. $('.toggle-wip a, .toggle-wip button').on('click', async (e) => {
  513. e.preventDefault();
  514. const toggleWip = e.currentTarget.closest('.toggle-wip');
  515. const title = toggleWip.getAttribute('data-title');
  516. const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
  517. const updateUrl = toggleWip.getAttribute('data-update-url');
  518. try {
  519. const params = new URLSearchParams();
  520. params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
  521. const response = await POST(updateUrl, {data: params});
  522. if (!response.ok) {
  523. throw new Error('Failed to toggle WIP status');
  524. }
  525. window.location.reload();
  526. } catch (error) {
  527. console.error(error);
  528. }
  529. });
  530. }
  531. async function pullrequest_targetbranch_change(update_url) {
  532. const targetBranch = $('#pull-target-branch').data('branch');
  533. const $branchTarget = $('#branch_target');
  534. if (targetBranch === $branchTarget.text()) {
  535. window.location.reload();
  536. return false;
  537. }
  538. try {
  539. await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
  540. } catch (error) {
  541. console.error(error);
  542. } finally {
  543. window.location.reload();
  544. }
  545. }
  546. export function initRepoIssueTitleEdit() {
  547. // Edit issue title
  548. const $issueTitle = $('#issue-title');
  549. const $editInput = $('#edit-title-input input');
  550. const editTitleToggle = function () {
  551. toggleElem($issueTitle);
  552. toggleElem('.not-in-edit');
  553. toggleElem('#edit-title-input');
  554. toggleElem('#pull-desc');
  555. toggleElem('#pull-desc-edit');
  556. toggleElem('.in-edit');
  557. toggleElem('.new-issue-button');
  558. document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
  559. $editInput[0].focus();
  560. $editInput[0].select();
  561. return false;
  562. };
  563. $('#edit-title').on('click', editTitleToggle);
  564. $('#cancel-edit-title').on('click', editTitleToggle);
  565. $('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
  566. const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
  567. if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
  568. $editInput.val($issueTitle.text());
  569. await pullrequest_targetbranch_change(pullrequest_target_update_url);
  570. } else {
  571. try {
  572. const params = new URLSearchParams();
  573. params.append('title', $editInput.val());
  574. const response = await POST(this.getAttribute('data-update-url'), {data: params});
  575. const data = await response.json();
  576. $editInput.val(data.title);
  577. $issueTitle.text(data.title);
  578. if (pullrequest_target_update_url) {
  579. await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
  580. } else {
  581. window.location.reload();
  582. }
  583. } catch (error) {
  584. console.error(error);
  585. }
  586. }
  587. return false;
  588. });
  589. }
  590. export function initRepoIssueBranchSelect() {
  591. const changeBranchSelect = function () {
  592. const $selectionTextField = $('#pull-target-branch');
  593. const baseName = $selectionTextField.data('basename');
  594. const branchNameNew = $(this).data('branch');
  595. const branchNameOld = $selectionTextField.data('branch');
  596. // Replace branch name to keep translation from HTML template
  597. $selectionTextField.html($selectionTextField.html().replace(
  598. `${baseName}:${branchNameOld}`,
  599. `${baseName}:${branchNameNew}`,
  600. ));
  601. $selectionTextField.data('branch', branchNameNew); // update branch name in setting
  602. };
  603. $('#branch-select > .item').on('click', changeBranchSelect);
  604. }
  605. export function initSingleCommentEditor($commentForm) {
  606. // pages:
  607. // * normal new issue/pr page, no status-button
  608. // * issue/pr view page, with comment form, has status-button
  609. const opts = {};
  610. const statusButton = document.getElementById('status-button');
  611. if (statusButton) {
  612. opts.onContentChanged = (editor) => {
  613. const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
  614. statusButton.textContent = statusText;
  615. };
  616. }
  617. initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
  618. }
  619. export function initIssueTemplateCommentEditors($commentForm) {
  620. // pages:
  621. // * new issue with issue template
  622. const $comboFields = $commentForm.find('.combo-editor-dropzone');
  623. const initCombo = async ($combo) => {
  624. const $dropzoneContainer = $combo.find('.form-field-dropzone');
  625. const $formField = $combo.find('.form-field-real');
  626. const $markdownEditor = $combo.find('.combo-markdown-editor');
  627. const editor = await initComboMarkdownEditor($markdownEditor, {
  628. onContentChanged: (editor) => {
  629. $formField.val(editor.value());
  630. },
  631. });
  632. $formField.on('focus', async () => {
  633. // deactivate all markdown editors
  634. showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
  635. hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
  636. hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
  637. // activate this markdown editor
  638. hideElem($formField);
  639. showElem($markdownEditor);
  640. showElem($dropzoneContainer);
  641. await editor.switchToUserPreference();
  642. editor.focus();
  643. });
  644. };
  645. for (const el of $comboFields) {
  646. initCombo($(el));
  647. }
  648. }
  649. // This function used to show and hide archived label on issue/pr
  650. // page in the sidebar where we select the labels
  651. // If we have any archived label tagged to issue and pr. We will show that
  652. // archived label with checked classed otherwise we will hide it
  653. // with the help of this function.
  654. // This function runs globally.
  655. export function initArchivedLabelHandler() {
  656. if (!document.querySelector('.archived-label-hint')) return;
  657. for (const label of document.querySelectorAll('[data-is-archived]')) {
  658. toggleElem(label, label.classList.contains('checked'));
  659. }
  660. }