您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

repo-issue.js 26KB

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