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

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