gitea/web_src/js/features/repo-issue.js

695 lines
23 KiB
JavaScript

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';
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 gt-word-break">${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, .ui.dropdown.select-label').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('gt-invisible');
} else {
$(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('gt-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 = 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 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) {
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));
}
}
// 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'));
}
}