code.gitea.io/gitea@v1.21.7/web_src/js/features/repo-legacy.js (about)

     1  import $ from 'jquery';
     2  import {
     3    initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
     4    initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
     5    initRepoIssueTitleEdit, initRepoIssueWipToggle,
     6    initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
     7  } from './repo-issue.js';
     8  import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
     9  import {svg} from '../svg.js';
    10  import {htmlEscape} from 'escape-goat';
    11  import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
    12  import {
    13    initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
    14  } from './repo-common.js';
    15  import {initCitationFileCopyContent} from './citation.js';
    16  import {initCompLabelEdit} from './comp/LabelEdit.js';
    17  import {initRepoDiffConversationNav} from './repo-diff.js';
    18  import {createDropzone} from './dropzone.js';
    19  import {initCommentContent, initMarkupContent} from '../markup/content.js';
    20  import {initCompReactionSelector} from './comp/ReactionSelector.js';
    21  import {initRepoSettingBranches} from './repo-settings.js';
    22  import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
    23  import {hideElem, showElem} from '../utils/dom.js';
    24  import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
    25  import {attachRefIssueContextPopup} from './contextpopup.js';
    26  
    27  const {csrfToken} = window.config;
    28  
    29  // if there are draft comments, confirm before reloading, to avoid losing comments
    30  function reloadConfirmDraftComment() {
    31    const commentTextareas = [
    32      document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
    33      document.querySelector('#comment-form textarea'),
    34    ];
    35    for (const textarea of commentTextareas) {
    36      // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
    37      // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
    38      if (textarea && textarea.value.trim().length > 10) {
    39        textarea.parentElement.scrollIntoView();
    40        if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
    41          return;
    42        }
    43        break;
    44      }
    45    }
    46    window.location.reload();
    47  }
    48  
    49  export function initRepoCommentForm() {
    50    const $commentForm = $('.comment.form');
    51    if ($commentForm.length === 0) {
    52      return;
    53    }
    54  
    55    if ($commentForm.find('.field.combo-editor-dropzone').length) {
    56      // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
    57      initIssueTemplateCommentEditors($commentForm);
    58    } else if ($commentForm.find('.combo-markdown-editor').length) {
    59      // it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message
    60      initSingleCommentEditor($commentForm);
    61    }
    62  
    63    function initBranchSelector() {
    64      const $selectBranch = $('.ui.select-branch');
    65      const $branchMenu = $selectBranch.find('.reference-list-menu');
    66      const $isNewIssue = $branchMenu.hasClass('new-issue');
    67      $branchMenu.find('.item:not(.no-select)').on('click', function () {
    68        const selectedValue = $(this).data('id');
    69        const editMode = $('#editing_mode').val();
    70        $($(this).data('id-selector')).val(selectedValue);
    71        if ($isNewIssue) {
    72          $selectBranch.find('.ui .branch-name').text($(this).data('name'));
    73          return;
    74        }
    75  
    76        if (editMode === 'true') {
    77          const form = $('#update_issueref_form');
    78          $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload());
    79        } else if (editMode === '') {
    80          $selectBranch.find('.ui .branch-name').text(selectedValue);
    81        }
    82      });
    83      $selectBranch.find('.reference.column').on('click', function () {
    84        hideElem($selectBranch.find('.scrolling.reference-list-menu'));
    85        $selectBranch.find('.reference .text').removeClass('black');
    86        showElem($($(this).data('target')));
    87        $(this).find('.text').addClass('black');
    88        return false;
    89      });
    90    }
    91  
    92    initBranchSelector();
    93  
    94    // List submits
    95    function initListSubmits(selector, outerSelector) {
    96      const $list = $(`.ui.${outerSelector}.list`);
    97      const $noSelect = $list.find('.no-select');
    98      const $listMenu = $(`.${selector} .menu`);
    99      let hasUpdateAction = $listMenu.data('action') === 'update';
   100      const items = {};
   101  
   102      $(`.${selector}`).dropdown({
   103        'action': 'nothing', // do not hide the menu if user presses Enter
   104        fullTextSearch: 'exact',
   105        async onHide() {
   106          hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
   107          if (hasUpdateAction) {
   108            // TODO: Add batch functionality and make this 1 network request.
   109            const itemEntries = Object.entries(items);
   110            for (const [elementId, item] of itemEntries) {
   111              await updateIssuesMeta(
   112                item['update-url'],
   113                item.action,
   114                item['issue-id'],
   115                elementId,
   116              );
   117            }
   118            if (itemEntries.length) {
   119              reloadConfirmDraftComment();
   120            }
   121          }
   122        },
   123      });
   124  
   125      $listMenu.find('.item:not(.no-select)').on('click', function (e) {
   126        e.preventDefault();
   127        if ($(this).hasClass('ban-change')) {
   128          return false;
   129        }
   130  
   131        hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
   132  
   133        const clickedItem = $(this);
   134        const scope = $(this).attr('data-scope');
   135  
   136        $(this).parent().find('.item').each(function () {
   137          if (scope) {
   138            // Enable only clicked item for scoped labels
   139            if ($(this).attr('data-scope') !== scope) {
   140              return true;
   141            }
   142            if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) {
   143              return true;
   144            }
   145          } else if (!$(this).is(clickedItem)) {
   146            // Toggle for other labels
   147            return true;
   148          }
   149  
   150          if ($(this).hasClass('checked')) {
   151            $(this).removeClass('checked');
   152            $(this).find('.octicon-check').addClass('gt-invisible');
   153            if (hasUpdateAction) {
   154              if (!($(this).data('id') in items)) {
   155                items[$(this).data('id')] = {
   156                  'update-url': $listMenu.data('update-url'),
   157                  action: 'detach',
   158                  'issue-id': $listMenu.data('issue-id'),
   159                };
   160              } else {
   161                delete items[$(this).data('id')];
   162              }
   163            }
   164          } else {
   165            $(this).addClass('checked');
   166            $(this).find('.octicon-check').removeClass('gt-invisible');
   167            if (hasUpdateAction) {
   168              if (!($(this).data('id') in items)) {
   169                items[$(this).data('id')] = {
   170                  'update-url': $listMenu.data('update-url'),
   171                  action: 'attach',
   172                  'issue-id': $listMenu.data('issue-id'),
   173                };
   174              } else {
   175                delete items[$(this).data('id')];
   176              }
   177            }
   178          }
   179        });
   180  
   181        // TODO: Which thing should be done for choosing review requests
   182        // to make chosen items be shown on time here?
   183        if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
   184          return false;
   185        }
   186  
   187        const listIds = [];
   188        $(this).parent().find('.item').each(function () {
   189          if ($(this).hasClass('checked')) {
   190            listIds.push($(this).data('id'));
   191            $($(this).data('id-selector')).removeClass('gt-hidden');
   192          } else {
   193            $($(this).data('id-selector')).addClass('gt-hidden');
   194          }
   195        });
   196        if (listIds.length === 0) {
   197          $noSelect.removeClass('gt-hidden');
   198        } else {
   199          $noSelect.addClass('gt-hidden');
   200        }
   201        $($(this).parent().data('id')).val(listIds.join(','));
   202        return false;
   203      });
   204      $listMenu.find('.no-select.item').on('click', function (e) {
   205        e.preventDefault();
   206        if (hasUpdateAction) {
   207          updateIssuesMeta(
   208            $listMenu.data('update-url'),
   209            'clear',
   210            $listMenu.data('issue-id'),
   211            '',
   212          ).then(reloadConfirmDraftComment);
   213        }
   214  
   215        $(this).parent().find('.item').each(function () {
   216          $(this).removeClass('checked');
   217          $(this).find('.octicon-check').addClass('gt-invisible');
   218        });
   219  
   220        if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
   221          return false;
   222        }
   223  
   224        $list.find('.item').each(function () {
   225          $(this).addClass('gt-hidden');
   226        });
   227        $noSelect.removeClass('gt-hidden');
   228        $($(this).parent().data('id')).val('');
   229      });
   230    }
   231  
   232    // Init labels and assignees
   233    initListSubmits('select-label', 'labels');
   234    initListSubmits('select-assignees', 'assignees');
   235    initListSubmits('select-assignees-modify', 'assignees');
   236    initListSubmits('select-reviewers-modify', 'assignees');
   237  
   238    function selectItem(select_id, input_id) {
   239      const $menu = $(`${select_id} .menu`);
   240      const $list = $(`.ui${select_id}.list`);
   241      const hasUpdateAction = $menu.data('action') === 'update';
   242  
   243      $menu.find('.item:not(.no-select)').on('click', function () {
   244        $(this).parent().find('.item').each(function () {
   245          $(this).removeClass('selected active');
   246        });
   247  
   248        $(this).addClass('selected active');
   249        if (hasUpdateAction) {
   250          updateIssuesMeta(
   251            $menu.data('update-url'),
   252            '',
   253            $menu.data('issue-id'),
   254            $(this).data('id'),
   255          ).then(reloadConfirmDraftComment);
   256        }
   257  
   258        let icon = '';
   259        if (input_id === '#milestone_id') {
   260          icon = svg('octicon-milestone', 18, 'gt-mr-3');
   261        } else if (input_id === '#project_id') {
   262          icon = svg('octicon-project', 18, 'gt-mr-3');
   263        } else if (input_id === '#assignee_id') {
   264          icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
   265        }
   266  
   267        $list.find('.selected').html(`
   268          <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
   269            ${icon}
   270            ${htmlEscape($(this).text())}
   271          </a>
   272        `);
   273  
   274        $(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
   275        $(input_id).val($(this).data('id'));
   276      });
   277      $menu.find('.no-select.item').on('click', function () {
   278        $(this).parent().find('.item:not(.no-select)').each(function () {
   279          $(this).removeClass('selected active');
   280        });
   281  
   282        if (hasUpdateAction) {
   283          updateIssuesMeta(
   284            $menu.data('update-url'),
   285            '',
   286            $menu.data('issue-id'),
   287            $(this).data('id'),
   288          ).then(reloadConfirmDraftComment);
   289        }
   290  
   291        $list.find('.selected').html('');
   292        $list.find('.no-select').removeClass('gt-hidden');
   293        $(input_id).val('');
   294      });
   295    }
   296  
   297    // Milestone, Assignee, Project
   298    selectItem('.select-project', '#project_id');
   299    selectItem('.select-milestone', '#milestone_id');
   300    selectItem('.select-assignee', '#assignee_id');
   301  }
   302  
   303  
   304  async function onEditContent(event) {
   305    event.preventDefault();
   306  
   307    const $segment = $(this).closest('.header').next();
   308    const $editContentZone = $segment.find('.edit-content-zone');
   309    const $renderContent = $segment.find('.render-content');
   310    const $rawContent = $segment.find('.raw-content');
   311  
   312    let comboMarkdownEditor;
   313  
   314    const setupDropzone = async ($dropzone) => {
   315      if ($dropzone.length === 0) return null;
   316  
   317      let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
   318      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
   319      const dz = await createDropzone($dropzone[0], {
   320        url: $dropzone.attr('data-upload-url'),
   321        headers: {'X-Csrf-Token': csrfToken},
   322        maxFiles: $dropzone.attr('data-max-file'),
   323        maxFilesize: $dropzone.attr('data-max-size'),
   324        acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
   325        addRemoveLinks: true,
   326        dictDefaultMessage: $dropzone.attr('data-default-message'),
   327        dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
   328        dictFileTooBig: $dropzone.attr('data-file-too-big'),
   329        dictRemoveFile: $dropzone.attr('data-remove-file'),
   330        timeout: 0,
   331        thumbnailMethod: 'contain',
   332        thumbnailWidth: 480,
   333        thumbnailHeight: 480,
   334        init() {
   335          this.on('success', (file, data) => {
   336            file.uuid = data.uuid;
   337            fileUuidDict[file.uuid] = {submitted: false};
   338            const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
   339            $dropzone.find('.files').append(input);
   340          });
   341          this.on('removedfile', (file) => {
   342            if (disableRemovedfileEvent) return;
   343            $(`#${file.uuid}`).remove();
   344            if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
   345              $.post($dropzone.attr('data-remove-url'), {
   346                file: file.uuid,
   347                _csrf: csrfToken,
   348              });
   349            }
   350          });
   351          this.on('submit', () => {
   352            $.each(fileUuidDict, (fileUuid) => {
   353              fileUuidDict[fileUuid].submitted = true;
   354            });
   355          });
   356          this.on('reload', () => {
   357            $.getJSON($editContentZone.attr('data-attachment-url'), (data) => {
   358              // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
   359              disableRemovedfileEvent = true;
   360              dz.removeAllFiles(true);
   361              $dropzone.find('.files').empty();
   362              fileUuidDict = {};
   363              disableRemovedfileEvent = false;
   364  
   365              for (const attachment of data) {
   366                const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
   367                dz.emit('addedfile', attachment);
   368                dz.emit('thumbnail', attachment, imgSrc);
   369                dz.emit('complete', attachment);
   370                dz.files.push(attachment);
   371                fileUuidDict[attachment.uuid] = {submitted: true};
   372                $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
   373                const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
   374                $dropzone.find('.files').append(input);
   375              }
   376            });
   377          });
   378        },
   379      });
   380      dz.emit('reload');
   381      return dz;
   382    };
   383  
   384    const cancelAndReset = (dz) => {
   385      showElem($renderContent);
   386      hideElem($editContentZone);
   387      if (dz) {
   388        dz.emit('reload');
   389      }
   390    };
   391  
   392    const saveAndRefresh = (dz, $dropzone) => {
   393      showElem($renderContent);
   394      hideElem($editContentZone);
   395      const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
   396        return $(this).val();
   397      }).get();
   398      $.post($editContentZone.attr('data-update-url'), {
   399        _csrf: csrfToken,
   400        content: comboMarkdownEditor.value(),
   401        context: $editContentZone.attr('data-context'),
   402        files: $attachments,
   403      }, (data) => {
   404        if (!data.content) {
   405          $renderContent.html($('#no-content').html());
   406          $rawContent.text('');
   407        } else {
   408          $renderContent.html(data.content);
   409          $rawContent.text(comboMarkdownEditor.value());
   410  
   411          const refIssues = $renderContent.find('p .ref-issue');
   412          attachRefIssueContextPopup(refIssues);
   413        }
   414        const $content = $segment;
   415        if (!$content.find('.dropzone-attachments').length) {
   416          if (data.attachments !== '') {
   417            $content.append(`<div class="dropzone-attachments"></div>`);
   418            $content.find('.dropzone-attachments').replaceWith(data.attachments);
   419          }
   420        } else if (data.attachments === '') {
   421          $content.find('.dropzone-attachments').remove();
   422        } else {
   423          $content.find('.dropzone-attachments').replaceWith(data.attachments);
   424        }
   425        if (dz) {
   426          dz.emit('submit');
   427          dz.emit('reload');
   428        }
   429        initMarkupContent();
   430        initCommentContent();
   431      });
   432    };
   433  
   434    if (!$editContentZone.html()) {
   435      $editContentZone.html($('#issue-comment-editor-template').html());
   436      comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
   437  
   438      const $dropzone = $editContentZone.find('.dropzone');
   439      const dz = await setupDropzone($dropzone);
   440      $editContentZone.find('.cancel.button').on('click', (e) => {
   441        e.preventDefault();
   442        cancelAndReset(dz);
   443      });
   444      $editContentZone.find('.save.button').on('click', (e) => {
   445        e.preventDefault();
   446        saveAndRefresh(dz, $dropzone);
   447      });
   448    } else {
   449      comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
   450    }
   451  
   452    // Show write/preview tab and copy raw content as needed
   453    showElem($editContentZone);
   454    hideElem($renderContent);
   455    if (!comboMarkdownEditor.value()) {
   456      comboMarkdownEditor.value($rawContent.text());
   457    }
   458    comboMarkdownEditor.focus();
   459  }
   460  
   461  export function initRepository() {
   462    if ($('.page-content.repository').length === 0) {
   463      return;
   464    }
   465  
   466    initRepoBranchTagSelector('.js-branch-tag-selector');
   467  
   468    // Options
   469    if ($('.repository.settings.options').length > 0) {
   470      // Enable or select internal/external wiki system and issue tracker.
   471      $('.enable-system').on('change', function () {
   472        if (this.checked) {
   473          $($(this).data('target')).removeClass('disabled');
   474          if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
   475        } else {
   476          $($(this).data('target')).addClass('disabled');
   477          if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
   478        }
   479      });
   480      $('.enable-system-radio').on('change', function () {
   481        if (this.value === 'false') {
   482          $($(this).data('target')).addClass('disabled');
   483          if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
   484        } else if (this.value === 'true') {
   485          $($(this).data('target')).removeClass('disabled');
   486          if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled');
   487        }
   488      });
   489      const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
   490      $trackerIssueStyleRadios.on('change input', () => {
   491        const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
   492        $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
   493      });
   494    }
   495  
   496    // Labels
   497    initCompLabelEdit('.repository.labels');
   498  
   499    // Milestones
   500    if ($('.repository.new.milestone').length > 0) {
   501      $('#clear-date').on('click', () => {
   502        $('#deadline').val('');
   503        return false;
   504      });
   505    }
   506  
   507    // Repo Creation
   508    if ($('.repository.new.repo').length > 0) {
   509      $('input[name="gitignores"], input[name="license"]').on('change', () => {
   510        const gitignores = $('input[name="gitignores"]').val();
   511        const license = $('input[name="license"]').val();
   512        if (gitignores || license) {
   513          $('input[name="auto_init"]').prop('checked', true);
   514        }
   515      });
   516    }
   517  
   518    // Compare or pull request
   519    const $repoDiff = $('.repository.diff');
   520    if ($repoDiff.length) {
   521      initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
   522      initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
   523    }
   524  
   525    initRepoCloneLink();
   526    initCitationFileCopyContent();
   527    initRepoSettingBranches();
   528  
   529    // Issues
   530    if ($('.repository.view.issue').length > 0) {
   531      initRepoIssueCommentEdit();
   532  
   533      initRepoIssueBranchSelect();
   534      initRepoIssueTitleEdit();
   535      initRepoIssueWipToggle();
   536      initRepoIssueComments();
   537  
   538      initRepoDiffConversationNav();
   539      initRepoIssueReferenceIssue();
   540  
   541  
   542      initRepoIssueCommentDelete();
   543      initRepoIssueDependencyDelete();
   544      initRepoIssueCodeCommentCancel();
   545      initRepoPullRequestUpdate();
   546      initCompReactionSelector($(document));
   547  
   548      initRepoPullRequestMergeForm();
   549    }
   550  
   551    // Pull request
   552    const $repoComparePull = $('.repository.compare.pull');
   553    if ($repoComparePull.length > 0) {
   554      // show pull request form
   555      $repoComparePull.find('button.show-form').on('click', function (e) {
   556        e.preventDefault();
   557        hideElem($(this).parent());
   558  
   559        const $form = $repoComparePull.find('.pullrequest-form');
   560        showElem($form);
   561      });
   562    }
   563  
   564    initUnicodeEscapeButton();
   565  }
   566  
   567  function initRepoIssueCommentEdit() {
   568    // Edit issue or comment content
   569    $(document).on('click', '.edit-content', onEditContent);
   570  
   571    // Quote reply
   572    $(document).on('click', '.quote-reply', async function (event) {
   573      event.preventDefault();
   574      const target = $(this).data('target');
   575      const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
   576      const content = `> ${quote}\n\n`;
   577      let editor;
   578      if ($(this).hasClass('quote-reply-diff')) {
   579        const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
   580        editor = await handleReply($replyBtn);
   581      } else {
   582        // for normal issue/comment page
   583        editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
   584      }
   585      if (editor) {
   586        if (editor.value()) {
   587          editor.value(`${editor.value()}\n\n${content}`);
   588        } else {
   589          editor.value(content);
   590        }
   591        editor.focus();
   592        editor.moveCursorToEnd();
   593      }
   594    });
   595  }