123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- import $ from 'jquery';
- import {
- initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
- initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
- initRepoIssueTitleEdit, initRepoIssueWipToggle,
- initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
- } from './repo-issue.js';
- import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
- import {svg} from '../svg.js';
- import {htmlEscape} from 'escape-goat';
- import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
- import {
- initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
- } from './repo-common.js';
- import {initCitationFileCopyContent} from './citation.js';
- import {initCompLabelEdit} from './comp/LabelEdit.js';
- import {initRepoDiffConversationNav} from './repo-diff.js';
- import {createDropzone} from './dropzone.js';
- import {initCommentContent, initMarkupContent} from '../markup/content.js';
- import {initCompReactionSelector} from './comp/ReactionSelector.js';
- import {initRepoSettingBranches} from './repo-settings.js';
- import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
- import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
- import {hideElem, showElem} from '../utils/dom.js';
- import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
- import {attachRefIssueContextPopup} from './contextpopup.js';
- import {POST, GET} from '../modules/fetch.js';
-
- const {csrfToken} = window.config;
-
- // if there are draft comments, confirm before reloading, to avoid losing comments
- function reloadConfirmDraftComment() {
- const commentTextareas = [
- document.querySelector('.edit-content-zone:not(.gt-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 initRepoCommentForm() {
- const $commentForm = $('.comment.form');
- if ($commentForm.length === 0) {
- return;
- }
-
- if ($commentForm.find('.field.combo-editor-dropzone').length) {
- // 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) {
- // 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);
- }
-
- function initBranchSelector() {
- const $selectBranch = $('.ui.select-branch');
- const $branchMenu = $selectBranch.find('.reference-list-menu');
- const $isNewIssue = $branchMenu.hasClass('new-issue');
- $branchMenu.find('.item:not(.no-select)').on('click', async function () {
- const selectedValue = $(this).data('id');
- const editMode = $('#editing_mode').val();
- $($(this).data('id-selector')).val(selectedValue);
- if ($isNewIssue) {
- $selectBranch.find('.ui .branch-name').text($(this).data('name'));
- return;
- }
-
- if (editMode === 'true') {
- const $form = $('#update_issueref_form');
- const params = new URLSearchParams();
- params.append('ref', selectedValue);
- try {
- await POST($form.attr('action'), {data: params});
- window.location.reload();
- } catch (error) {
- console.error(error);
- }
- } else if (editMode === '') {
- $selectBranch.find('.ui .branch-name').text(selectedValue);
- }
- });
- $selectBranch.find('.reference.column').on('click', function () {
- hideElem($selectBranch.find('.scrolling.reference-list-menu'));
- $selectBranch.find('.reference .text').removeClass('black');
- showElem($($(this).data('target')));
- $(this).find('.text').addClass('black');
- return false;
- });
- }
-
- initBranchSelector();
-
- // List submits
- function initListSubmits(selector, outerSelector) {
- const $list = $(`.ui.${outerSelector}.list`);
- const $noSelect = $list.find('.no-select');
- const $listMenu = $(`.${selector} .menu`);
- let hasUpdateAction = $listMenu.data('action') === 'update';
- const items = {};
-
- $(`.${selector}`).dropdown({
- 'action': 'nothing', // do not hide the menu if user presses Enter
- fullTextSearch: 'exact',
- async onHide() {
- hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
- if (hasUpdateAction) {
- // TODO: Add batch functionality and make this 1 network request.
- const itemEntries = Object.entries(items);
- for (const [elementId, item] of itemEntries) {
- await updateIssuesMeta(
- item['update-url'],
- item.action,
- item['issue-id'],
- elementId,
- );
- }
- if (itemEntries.length) {
- reloadConfirmDraftComment();
- }
- }
- },
- });
-
- $listMenu.find('.item:not(.no-select)').on('click', function (e) {
- e.preventDefault();
- if ($(this).hasClass('ban-change')) {
- return false;
- }
-
- hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
-
- const $clickedItem = $(this);
- const scope = $(this).attr('data-scope');
-
- $(this).parent().find('.item').each(function () {
- if (scope) {
- // Enable only clicked item for scoped labels
- if ($(this).attr('data-scope') !== scope) {
- return true;
- }
- if (!$(this).is($clickedItem) && !$(this).hasClass('checked')) {
- return true;
- }
- } else if (!$(this).is($clickedItem)) {
- // Toggle for other labels
- return true;
- }
-
- if ($(this).hasClass('checked')) {
- $(this).removeClass('checked');
- $(this).find('.octicon-check').addClass('tw-invisible');
- if (hasUpdateAction) {
- if (!($(this).data('id') in items)) {
- items[$(this).data('id')] = {
- 'update-url': $listMenu.data('update-url'),
- action: 'detach',
- 'issue-id': $listMenu.data('issue-id'),
- };
- } else {
- delete items[$(this).data('id')];
- }
- }
- } else {
- $(this).addClass('checked');
- $(this).find('.octicon-check').removeClass('tw-invisible');
- if (hasUpdateAction) {
- if (!($(this).data('id') in items)) {
- items[$(this).data('id')] = {
- 'update-url': $listMenu.data('update-url'),
- action: 'attach',
- 'issue-id': $listMenu.data('issue-id'),
- };
- } else {
- delete items[$(this).data('id')];
- }
- }
- }
- });
-
- // TODO: Which thing should be done for choosing review requests
- // to make chosen items be shown on time here?
- if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
- return false;
- }
-
- const listIds = [];
- $(this).parent().find('.item').each(function () {
- if ($(this).hasClass('checked')) {
- listIds.push($(this).data('id'));
- $($(this).data('id-selector')).removeClass('gt-hidden');
- } else {
- $($(this).data('id-selector')).addClass('gt-hidden');
- }
- });
- if (listIds.length === 0) {
- $noSelect.removeClass('gt-hidden');
- } else {
- $noSelect.addClass('gt-hidden');
- }
- $($(this).parent().data('id')).val(listIds.join(','));
- return false;
- });
- $listMenu.find('.no-select.item').on('click', function (e) {
- e.preventDefault();
- if (hasUpdateAction) {
- (async () => {
- await updateIssuesMeta(
- $listMenu.data('update-url'),
- 'clear',
- $listMenu.data('issue-id'),
- '',
- );
- reloadConfirmDraftComment();
- })();
- }
-
- $(this).parent().find('.item').each(function () {
- $(this).removeClass('checked');
- $(this).find('.octicon-check').addClass('tw-invisible');
- });
-
- if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
- return false;
- }
-
- $list.find('.item').each(function () {
- $(this).addClass('gt-hidden');
- });
- $noSelect.removeClass('gt-hidden');
- $($(this).parent().data('id')).val('');
- });
- }
-
- // Init labels and assignees
- initListSubmits('select-label', 'labels');
- initListSubmits('select-assignees', 'assignees');
- initListSubmits('select-assignees-modify', 'assignees');
- initListSubmits('select-reviewers-modify', 'assignees');
-
- function selectItem(select_id, input_id) {
- const $menu = $(`${select_id} .menu`);
- const $list = $(`.ui${select_id}.list`);
- const hasUpdateAction = $menu.data('action') === 'update';
-
- $menu.find('.item:not(.no-select)').on('click', function () {
- $(this).parent().find('.item').each(function () {
- $(this).removeClass('selected active');
- });
-
- $(this).addClass('selected active');
- if (hasUpdateAction) {
- (async () => {
- await updateIssuesMeta(
- $menu.data('update-url'),
- '',
- $menu.data('issue-id'),
- $(this).data('id'),
- );
- reloadConfirmDraftComment();
- })();
- }
-
- let icon = '';
- if (input_id === '#milestone_id') {
- icon = svg('octicon-milestone', 18, 'gt-mr-3');
- } else if (input_id === '#project_id') {
- icon = svg('octicon-project', 18, 'gt-mr-3');
- } else if (input_id === '#assignee_id') {
- icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
- }
-
- $list.find('.selected').html(`
- <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
- ${icon}
- ${htmlEscape($(this).text())}
- </a>
- `);
-
- $(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
- $(input_id).val($(this).data('id'));
- });
- $menu.find('.no-select.item').on('click', function () {
- $(this).parent().find('.item:not(.no-select)').each(function () {
- $(this).removeClass('selected active');
- });
-
- if (hasUpdateAction) {
- (async () => {
- await updateIssuesMeta(
- $menu.data('update-url'),
- '',
- $menu.data('issue-id'),
- $(this).data('id'),
- );
- reloadConfirmDraftComment();
- })();
- }
-
- $list.find('.selected').html('');
- $list.find('.no-select').removeClass('gt-hidden');
- $(input_id).val('');
- });
- }
-
- // Milestone, Assignee, Project
- selectItem('.select-project', '#project_id');
- selectItem('.select-milestone', '#milestone_id');
- selectItem('.select-assignee', '#assignee_id');
- }
-
- async function onEditContent(event) {
- event.preventDefault();
-
- const $segment = $(this).closest('.header').next();
- const $editContentZone = $segment.find('.edit-content-zone');
- const $renderContent = $segment.find('.render-content');
- const $rawContent = $segment.find('.raw-content');
-
- let comboMarkdownEditor;
-
- const setupDropzone = async ($dropzone) => {
- if ($dropzone.length === 0) return null;
-
- let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
- let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
- const dz = await createDropzone($dropzone[0], {
- url: $dropzone.attr('data-upload-url'),
- headers: {'X-Csrf-Token': csrfToken},
- maxFiles: $dropzone.attr('data-max-file'),
- maxFilesize: $dropzone.attr('data-max-size'),
- acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
- addRemoveLinks: true,
- dictDefaultMessage: $dropzone.attr('data-default-message'),
- dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
- dictFileTooBig: $dropzone.attr('data-file-too-big'),
- dictRemoveFile: $dropzone.attr('data-remove-file'),
- timeout: 0,
- thumbnailMethod: 'contain',
- thumbnailWidth: 480,
- thumbnailHeight: 480,
- init() {
- this.on('success', (file, data) => {
- file.uuid = data.uuid;
- fileUuidDict[file.uuid] = {submitted: false};
- const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
- $dropzone.find('.files').append($input);
- });
- this.on('removedfile', async (file) => {
- if (disableRemovedfileEvent) return;
- $(`#${file.uuid}`).remove();
- if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
- try {
- await POST($dropzone.attr('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
- } catch (error) {
- console.error(error);
- }
- }
- });
- this.on('submit', () => {
- $.each(fileUuidDict, (fileUuid) => {
- fileUuidDict[fileUuid].submitted = true;
- });
- });
- this.on('reload', async () => {
- try {
- const response = await GET($editContentZone.attr('data-attachment-url'));
- const data = await response.json();
- // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
- disableRemovedfileEvent = true;
- dz.removeAllFiles(true);
- $dropzone.find('.files').empty();
- fileUuidDict = {};
- disableRemovedfileEvent = false;
-
- for (const attachment of data) {
- const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
- dz.emit('addedfile', attachment);
- dz.emit('thumbnail', attachment, imgSrc);
- dz.emit('complete', attachment);
- dz.files.push(attachment);
- fileUuidDict[attachment.uuid] = {submitted: true};
- $dropzone.find(`img[src='${imgSrc}']`)[0].style.maxWidth = '100%';
- const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
- $dropzone.find('.files').append($input);
- }
- } catch (error) {
- console.error(error);
- }
- });
- },
- });
- dz.emit('reload');
- return dz;
- };
-
- const cancelAndReset = (dz) => {
- showElem($renderContent);
- hideElem($editContentZone);
- if (dz) {
- dz.emit('reload');
- }
- };
-
- const saveAndRefresh = async (dz) => {
- showElem($renderContent);
- hideElem($editContentZone);
-
- try {
- const params = new URLSearchParams({
- content: comboMarkdownEditor.value(),
- context: $editContentZone.attr('data-context'),
- });
- for (const file of dz.files) params.append('files[]', file.uuid);
-
- const response = await POST($editContentZone.attr('data-update-url'), {data: params});
- const data = await response.json();
- if (!data.content) {
- $renderContent.html($('#no-content').html());
- $rawContent.text('');
- } else {
- $renderContent.html(data.content);
- $rawContent.text(comboMarkdownEditor.value());
- const $refIssues = $renderContent.find('p .ref-issue');
- attachRefIssueContextPopup($refIssues);
- }
- const $content = $segment;
- if (!$content.find('.dropzone-attachments').length) {
- if (data.attachments !== '') {
- $content[0].append(data.attachments);
- }
- } else if (data.attachments === '') {
- $content.find('.dropzone-attachments').remove();
- } else {
- $content.find('.dropzone-attachments')[0].outerHTML = data.attachments;
- }
- if (dz) {
- dz.emit('submit');
- dz.emit('reload');
- }
- initMarkupContent();
- initCommentContent();
- } catch (error) {
- console.error(error);
- }
- };
-
- if (!$editContentZone.html()) {
- $editContentZone.html($('#issue-comment-editor-template').html());
- comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
-
- const $dropzone = $editContentZone.find('.dropzone');
- const dz = await setupDropzone($dropzone);
- $editContentZone.find('.cancel.button').on('click', (e) => {
- e.preventDefault();
- cancelAndReset(dz);
- });
- $editContentZone.find('.save.button').on('click', (e) => {
- e.preventDefault();
- saveAndRefresh(dz);
- });
- } else {
- comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
- }
-
- // Show write/preview tab and copy raw content as needed
- showElem($editContentZone);
- hideElem($renderContent);
- if (!comboMarkdownEditor.value()) {
- comboMarkdownEditor.value($rawContent.text());
- }
- comboMarkdownEditor.focus();
- }
-
- export function initRepository() {
- if ($('.page-content.repository').length === 0) {
- return;
- }
-
- initRepoBranchTagSelector('.js-branch-tag-selector');
-
- // Options
- if ($('.repository.settings.options').length > 0) {
- // Enable or select internal/external wiki system and issue tracker.
- $('.enable-system').on('change', function () {
- if (this.checked) {
- $($(this).data('target')).removeClass('disabled');
- if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
- } else {
- $($(this).data('target')).addClass('disabled');
- if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
- }
- });
- $('.enable-system-radio').on('change', function () {
- if (this.value === 'false') {
- $($(this).data('target')).addClass('disabled');
- if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
- } else if (this.value === 'true') {
- $($(this).data('target')).removeClass('disabled');
- if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled');
- }
- });
- const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
- $trackerIssueStyleRadios.on('change input', () => {
- const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
- $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
- });
- }
-
- // Labels
- initCompLabelEdit('.repository.labels');
-
- // Milestones
- if ($('.repository.new.milestone').length > 0) {
- $('#clear-date').on('click', () => {
- $('#deadline').val('');
- return false;
- });
- }
-
- // Repo Creation
- if ($('.repository.new.repo').length > 0) {
- $('input[name="gitignores"], input[name="license"]').on('change', () => {
- const gitignores = $('input[name="gitignores"]').val();
- const license = $('input[name="license"]').val();
- if (gitignores || license) {
- document.querySelector('input[name="auto_init"]').checked = true;
- }
- });
- }
-
- // Compare or pull request
- const $repoDiff = $('.repository.diff');
- if ($repoDiff.length) {
- initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
- initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
- }
-
- initRepoCloneLink();
- initCitationFileCopyContent();
- initRepoSettingBranches();
-
- // Issues
- if ($('.repository.view.issue').length > 0) {
- initRepoIssueCommentEdit();
-
- initRepoIssueBranchSelect();
- initRepoIssueTitleEdit();
- initRepoIssueWipToggle();
- initRepoIssueComments();
-
- initRepoDiffConversationNav();
- initRepoIssueReferenceIssue();
-
- initRepoIssueCommentDelete();
- initRepoIssueDependencyDelete();
- initRepoIssueCodeCommentCancel();
- initRepoPullRequestUpdate();
- initCompReactionSelector($(document));
-
- initRepoPullRequestMergeForm();
- initRepoPullRequestCommitStatus();
- }
-
- // Pull request
- const $repoComparePull = $('.repository.compare.pull');
- if ($repoComparePull.length > 0) {
- // show pull request form
- $repoComparePull.find('button.show-form').on('click', function (e) {
- e.preventDefault();
- hideElem($(this).parent());
-
- const $form = $repoComparePull.find('.pullrequest-form');
- showElem($form);
- });
- }
-
- initUnicodeEscapeButton();
- }
-
- function initRepoIssueCommentEdit() {
- // Edit issue or comment content
- $(document).on('click', '.edit-content', onEditContent);
-
- // Quote reply
- $(document).on('click', '.quote-reply', async function (event) {
- event.preventDefault();
- const target = $(this).data('target');
- const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
- const content = `> ${quote}\n\n`;
- let editor;
- if ($(this).hasClass('quote-reply-diff')) {
- const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
- editor = await handleReply($replyBtn);
- } else {
- // for normal issue/comment page
- editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
- }
- if (editor) {
- if (editor.value()) {
- editor.value(`${editor.value()}\n\n${content}`);
- } else {
- editor.value(content);
- }
- editor.focus();
- editor.moveCursorToEnd();
- }
- });
- }
|