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'; const {appSubUrl, csrfToken} = window.config; 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'); }); } function updateDeadline(deadlineString) { hideElem($('#deadline-err-invalid-date')); $('#deadline-loader').addClass('loading'); let realDeadline = null; if (deadlineString !== '') { const newDate = Date.parse(deadlineString); if (Number.isNaN(newDate)) { $('#deadline-loader').removeClass('loading'); showElem($('#deadline-err-invalid-date')); return false; } realDeadline = new Date(newDate); } $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, { data: JSON.stringify({ due_date: realDeadline, }), headers: { 'X-Csrf-Token': csrfToken, }, contentType: 'application/json', type: 'POST', success() { window.location.reload(); }, error() { $('#deadline-loader').removeClass('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; }); } 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} ${htmlEscape(issue.title) }<div class="text small dont-break-out">${htmlEscape(issue.repository.full_name)}</div>`, value: issue.id, }); }); return filteredResponse; }, cache: false, }, fullTextSearch: true, }); function excludeLabel(item) { const href = $(item).attr('href'); const id = $(item).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); } $('.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 selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected'); if (selectedItems.length > 0) { excludeLabel($(selectedItems[0])); } } }); $('.ui.dropdown.label-filter').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems'); } export function initRepoIssueCommentDelete() { // Delete comment $(document).on('click', '.delete-comment', function () { const $this = $(this); if (window.confirm($this.data('locale'))) { $.post($this.data('url'), { _csrf: csrfToken, }).done(() => { const $conversationHolder = $this.closest('.conversation-holder'); // Check if this was a pending comment. if ($conversationHolder.find('.pending-label').length) { const $counter = $('#review-box .review-comments-counter'); let num = parseInt($counter.attr('data-pending-comment-number')) - 1 || 0; num = Math.max(num, 0); $counter.attr('data-pending-comment-number', num); $counter.text(num); } $(`#${$this.data('comment-id')}`).remove(); if ($conversationHolder.length && !$conversationHolder.find('.comment').length) { const path = $conversationHolder.data('path'); const side = $conversationHolder.data('side'); const idx = $conversationHolder.data('idx'); const lineType = $conversationHolder.closest('tr').data('line-type'); if (lineType === 'same') { $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('invisible'); } else { $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('invisible'); } $conversationHolder.remove(); } }); } return false; }); } 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).on('click', '.cancel-code-comment', (e) => { const form = $(e.currentTarget).closest('form'); if (form.length > 0 && form.hasClass('comment-form')) { form.addClass('gt-hidden'); showElem(form.closest('.comment-code-cloud').find('button.comment-form-reply')); } else { form.closest('.comment-code-cloud').remove(); } }); } export function initRepoPullRequestUpdate() { // Pull Request update button const $pullUpdateButton = $('.update-button > button'); $pullUpdateButton.on('click', function (e) { e.preventDefault(); const $this = $(this); const redirect = $this.data('redirect'); $this.addClass('loading'); $.post($this.data('do'), { _csrf: csrfToken }).done((data) => { 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.data('do'); if ($url) { $pullUpdateButton.find('.button-text').text($choice.text()); $pullUpdateButton.data('do', $url); } } }); } export function initRepoPullRequestMergeInstruction() { $('.show-instruction').on('click', () => { toggleElem($('.instruct-content')); }); } export function initRepoPullRequestAllowMaintainerEdit() { const $checkbox = $('#allow-edits-from-maintainers'); if (!$checkbox.length) return; const promptError = $checkbox.attr('data-prompt-error'); $checkbox.checkbox({ 'onChange': () => { const checked = $checkbox.checkbox('is checked'); let url = $checkbox.attr('data-url'); url += '/set_allow_maintainer_edit'; $checkbox.checkbox('set disabled'); $.ajax({url, type: 'POST', data: {_csrf: csrfToken, allow_maintainer_edit: checked}, error: () => { showTemporaryTooltip($checkbox[0], promptError); }, complete: () => { $checkbox.checkbox('set enabled'); }, }); }, }); } 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'); $form.attr('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, issueIds, elementId) { return $.ajax({ type: 'POST', url, data: { _csrf: csrfToken, action, issue_ids: issueIds, id: elementId, }, }); } export function initRepoIssueComments() { if ($('.repository.view.issue .timeline').length === 0) return; $('.re-request-review').on('click', function (e) { e.preventDefault(); const url = $(this).data('update-url'); const issueId = $(this).data('issue-id'); const id = $(this).data('id'); const isChecked = $(this).hasClass('checked'); updateIssuesMeta( url, isChecked ? 'detach' : 'attach', issueId, id, ).then(() => window.location.reload()); }); $(document).on('click', (event) => { const urlTarget = $(':target'); if (urlTarget.length === 0) return; const urlTargetId = urlTarget.attr('id'); if (!urlTargetId) return; if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return; const $target = $(event.target); if ($target.closest(`#${urlTargetId}`).length === 0) { 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'); form.removeClass('gt-hidden'); const $textarea = form.find('textarea'); let editor = getComboMarkdownEditor($textarea); if (!editor) { 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 = $(window.location.hash); if (commentDiv) { // get the name of the parent id const groupID = commentDiv.closest('div[id^="code-comments-"]').attr('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.find('.diff-file-header'); // offset is for scrolling let offset = 30; if (diffHeader[0]) { offset += $('.diff-detail-box').outerHeight() + diffHeader.outerHeight(); } $(`#show-outdated-${id}`).addClass('gt-hidden'); $(`#code-comments-${id}`).removeClass('gt-hidden'); $(`#code-preview-${id}`).removeClass('gt-hidden'); $(`#hide-outdated-${id}`).removeClass('gt-hidden'); // if the comment box is folded, expand it if (ancestorDiffBox.attr('data-folded') && ancestorDiffBox.attr('data-folded') === 'true') { setFileFolding(ancestorDiffBox[0], ancestorDiffBox.find('.fold-file')[0], false); } window.scrollTo({ top: commentDiv.offset().top - offset, behavior: 'instant' }); } } } $(document).on('click', '.show-outdated', function (e) { e.preventDefault(); const id = $(this).data('comment'); $(this).addClass('gt-hidden'); $(`#code-comments-${id}`).removeClass('gt-hidden'); $(`#code-preview-${id}`).removeClass('gt-hidden'); $(`#hide-outdated-${id}`).removeClass('gt-hidden'); }); $(document).on('click', '.hide-outdated', function (e) { e.preventDefault(); const id = $(this).data('comment'); $(this).addClass('gt-hidden'); $(`#code-comments-${id}`).addClass('gt-hidden'); $(`#code-preview-${id}`).addClass('gt-hidden'); $(`#show-outdated-${id}`).removeClass('gt-hidden'); }); $(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 === 0) { 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).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745 e.preventDefault(); const isSplit = $(this).closest('.code-diff').hasClass('code-diff-split'); const side = $(this).data('side'); const idx = $(this).data('idx'); const path = $(this).closest('[data-path]').data('path'); const tr = $(this).closest('tr'); const lineType = tr.data('line-type'); let ntr = tr.next(); if (!ntr.hasClass('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 === 0 && !ntr.find('button[name="pending_review"]').length) { const html = await $.get($(this).closest('[data-new-comment-url]').attr('data-new-comment-url')); 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); const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor')); editor.focus(); } }); } 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 = $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 wipPrefix = toggleWip.getAttribute('data-wip-prefix'); const updateUrl = toggleWip.getAttribute('data-update-url'); await $.post(updateUrl, { _csrf: csrfToken, title: title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`, }); window.location.reload(); }); } export function initRepoIssueTitleEdit() { // Edit issue title const $issueTitle = $('#issue-title'); const $editInput = $('#edit-title-input input'); const editTitleToggle = function () { toggleElem($issueTitle); toggleElem($('.not-in-edit')); toggleElem($('#edit-title-input')); toggleElem($('#pull-desc')); toggleElem($('#pull-desc-edit')); toggleElem($('.in-edit')); toggleElem($('.new-issue-button')); $('#issue-title-wrapper').toggleClass('edit-active'); $editInput[0].focus(); $editInput[0].select(); return false; }; $('#edit-title').on('click', editTitleToggle); $('#cancel-edit-title').on('click', editTitleToggle); $('#save-edit-title').on('click', editTitleToggle).on('click', function () { const pullrequest_targetbranch_change = function (update_url) { const targetBranch = $('#pull-target-branch').data('branch'); const $branchTarget = $('#branch_target'); if (targetBranch === $branchTarget.text()) { window.location.reload(); return false; } $.post(update_url, { _csrf: csrfToken, target_branch: targetBranch }).always(() => { window.location.reload(); }); }; const pullrequest_target_update_url = $(this).attr('data-target-update-url'); if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) { $editInput.val($issueTitle.text()); pullrequest_targetbranch_change(pullrequest_target_update_url); } else { $.post($(this).attr('data-update-url'), { _csrf: csrfToken, title: $editInput.val() }, (data) => { $editInput.val(data.title); $issueTitle.text(data.title); if (pullrequest_target_update_url) { pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window } else { window.location.reload(); } }); } return false; }); } export function initRepoIssueBranchSelect() { const changeBranchSelect = function () { const selectionTextField = $('#pull-target-branch'); const baseName = selectionTextField.data('basename'); const branchNameNew = $(this).data('branch'); const branchNameOld = selectionTextField.data('branch'); // Replace branch name to keep translation from HTML template selectionTextField.html(selectionTextField.html().replace( `${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}` )); selectionTextField.data('branch', branchNameNew); // update branch name in setting }; $('#branch-select > .item').on('click', changeBranchSelect); } 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 = $('#status-button'); if ($statusButton.length) { $statusButton.on('click', (e) => { e.preventDefault(); $('#status').val($statusButton.data('status-val')); $('#comment-form').trigger('submit'); }); opts.onContentChanged = (editor) => { $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status')); }; } 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)); } }