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