summaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-issue.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /web_src/js/features/repo-issue.js
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--web_src/js/features/repo-issue.js797
1 files changed, 797 insertions, 0 deletions
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
new file mode 100644
index 0000000..9938d53
--- /dev/null
+++ b/web_src/js/features/repo-issue.js
@@ -0,0 +1,797 @@
+import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
+import {showTemporaryTooltip, createTippy} from '../modules/tippy.js';
+import {hideElem, showElem, toggleElem} from '../utils/dom.js';
+import {setFileFolding} from './file-fold.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {toAbsoluteUrl} from '../utils.js';
+import {initDropzone} from './common-global.js';
+import {POST, GET} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
+import {emojiHTML} from './emoji.js';
+
+const {appSubUrl} = window.config;
+
+// if there are draft comments, confirm before reloading, to avoid losing comments
+export function reloadConfirmDraftComment() {
+ const commentTextareas = [
+ document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
+ document.querySelector('#comment-form textarea'),
+ ];
+ for (const textarea of commentTextareas) {
+ // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
+ // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
+ if (textarea && textarea.value.trim().length > 10) {
+ textarea.parentElement.scrollIntoView();
+ if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
+ return;
+ }
+ break;
+ }
+ }
+ window.location.reload();
+}
+
+export function initRepoIssueTimeTracking() {
+ $(document).on('click', '.issue-add-time', () => {
+ $('.issue-start-time-modal').modal({
+ duration: 200,
+ onApprove() {
+ $('#add_time_manual_form').trigger('submit');
+ },
+ }).modal('show');
+ $('.issue-start-time-modal input').on('keydown', (e) => {
+ if ((e.keyCode || e.key) === 13) {
+ $('#add_time_manual_form').trigger('submit');
+ }
+ });
+ });
+ $(document).on('click', '.issue-start-time, .issue-stop-time', () => {
+ $('#toggle_stopwatch_form').trigger('submit');
+ });
+ $(document).on('click', '.issue-cancel-time', () => {
+ $('#cancel_stopwatch_form').trigger('submit');
+ });
+ $(document).on('click', 'button.issue-delete-time', function () {
+ const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`;
+ $(sel).modal({
+ duration: 200,
+ onApprove() {
+ $(`${sel} form`).trigger('submit');
+ },
+ }).modal('show');
+ });
+}
+
+async function updateDeadline(deadlineString) {
+ hideElem('#deadline-err-invalid-date');
+ document.getElementById('deadline-loader')?.classList.add('is-loading');
+
+ let realDeadline = null;
+ if (deadlineString !== '') {
+ const newDate = Date.parse(deadlineString);
+
+ if (Number.isNaN(newDate)) {
+ document.getElementById('deadline-loader')?.classList.remove('is-loading');
+ showElem('#deadline-err-invalid-date');
+ return false;
+ }
+ realDeadline = new Date(newDate);
+ }
+
+ try {
+ const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
+ data: {due_date: realDeadline},
+ });
+
+ if (response.ok) {
+ window.location.reload();
+ } else {
+ throw new Error('Invalid response');
+ }
+ } catch (error) {
+ console.error(error);
+ document.getElementById('deadline-loader').classList.remove('is-loading');
+ showElem('#deadline-err-invalid-date');
+ }
+}
+
+export function initRepoIssueDue() {
+ $(document).on('click', '.issue-due-edit', () => {
+ toggleElem('#deadlineForm');
+ });
+ $(document).on('click', '.issue-due-remove', () => {
+ updateDeadline('');
+ });
+ $(document).on('submit', '.issue-due-form', () => {
+ updateDeadline($('#deadlineDate').val());
+ return false;
+ });
+}
+
+/**
+ * @param {HTMLElement} item
+ */
+function excludeLabel(item) {
+ const href = item.getAttribute('href');
+ const id = item.getAttribute('data-label-id');
+
+ const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
+ const newStr = 'labels=$1-$2$3&';
+
+ window.location = href.replace(new RegExp(regStr), newStr);
+}
+
+export function initRepoIssueSidebarList() {
+ const repolink = $('#repolink').val();
+ const repoId = $('#repoId').val();
+ const crossRepoSearch = $('#crossRepoSearch').val();
+ const tp = $('#type').val();
+ let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
+ if (crossRepoSearch === 'true') {
+ issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
+ }
+ $('#new-dependency-drop-list')
+ .dropdown({
+ apiSettings: {
+ url: issueSearchUrl,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ const currIssueId = $('#new-dependency-drop-list').data('issue-id');
+ // Parse the response from the api to work with our dropdown
+ $.each(response, (_i, issue) => {
+ // Don't list current issue in the dependency list.
+ if (issue.id === currIssueId) {
+ return;
+ }
+ filteredResponse.results.push({
+ name: `#${issue.number} ${issueTitleHTML(htmlEscape(issue.title))
+ }<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
+ value: issue.id,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+
+ fullTextSearch: true,
+ });
+
+ $('.menu a.label-filter-item').each(function () {
+ $(this).on('click', function (e) {
+ if (e.altKey) {
+ e.preventDefault();
+ excludeLabel(this);
+ }
+ });
+ });
+
+ $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
+ if (e.altKey && e.keyCode === 13) {
+ const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
+ if (selectedItem) {
+ excludeLabel(selectedItem);
+ }
+ }
+ });
+ $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
+}
+
+export function initRepoIssueCommentDelete() {
+ // Delete comment
+ document.addEventListener('click', async (e) => {
+ if (!e.target.matches('.delete-comment')) return;
+ e.preventDefault();
+
+ const deleteButton = e.target;
+ if (window.confirm(deleteButton.getAttribute('data-locale'))) {
+ try {
+ const response = await POST(deleteButton.getAttribute('data-url'));
+ if (!response.ok) throw new Error('Failed to delete comment');
+
+ const conversationHolder = deleteButton.closest('.conversation-holder');
+ const parentTimelineItem = deleteButton.closest('.timeline-item');
+ const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
+
+ // Check if this was a pending comment.
+ if (conversationHolder?.querySelector('.pending-label')) {
+ const counter = document.querySelector('#review-box .review-comments-counter');
+ let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
+ num = Math.max(num, 0);
+ counter.setAttribute('data-pending-comment-number', num);
+ counter.textContent = String(num);
+ }
+
+ document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
+
+ if (conversationHolder && !conversationHolder.querySelector('.comment')) {
+ const path = conversationHolder.getAttribute('data-path');
+ const side = conversationHolder.getAttribute('data-side');
+ const idx = conversationHolder.getAttribute('data-idx');
+ const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
+
+ if (lineType === 'same') {
+ document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
+ } else {
+ document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
+ }
+
+ conversationHolder.remove();
+ }
+
+ // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
+ if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
+ const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
+ timelineAvatar?.classList.remove('timeline-avatar-offset');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+}
+
+export function initRepoIssueDependencyDelete() {
+ // Delete Issue dependency
+ $(document).on('click', '.delete-dependency-button', (e) => {
+ const id = e.currentTarget.getAttribute('data-id');
+ const type = e.currentTarget.getAttribute('data-type');
+
+ $('.remove-dependency').modal({
+ closable: false,
+ duration: 200,
+ onApprove: () => {
+ $('#removeDependencyID').val(id);
+ $('#dependencyType').val(type);
+ $('#removeDependencyForm').trigger('submit');
+ },
+ }).modal('show');
+ });
+}
+
+export function initRepoIssueCodeCommentCancel() {
+ // Cancel inline code comment
+ document.addEventListener('click', (e) => {
+ if (!e.target.matches('.cancel-code-comment')) return;
+
+ const form = e.target.closest('form');
+ if (form?.classList.contains('comment-form')) {
+ hideElem(form);
+ showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
+ } else {
+ form.closest('.comment-code-cloud')?.remove();
+ }
+ });
+}
+
+export function initRepoPullRequestUpdate() {
+ // Pull Request update button
+ const pullUpdateButton = document.querySelector('.update-button > button');
+ if (!pullUpdateButton) return;
+
+ pullUpdateButton.addEventListener('click', async function (e) {
+ e.preventDefault();
+ const redirect = this.getAttribute('data-redirect');
+ this.classList.add('is-loading');
+ let response;
+ try {
+ response = await POST(this.getAttribute('data-do'));
+ } catch (error) {
+ console.error(error);
+ } finally {
+ this.classList.remove('is-loading');
+ }
+ let data;
+ try {
+ data = await response?.json(); // the response is probably not a JSON
+ } catch (error) {
+ console.error(error);
+ }
+ if (data?.redirect) {
+ window.location.href = data.redirect;
+ } else if (redirect) {
+ window.location.href = redirect;
+ } else {
+ window.location.reload();
+ }
+ });
+
+ $('.update-button > .dropdown').dropdown({
+ onChange(_text, _value, $choice) {
+ const url = $choice[0].getAttribute('data-do');
+ if (url) {
+ const buttonText = pullUpdateButton.querySelector('.button-text');
+ if (buttonText) {
+ buttonText.textContent = $choice.text();
+ }
+ pullUpdateButton.setAttribute('data-do', url);
+ }
+ },
+ });
+}
+
+export function initRepoPullRequestAllowMaintainerEdit() {
+ const wrapper = document.getElementById('allow-edits-from-maintainers');
+ if (!wrapper) return;
+ const checkbox = wrapper.querySelector('input[type="checkbox"]');
+ checkbox.addEventListener('input', async () => {
+ const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
+ wrapper.classList.add('is-loading');
+ try {
+ const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})});
+ if (!resp.ok) {
+ throw new Error('Failed to update maintainer edit permission');
+ }
+ const data = await resp.json();
+ checkbox.checked = data.allow_maintainer_edit;
+ } catch (error) {
+ checkbox.checked = !checkbox.checked;
+ console.error(error);
+ showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
+ } finally {
+ wrapper.classList.remove('is-loading');
+ }
+ });
+}
+
+export function initRepoIssueReferenceRepositorySearch() {
+ $('.issue_reference_repository_search')
+ .dropdown({
+ apiSettings: {
+ url: `${appSubUrl}/repo/search?q={query}&limit=20`,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ $.each(response.data, (_r, repo) => {
+ filteredResponse.results.push({
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.full_name,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+ onChange(_value, _text, $choice) {
+ const $form = $choice.closest('form');
+ if (!$form.length) return;
+
+ $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
+ },
+ fullTextSearch: true,
+ });
+}
+
+export function initRepoIssueWipTitle() {
+ $('.title_wip_desc > a').on('click', (e) => {
+ e.preventDefault();
+
+ const $issueTitle = $('#issue_title');
+ $issueTitle.trigger('focus');
+ const value = $issueTitle.val().trim().toUpperCase();
+
+ const wipPrefixes = $('.title_wip_desc').data('wip-prefixes');
+ for (const prefix of wipPrefixes) {
+ if (value.startsWith(prefix.toUpperCase())) {
+ return;
+ }
+ }
+
+ $issueTitle.val(`${wipPrefixes[0]} ${$issueTitle.val()}`);
+ });
+}
+
+export async function updateIssuesMeta(url, action, issue_ids, id) {
+ try {
+ const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
+ if (!response.ok) {
+ throw new Error('Failed to update issues meta');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+export function initRepoIssueComments() {
+ if (!$('.repository.view.issue .timeline').length) return;
+
+ $('.re-request-review').on('click', async function (e) {
+ e.preventDefault();
+ const url = this.getAttribute('data-update-url');
+ const issueId = this.getAttribute('data-issue-id');
+ const id = this.getAttribute('data-id');
+ const isChecked = this.classList.contains('checked');
+
+ await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
+ window.location.reload();
+ });
+
+ document.addEventListener('click', (e) => {
+ const urlTarget = document.querySelector(':target');
+ if (!urlTarget) return;
+
+ const urlTargetId = urlTarget.id;
+ if (!urlTargetId) return;
+
+ if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
+
+ if (!e.target.closest(`#${urlTargetId}`)) {
+ const scrollPosition = $(window).scrollTop();
+ window.location.hash = '';
+ $(window).scrollTop(scrollPosition);
+ window.history.pushState(null, null, ' ');
+ }
+ });
+}
+
+export async function handleReply($el) {
+ hideElem($el);
+ const $form = $el.closest('.comment-code-cloud').find('.comment-form');
+ showElem($form);
+
+ const $textarea = $form.find('textarea');
+ let editor = getComboMarkdownEditor($textarea);
+ if (!editor) {
+ // FIXME: the initialization of the dropzone is not consistent.
+ // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
+ // When the form is submitted and partially reload, none of them is initialized.
+ const dropzone = $form.find('.dropzone')[0];
+ if (!dropzone.dropzone) initDropzone(dropzone);
+ editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
+ }
+ editor.focus();
+ return editor;
+}
+
+export function initRepoPullRequestReview() {
+ if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) {
+ // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
+ if (window.history.scrollRestoration !== 'manual') {
+ window.history.scrollRestoration = 'manual';
+ }
+ const commentDiv = document.querySelector(window.location.hash);
+ if (commentDiv) {
+ // get the name of the parent id
+ const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
+ if (groupID && groupID.startsWith('code-comments-')) {
+ const id = groupID.slice(14);
+ const ancestorDiffBox = commentDiv.closest('.diff-file-box');
+ // on pages like conversation, there is no diff header
+ const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
+
+ // offset is for scrolling
+ let offset = 30;
+ if (diffHeader) {
+ offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
+ }
+
+ hideElem(`#show-outdated-${id}`);
+ showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
+ // if the comment box is folded, expand it
+ if (ancestorDiffBox?.getAttribute('data-folded') === 'true') {
+ setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
+ }
+
+ window.scrollTo({
+ top: $(commentDiv).offset().top - offset,
+ behavior: 'instant',
+ });
+ }
+ }
+ } else if (window.history.scrollRestoration === 'manual') {
+ // reset scrollRestoration to 'auto' if there is no hash in url and we set it to 'manual' before
+ window.history.scrollRestoration = 'auto';
+ }
+
+ $(document).on('click', '.show-outdated', function (e) {
+ e.preventDefault();
+ const id = this.getAttribute('data-comment');
+ hideElem(this);
+ showElem(`#code-comments-${id}`);
+ showElem(`#code-preview-${id}`);
+ showElem(`#hide-outdated-${id}`);
+ });
+
+ $(document).on('click', '.hide-outdated', function (e) {
+ e.preventDefault();
+ const id = this.getAttribute('data-comment');
+ hideElem(this);
+ hideElem(`#code-comments-${id}`);
+ hideElem(`#code-preview-${id}`);
+ showElem(`#show-outdated-${id}`);
+ });
+
+ $(document).on('click', 'button.comment-form-reply', async function (e) {
+ e.preventDefault();
+ await handleReply($(this));
+ });
+
+ const $reviewBox = $('.review-box-panel');
+ if ($reviewBox.length === 1) {
+ const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
+ }
+
+ // The following part is only for diff views
+ if (!$('.repository.pull.diff').length) return;
+
+ const $reviewBtn = $('.js-btn-review');
+ const $panel = $reviewBtn.parent().find('.review-box-panel');
+ const $closeBtn = $panel.find('.close');
+
+ if ($reviewBtn.length && $panel.length) {
+ const tippy = createTippy($reviewBtn[0], {
+ content: $panel[0],
+ placement: 'bottom',
+ trigger: 'click',
+ maxWidth: 'none',
+ interactive: true,
+ hideOnClick: true,
+ });
+
+ $closeBtn.on('click', (e) => {
+ e.preventDefault();
+ tippy.hide();
+ });
+ }
+
+ $(document).on('click', '.add-code-comment', async function (e) {
+ if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
+ e.preventDefault();
+
+ const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
+ const side = this.getAttribute('data-side');
+ const idx = this.getAttribute('data-idx');
+ const path = this.closest('[data-path]')?.getAttribute('data-path');
+ const tr = this.closest('tr');
+ const lineType = tr.getAttribute('data-line-type');
+
+ const ntr = tr.nextElementSibling;
+ let $ntr = $(ntr);
+ if (!ntr?.classList.contains('add-comment')) {
+ $ntr = $(`
+ <tr class="add-comment" data-line-type="${lineType}">
+ ${isSplit ? `
+ <td class="add-comment-left" colspan="4"></td>
+ <td class="add-comment-right" colspan="4"></td>
+ ` : `
+ <td class="add-comment-left add-comment-right" colspan="5"></td>
+ `}
+ </tr>`);
+ $(tr).after($ntr);
+ }
+
+ const $td = $ntr.find(`.add-comment-${side}`);
+ const $commentCloud = $td.find('.comment-code-cloud');
+ if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
+ try {
+ const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
+ const html = await response.text();
+ $td.html(html);
+ $td.find("input[name='line']").val(idx);
+ $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
+ $td.find("input[name='path']").val(path);
+
+ initDropzone($td.find('.dropzone')[0]);
+ const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
+ editor.focus();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+}
+
+export function initRepoIssueReferenceIssue() {
+ // Reference issue
+ $(document).on('click', '.reference-issue', function (event) {
+ const $this = $(this);
+ const content = $(`#${$this.data('target')}`).text();
+ const poster = $this.data('poster-username');
+ const reference = toAbsoluteUrl($this.data('reference'));
+ const $modal = $($this.data('modal'));
+ $modal.find('textarea[name="content"]').val(`${content}\n\n_Originally posted by @${poster} in ${reference}_`);
+ $modal.modal('show');
+
+ event.preventDefault();
+ });
+}
+
+export function initRepoIssueWipToggle() {
+ // Toggle WIP
+ $('.toggle-wip a, .toggle-wip button').on('click', async (e) => {
+ e.preventDefault();
+ const toggleWip = e.currentTarget.closest('.toggle-wip');
+ const title = toggleWip.getAttribute('data-title');
+ const wipPrefixes = JSON.parse(toggleWip.getAttribute('data-wip-prefixes'));
+ const updateUrl = toggleWip.getAttribute('data-update-url');
+ const prefix = wipPrefixes.find((prefix) => title.startsWith(prefix));
+
+ try {
+ const params = new URLSearchParams();
+ params.append('title', prefix !== undefined ? title.slice(prefix.length).trim() : `${wipPrefixes[0].trim()} ${title}`);
+
+ const response = await POST(updateUrl, {data: params});
+ if (!response.ok) {
+ throw new Error('Failed to toggle WIP status');
+ }
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ }
+ });
+}
+
+export function initRepoIssueTitleEdit() {
+ const issueTitleDisplay = document.querySelector('#issue-title-display');
+ const issueTitleEditor = document.querySelector('#issue-title-editor');
+ if (!issueTitleEditor) return;
+
+ const issueTitleInput = issueTitleEditor.querySelector('input');
+ const oldTitle = issueTitleInput.getAttribute('data-old-title');
+ issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
+ hideElem(issueTitleDisplay);
+ hideElem('#pull-desc-display');
+ showElem(issueTitleEditor);
+ showElem('#pull-desc-editor');
+ if (!issueTitleInput.value.trim()) {
+ issueTitleInput.value = oldTitle;
+ }
+ issueTitleInput.focus();
+ });
+ issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
+ hideElem(issueTitleEditor);
+ hideElem('#pull-desc-editor');
+ showElem(issueTitleDisplay);
+ showElem('#pull-desc-display');
+ });
+
+ const pullDescEditor = document.querySelector('#pull-desc-editor'); // it may not exist for a merged PR
+ const prTargetUpdateUrl = pullDescEditor?.getAttribute('data-target-update-url');
+
+ const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
+ const saveAndRefresh = async () => {
+ const newTitle = issueTitleInput.value.trim();
+ try {
+ if (newTitle && newTitle !== oldTitle) {
+ const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
+ if (!resp.ok) {
+ throw new Error(`Failed to update issue title: ${resp.statusText}`);
+ }
+ }
+ if (prTargetUpdateUrl) {
+ const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
+ const oldTargetBranch = document.querySelector('#branch_target').textContent;
+ if (newTargetBranch !== oldTargetBranch) {
+ const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
+ if (!resp.ok) {
+ throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
+ }
+ }
+ }
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ showErrorToast(error.message);
+ }
+ };
+ editSaveButton.addEventListener('click', saveAndRefresh);
+ issueTitleEditor.querySelector('input').addEventListener('ce-quick-submit', saveAndRefresh);
+}
+
+export function initRepoIssueBranchSelect() {
+ document.querySelector('#branch-select')?.addEventListener('click', (e) => {
+ const el = e.target.closest('.item[data-branch]');
+ if (!el) return;
+ const pullTargetBranch = document.querySelector('#pull-target-branch');
+ const baseName = pullTargetBranch.getAttribute('data-basename');
+ const branchNameNew = el.getAttribute('data-branch');
+ const branchNameOld = pullTargetBranch.getAttribute('data-branch');
+ pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
+ pullTargetBranch.setAttribute('data-branch', branchNameNew);
+ });
+}
+
+export function initRepoIssueAssignMe() {
+ // Assign to me button
+ document.querySelector('.ui.assignees.list .item.no-select .select-assign-me')
+ ?.addEventListener('click', (e) => {
+ e.preventDefault();
+ const selectMe = e.target;
+ const noSelect = selectMe.parentElement;
+ const selectorList = document.querySelector('.ui.select-assignees .menu');
+
+ if (selectMe.getAttribute('data-action') === 'update') {
+ (async () => {
+ await updateIssuesMeta(
+ selectMe.getAttribute('data-update-url'),
+ selectMe.getAttribute('data-action'),
+ selectMe.getAttribute('data-issue-id'),
+ selectMe.getAttribute('data-id'),
+ );
+ reloadConfirmDraftComment();
+ })();
+ } else {
+ for (const item of selectorList.querySelectorAll('.item')) {
+ if (item.getAttribute('data-id') === selectMe.getAttribute('data-id')) {
+ item.classList.add('checked');
+ item.querySelector('.octicon-check').classList.remove('tw-invisible');
+ }
+ }
+ document.querySelector(selectMe.getAttribute('data-id-selector')).classList.remove('tw-hidden');
+ noSelect.classList.add('tw-hidden');
+ document.querySelector(selectorList.getAttribute('data-id')).value = selectMe.getAttribute('data-id');
+ return false;
+ }
+ });
+}
+
+export function initSingleCommentEditor($commentForm) {
+ // pages:
+ // * normal new issue/pr page, no status-button
+ // * issue/pr view page, with comment form, has status-button
+ const opts = {};
+ const statusButton = document.getElementById('status-button');
+ if (statusButton) {
+ opts.onContentChanged = (editor) => {
+ const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
+ statusButton.textContent = statusText;
+ };
+ }
+ initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
+}
+
+export function initIssueTemplateCommentEditors($commentForm) {
+ // pages:
+ // * new issue with issue template
+ const $comboFields = $commentForm.find('.combo-editor-dropzone');
+
+ const initCombo = async ($combo) => {
+ const $dropzoneContainer = $combo.find('.form-field-dropzone');
+ const $formField = $combo.find('.form-field-real');
+ const $markdownEditor = $combo.find('.combo-markdown-editor');
+
+ const editor = await initComboMarkdownEditor($markdownEditor, {
+ onContentChanged: (editor) => {
+ $formField.val(editor.value());
+ },
+ });
+
+ $formField.on('focus', async () => {
+ // deactivate all markdown editors
+ showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
+ hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
+ hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
+
+ // activate this markdown editor
+ hideElem($formField);
+ showElem($markdownEditor);
+ showElem($dropzoneContainer);
+
+ await editor.switchToUserPreference();
+ editor.focus();
+ });
+ };
+
+ for (const el of $comboFields) {
+ initCombo($(el));
+ }
+}
+
+// This function used to show and hide archived label on issue/pr
+// page in the sidebar where we select the labels
+// If we have any archived label tagged to issue and pr. We will show that
+// archived label with checked classed otherwise we will hide it
+// with the help of this function.
+// This function runs globally.
+export function initArchivedLabelHandler() {
+ if (!document.querySelector('.archived-label-hint')) return;
+ for (const label of document.querySelectorAll('[data-is-archived]')) {
+ toggleElem(label, label.classList.contains('checked'));
+ }
+}
+
+// Render the issue's title. It converts emojis and code blocks syntax into their respective HTML equivalent.
+export function issueTitleHTML(title) {
+ return title.replaceAll(/:[-+\w]+:/g, (emoji) => emojiHTML(emoji.substring(1, emoji.length - 1)))
+ .replaceAll(/`[^`]+`/g, (code) => `<code class="inline-code-block">${code.substring(1, code.length - 1)}</code>`);
+}