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 }