aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-issue.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web_src/js/features/repo-issue.ts')
-rw-r--r--web_src/js/features/repo-issue.ts316
1 files changed, 115 insertions, 201 deletions
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index d2a89682e8..49e8fc40a2 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -1,5 +1,4 @@
-import $ from 'jquery';
-import {htmlEscape} from 'escape-goat';
+import {html, htmlEscape} from '../utils/html.ts';
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
import {
addDelegatedEventListener,
@@ -18,38 +17,40 @@ import {showErrorToast} from '../modules/toast.ts';
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
+import {registerGlobalInitFunc} from '../modules/observer.ts';
const {appSubUrl} = window.config;
-export function initRepoIssueSidebarList() {
+export function initRepoIssueSidebarDependency() {
+ const elDropdown = document.querySelector('#new-dependency-drop-list');
+ if (!elDropdown) return;
+
const issuePageInfo = parseIssuePageInfo();
- const crossRepoSearch = $('#crossRepoSearch').val();
+ const crossRepoSearch = elDropdown.getAttribute('data-issue-cross-repo-search');
let issueSearchUrl = `${issuePageInfo.repoLink}/issues/search?q={query}&type=${issuePageInfo.issueDependencySearchType}`;
if (crossRepoSearch === 'true') {
issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${issuePageInfo.repoId}&type=${issuePageInfo.issueDependencySearchType}`;
}
- fomanticQuery('#new-dependency-drop-list').dropdown({
+ fomanticQuery(elDropdown).dropdown({
fullTextSearch: true,
apiSettings: {
+ cache: false,
+ rawResponse: true,
url: issueSearchUrl,
- onResponse(response) {
- const filteredResponse = {success: true, results: []};
- const currIssueId = $('#new-dependency-drop-list').data('issue-id');
+ onResponse(response: any) {
+ const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
+ const currIssueId = elDropdown.getAttribute('data-issue-id');
// Parse the response from the api to work with our dropdown
- $.each(response, (_i, issue) => {
+ for (const issue of response) {
// Don't list current issue in the dependency list.
- if (issue.id === currIssueId) {
- return;
- }
+ if (String(issue.id) === currIssueId) continue;
filteredResponse.results.push({
- name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
-<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
value: issue.id,
+ name: html`<div class="gt-ellipsis">#${issue.number} ${issue.title}</div><div class="text small tw-break-anywhere">${issue.repository.full_name}</div>`,
});
- });
+ }
return filteredResponse;
},
- cache: false,
},
});
}
@@ -181,24 +182,6 @@ export function initRepoIssueCommentDelete() {
});
}
-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: DOMEvent<MouseEvent>) => {
@@ -214,59 +197,6 @@ export function initRepoIssueCodeCommentCancel() {
});
}
-export function initRepoPullRequestUpdate() {
- // Pull Request update button
- const pullUpdateButton = document.querySelector<HTMLButtonElement>('.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: Response;
- try {
- response = await POST(this.getAttribute('data-do'));
- } catch (error) {
- console.error(error);
- } finally {
- this.classList.remove('is-loading');
- }
- let data: Record<string, any>;
- 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 choiceEl = $choice[0];
- const url = choiceEl.getAttribute('data-do');
- if (url) {
- const buttonText = pullUpdateButton.querySelector('.button-text');
- if (buttonText) {
- buttonText.textContent = choiceEl.textContent;
- }
- pullUpdateButton.setAttribute('data-do', url);
- }
- },
- });
-}
-
-export function initRepoPullRequestMergeInstruction() {
- $('.show-instruction').on('click', () => {
- toggleElem($('.instruct-content'));
- });
-}
-
export function initRepoPullRequestAllowMaintainerEdit() {
const wrapper = document.querySelector('#allow-edits-from-maintainers');
if (!wrapper) return;
@@ -293,54 +223,8 @@ export function initRepoPullRequestAllowMaintainerEdit() {
});
}
-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() as string).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 function initRepoIssueComments() {
- if (!$('.repository.view.issue .timeline').length) return;
+ if (!document.querySelector('.repository.view.issue .timeline')) return;
document.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
const urlTarget = document.querySelector(':target');
@@ -352,15 +236,15 @@ export function initRepoIssueComments() {
if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
if (!e.target.closest(`#${urlTargetId}`)) {
- const scrollPosition = $(window).scrollTop();
- window.location.hash = '';
- $(window).scrollTop(scrollPosition);
+ // if the user clicks outside the comment, remove the hash from the url
+ // use empty hash and state to avoid scrolling
+ window.location.hash = ' ';
window.history.pushState(null, null, ' ');
}
});
}
-export async function handleReply(el) {
+export async function handleReply(el: HTMLElement) {
const form = el.closest('.comment-code-cloud').querySelector('.comment-form');
const textarea = form.querySelector('textarea');
@@ -379,7 +263,7 @@ export function initRepoPullRequestReview() {
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');
+ const ancestorDiffBox = commentDiv.closest<HTMLElement>('.diff-file-box');
hideElem(`#show-outdated-${id}`);
showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
@@ -395,39 +279,37 @@ export function initRepoPullRequestReview() {
}
}
- $(document).on('click', '.show-outdated', function (e) {
+ addDelegatedEventListener(document, 'click', '.show-outdated', (el, e) => {
e.preventDefault();
- const id = this.getAttribute('data-comment');
- hideElem(this);
+ const id = el.getAttribute('data-comment');
+ hideElem(el);
showElem(`#code-comments-${id}`);
showElem(`#code-preview-${id}`);
showElem(`#hide-outdated-${id}`);
});
- $(document).on('click', '.hide-outdated', function (e) {
+ addDelegatedEventListener(document, 'click', '.hide-outdated', (el, e) => {
e.preventDefault();
- const id = this.getAttribute('data-comment');
- hideElem(this);
+ const id = el.getAttribute('data-comment');
+ hideElem(el);
hideElem(`#code-comments-${id}`);
hideElem(`#code-preview-${id}`);
showElem(`#show-outdated-${id}`);
});
- $(document).on('click', 'button.comment-form-reply', async function (e) {
+ addDelegatedEventListener(document, 'click', 'button.comment-form-reply', (el, e) => {
e.preventDefault();
- await handleReply(this);
+ handleReply(el);
});
// 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 (!document.querySelector('.repository.pull.diff')) return;
- if ($reviewBtn.length && $panel.length) {
- const tippy = createTippy($reviewBtn[0], {
- content: $panel[0],
+ const elReviewBtn = document.querySelector('.js-btn-review');
+ const elReviewPanel = document.querySelector('.review-box-panel.tippy-target');
+ if (elReviewBtn && elReviewPanel) {
+ const tippy = createTippy(elReviewBtn, {
+ content: elReviewPanel,
theme: 'default',
placement: 'bottom',
trigger: 'click',
@@ -435,11 +317,7 @@ export function initRepoPullRequestReview() {
interactive: true,
hideOnClick: true,
});
-
- $closeBtn.on('click', (e) => {
- e.preventDefault();
- tippy.hide();
- });
+ elReviewPanel.querySelector('.close').addEventListener('click', () => tippy.hide());
}
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
@@ -480,43 +358,79 @@ export function initRepoPullRequestReview() {
}
export function initRepoIssueReferenceIssue() {
+ const elDropdown = document.querySelector('.issue_reference_repository_search');
+ if (!elDropdown) return;
+ const form = elDropdown.closest('form');
+ fomanticQuery(elDropdown).dropdown({
+ fullTextSearch: true,
+ apiSettings: {
+ cache: false,
+ rawResponse: true,
+ url: `${appSubUrl}/repo/search?q={query}&limit=20`,
+ onResponse(response: any) {
+ const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
+ for (const repo of response.data) {
+ filteredResponse.results.push({
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.full_name,
+ });
+ }
+ return filteredResponse;
+ },
+ },
+ onChange(_value: string, _text: string, _$choice: any) {
+ form.setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
+ },
+ });
+
// Reference issue
- $(document).on('click', '.reference-issue', function (e) {
- const target = this.getAttribute('data-target');
+ addDelegatedEventListener(document, 'click', '.reference-issue', (el, e) => {
+ e.preventDefault();
+ const target = el.getAttribute('data-target');
const content = document.querySelector(`#${target}`)?.textContent ?? '';
- const poster = this.getAttribute('data-poster-username');
- const reference = toAbsoluteUrl(this.getAttribute('data-reference'));
- const modalSelector = this.getAttribute('data-modal');
+ const poster = el.getAttribute('data-poster-username');
+ const reference = toAbsoluteUrl(el.getAttribute('data-reference'));
+ const modalSelector = el.getAttribute('data-modal');
const modal = document.querySelector(modalSelector);
- const textarea = modal.querySelector('textarea[name="content"]');
+ const textarea = modal.querySelector<HTMLTextAreaElement>('textarea[name="content"]');
textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
- $(modal).modal('show');
- e.preventDefault();
+ fomanticQuery(modal).modal('show');
});
}
+export function initRepoIssueWipNewTitle() {
+ // Toggle WIP for new PR
+ queryElems(document, '.title_wip_desc > a', (el) => el.addEventListener('click', (e) => {
+ e.preventDefault();
+ const wipPrefixes = JSON.parse(el.closest('.title_wip_desc').getAttribute('data-wip-prefixes'));
+ const titleInput = document.querySelector<HTMLInputElement>('#issue_title');
+ const titleValue = titleInput.value;
+ for (const prefix of wipPrefixes) {
+ if (titleValue.startsWith(prefix.toUpperCase())) {
+ return;
+ }
+ }
+ titleInput.value = `${wipPrefixes[0]} ${titleValue}`;
+ }));
+}
+
export function initRepoIssueWipToggle() {
- // Toggle WIP
- $('.toggle-wip a, .toggle-wip button').on('click', async (e) => {
+ // Toggle WIP for existing PR
+ registerGlobalInitFunc('initPullRequestWipToggle', (toggleWip) => toggleWip.addEventListener('click', async (e) => {
e.preventDefault();
- const toggleWip = e.currentTarget.closest('.toggle-wip');
const title = toggleWip.getAttribute('data-title');
const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
const updateUrl = toggleWip.getAttribute('data-update-url');
- try {
- const params = new URLSearchParams();
- params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.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);
+ const params = new URLSearchParams();
+ params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
+ const response = await POST(updateUrl, {data: params});
+ if (!response.ok) {
+ showErrorToast(`Failed to toggle 'work in progress' status`);
+ return;
}
- });
+ window.location.reload();
+ }));
}
export function initRepoIssueTitleEdit() {
@@ -589,11 +503,11 @@ export function initRepoIssueBranchSelect() {
});
}
-async function initSingleCommentEditor($commentForm) {
+async function initSingleCommentEditor(commentForm: HTMLFormElement) {
// pages:
// * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content)
// * issue/pr view page: with comment form, has status-button and comment-button
- const editor = await initComboMarkdownEditor($commentForm[0].querySelector('.combo-markdown-editor'));
+ const editor = await initComboMarkdownEditor(commentForm.querySelector('.combo-markdown-editor'));
const statusButton = document.querySelector<HTMLButtonElement>('#status-button');
const commentButton = document.querySelector<HTMLButtonElement>('#comment-button');
const syncUiState = () => {
@@ -611,27 +525,27 @@ async function initSingleCommentEditor($commentForm) {
syncUiState();
}
-function initIssueTemplateCommentEditors($commentForm) {
+function initIssueTemplateCommentEditors(commentForm: HTMLFormElement) {
// pages:
// * new issue with issue template
- const $comboFields = $commentForm.find('.combo-editor-dropzone');
+ const comboFields = commentForm.querySelectorAll<HTMLElement>('.combo-editor-dropzone');
const initCombo = async (elCombo: HTMLElement) => {
- const $formField = $(elCombo.querySelector('.form-field-real'));
+ const fieldTextarea = elCombo.querySelector<HTMLTextAreaElement>('.form-field-real');
const dropzoneContainer = elCombo.querySelector<HTMLElement>('.form-field-dropzone');
const markdownEditor = elCombo.querySelector<HTMLElement>('.combo-markdown-editor');
const editor = await initComboMarkdownEditor(markdownEditor);
- editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, () => $formField.val(editor.value()));
+ editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, () => fieldTextarea.value = editor.value());
- $formField.on('focus', async () => {
+ fieldTextarea.addEventListener('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'));
+ showElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-real'));
+ hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .combo-markdown-editor'));
+ hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-dropzone'));
// activate this markdown editor
- hideElem($formField);
+ hideElem(fieldTextarea);
showElem(markdownEditor);
showElem(dropzoneContainer);
@@ -640,21 +554,21 @@ function initIssueTemplateCommentEditors($commentForm) {
});
};
- for (const el of $comboFields) {
+ for (const el of comboFields) {
initCombo(el);
}
}
export function initRepoCommentFormAndSidebar() {
- const $commentForm = $('.comment.form');
- if (!$commentForm.length) return;
+ const commentForm = document.querySelector<HTMLFormElement>('.comment.form');
+ if (!commentForm) return;
- if ($commentForm.find('.field.combo-editor-dropzone').length) {
+ if (commentForm.querySelector('.field.combo-editor-dropzone')) {
// at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
- initIssueTemplateCommentEditors($commentForm);
- } else if ($commentForm.find('.combo-markdown-editor').length) {
+ initIssueTemplateCommentEditors(commentForm);
+ } else if (commentForm.querySelector('.combo-markdown-editor')) {
// it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message
- initSingleCommentEditor($commentForm);
+ initSingleCommentEditor(commentForm);
}
initRepoIssueSidebar();