Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

repo-legacy.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import $ from 'jquery';
  2. import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
  3. import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
  4. import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js';
  5. import {
  6. initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
  7. initRepoIssueCommentDelete,
  8. initRepoIssueComments, initRepoIssueDependencyDelete,
  9. initRepoIssueReferenceIssue, initRepoIssueStatusButton,
  10. initRepoIssueTitleEdit,
  11. initRepoIssueWipToggle, initRepoPullRequestUpdate,
  12. updateIssuesMeta,
  13. } from './repo-issue.js';
  14. import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
  15. import {svg} from '../svg.js';
  16. import {htmlEscape} from 'escape-goat';
  17. import {initRepoBranchTagDropdown} from '../components/RepoBranchTagDropdown.js';
  18. import {
  19. initRepoCloneLink,
  20. initRepoCommonBranchOrTagDropdown,
  21. initRepoCommonFilterSearchDropdown,
  22. initRepoCommonLanguageStats,
  23. } from './repo-common.js';
  24. import {initCompLabelEdit} from './comp/LabelEdit.js';
  25. import {initRepoDiffConversationNav} from './repo-diff.js';
  26. import attachTribute from './tribute.js';
  27. import createDropzone from './dropzone.js';
  28. import {initCommentContent, initMarkupContent} from '../markup/content.js';
  29. import {initCompReactionSelector} from './comp/ReactionSelector.js';
  30. import {initRepoSettingBranches} from './repo-settings.js';
  31. import initRepoPullRequestMergeForm from './repo-issue-pr-form.js';
  32. const {csrfToken} = window.config;
  33. export function initRepoCommentForm() {
  34. if ($('.comment.form').length === 0) {
  35. return;
  36. }
  37. function initBranchSelector() {
  38. const $selectBranch = $('.ui.select-branch');
  39. const $branchMenu = $selectBranch.find('.reference-list-menu');
  40. const $isNewIssue = $branchMenu.hasClass('new-issue');
  41. $branchMenu.find('.item:not(.no-select)').click(function () {
  42. const selectedValue = $(this).data('id');
  43. const editMode = $('#editing_mode').val();
  44. $($(this).data('id-selector')).val(selectedValue);
  45. if ($isNewIssue) {
  46. $selectBranch.find('.ui .branch-name').text($(this).data('name'));
  47. return;
  48. }
  49. if (editMode === 'true') {
  50. const form = $('#update_issueref_form');
  51. $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload());
  52. } else if (editMode === '') {
  53. $selectBranch.find('.ui .branch-name').text(selectedValue);
  54. }
  55. });
  56. $selectBranch.find('.reference.column').on('click', function () {
  57. $selectBranch.find('.scrolling.reference-list-menu').css('display', 'none');
  58. $selectBranch.find('.reference .text').removeClass('black');
  59. $($(this).data('target')).css('display', 'block');
  60. $(this).find('.text').addClass('black');
  61. return false;
  62. });
  63. }
  64. (async () => {
  65. await createCommentEasyMDE($('.comment.form textarea:not(.review-textarea)'));
  66. initCompImagePaste($('.comment.form'));
  67. })();
  68. initBranchSelector();
  69. initCompMarkupContentPreviewTab($('.comment.form'));
  70. // List submits
  71. function initListSubmits(selector, outerSelector) {
  72. const $list = $(`.ui.${outerSelector}.list`);
  73. const $noSelect = $list.find('.no-select');
  74. const $listMenu = $(`.${selector} .menu`);
  75. let hasUpdateAction = $listMenu.data('action') === 'update';
  76. const items = {};
  77. $(`.${selector}`).dropdown('setting', 'onHide', () => {
  78. hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
  79. if (hasUpdateAction) {
  80. // TODO: Add batch functionality and make this 1 network request.
  81. (async function() {
  82. for (const [elementId, item] of Object.entries(items)) {
  83. await updateIssuesMeta(
  84. item['update-url'],
  85. item.action,
  86. item['issue-id'],
  87. elementId,
  88. );
  89. }
  90. window.location.reload();
  91. })();
  92. }
  93. });
  94. $listMenu.find('.item:not(.no-select)').on('click', function (e) {
  95. e.preventDefault();
  96. if ($(this).hasClass('ban-change')) {
  97. return false;
  98. }
  99. hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
  100. if ($(this).hasClass('checked')) {
  101. $(this).removeClass('checked');
  102. $(this).find('.octicon-check').addClass('invisible');
  103. if (hasUpdateAction) {
  104. if (!($(this).data('id') in items)) {
  105. items[$(this).data('id')] = {
  106. 'update-url': $listMenu.data('update-url'),
  107. action: 'detach',
  108. 'issue-id': $listMenu.data('issue-id'),
  109. };
  110. } else {
  111. delete items[$(this).data('id')];
  112. }
  113. }
  114. } else {
  115. $(this).addClass('checked');
  116. $(this).find('.octicon-check').removeClass('invisible');
  117. if (hasUpdateAction) {
  118. if (!($(this).data('id') in items)) {
  119. items[$(this).data('id')] = {
  120. 'update-url': $listMenu.data('update-url'),
  121. action: 'attach',
  122. 'issue-id': $listMenu.data('issue-id'),
  123. };
  124. } else {
  125. delete items[$(this).data('id')];
  126. }
  127. }
  128. }
  129. // TODO: Which thing should be done for choosing review requests
  130. // to make chosen items be shown on time here?
  131. if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
  132. return false;
  133. }
  134. const listIds = [];
  135. $(this).parent().find('.item').each(function () {
  136. if ($(this).hasClass('checked')) {
  137. listIds.push($(this).data('id'));
  138. $($(this).data('id-selector')).removeClass('hide');
  139. } else {
  140. $($(this).data('id-selector')).addClass('hide');
  141. }
  142. });
  143. if (listIds.length === 0) {
  144. $noSelect.removeClass('hide');
  145. } else {
  146. $noSelect.addClass('hide');
  147. }
  148. $($(this).parent().data('id')).val(listIds.join(','));
  149. return false;
  150. });
  151. $listMenu.find('.no-select.item').on('click', function (e) {
  152. e.preventDefault();
  153. if (hasUpdateAction) {
  154. updateIssuesMeta(
  155. $listMenu.data('update-url'),
  156. 'clear',
  157. $listMenu.data('issue-id'),
  158. '',
  159. ).then(() => window.location.reload());
  160. }
  161. $(this).parent().find('.item').each(function () {
  162. $(this).removeClass('checked');
  163. $(this).find('.octicon').addClass('invisible');
  164. });
  165. if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
  166. return false;
  167. }
  168. $list.find('.item').each(function () {
  169. $(this).addClass('hide');
  170. });
  171. $noSelect.removeClass('hide');
  172. $($(this).parent().data('id')).val('');
  173. });
  174. }
  175. // Init labels and assignees
  176. initListSubmits('select-label', 'labels');
  177. initListSubmits('select-assignees', 'assignees');
  178. initListSubmits('select-assignees-modify', 'assignees');
  179. initListSubmits('select-reviewers-modify', 'assignees');
  180. function selectItem(select_id, input_id) {
  181. const $menu = $(`${select_id} .menu`);
  182. const $list = $(`.ui${select_id}.list`);
  183. const hasUpdateAction = $menu.data('action') === 'update';
  184. $menu.find('.item:not(.no-select)').on('click', function () {
  185. $(this).parent().find('.item').each(function () {
  186. $(this).removeClass('selected active');
  187. });
  188. $(this).addClass('selected active');
  189. if (hasUpdateAction) {
  190. updateIssuesMeta(
  191. $menu.data('update-url'),
  192. '',
  193. $menu.data('issue-id'),
  194. $(this).data('id'),
  195. ).then(() => window.location.reload());
  196. }
  197. let icon = '';
  198. if (input_id === '#milestone_id') {
  199. icon = svg('octicon-milestone', 18, 'mr-3');
  200. } else if (input_id === '#project_id') {
  201. icon = svg('octicon-project', 18, 'mr-3');
  202. } else if (input_id === '#assignee_id') {
  203. icon = `<img class="ui avatar image mr-3" src=${$(this).data('avatar')}>`;
  204. }
  205. $list.find('.selected').html(`
  206. <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
  207. ${icon}
  208. ${htmlEscape($(this).text())}
  209. </a>
  210. `);
  211. $(`.ui${select_id}.list .no-select`).addClass('hide');
  212. $(input_id).val($(this).data('id'));
  213. });
  214. $menu.find('.no-select.item').on('click', function () {
  215. $(this).parent().find('.item:not(.no-select)').each(function () {
  216. $(this).removeClass('selected active');
  217. });
  218. if (hasUpdateAction) {
  219. updateIssuesMeta(
  220. $menu.data('update-url'),
  221. '',
  222. $menu.data('issue-id'),
  223. $(this).data('id'),
  224. ).then(() => window.location.reload());
  225. }
  226. $list.find('.selected').html('');
  227. $list.find('.no-select').removeClass('hide');
  228. $(input_id).val('');
  229. });
  230. }
  231. // Milestone, Assignee, Project
  232. selectItem('.select-project', '#project_id');
  233. selectItem('.select-milestone', '#milestone_id');
  234. selectItem('.select-assignee', '#assignee_id');
  235. }
  236. async function onEditContent(event) {
  237. event.preventDefault();
  238. $(this).closest('.dropdown').find('.menu').toggle('visible');
  239. const $segment = $(this).closest('.header').next();
  240. const $editContentZone = $segment.find('.edit-content-zone');
  241. const $renderContent = $segment.find('.render-content');
  242. const $rawContent = $segment.find('.raw-content');
  243. let $textarea;
  244. let easyMDE;
  245. // Setup new form
  246. if ($editContentZone.html().length === 0) {
  247. $editContentZone.html($('#edit-content-form').html());
  248. $textarea = $editContentZone.find('textarea');
  249. await attachTribute($textarea.get(), {mentions: true, emoji: true});
  250. let dz;
  251. const $dropzone = $editContentZone.find('.dropzone');
  252. if ($dropzone.length === 1) {
  253. $dropzone.data('saved', false);
  254. const fileUuidDict = {};
  255. dz = await createDropzone($dropzone[0], {
  256. url: $dropzone.data('upload-url'),
  257. headers: {'X-Csrf-Token': csrfToken},
  258. maxFiles: $dropzone.data('max-file'),
  259. maxFilesize: $dropzone.data('max-size'),
  260. acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
  261. addRemoveLinks: true,
  262. dictDefaultMessage: $dropzone.data('default-message'),
  263. dictInvalidFileType: $dropzone.data('invalid-input-type'),
  264. dictFileTooBig: $dropzone.data('file-too-big'),
  265. dictRemoveFile: $dropzone.data('remove-file'),
  266. timeout: 0,
  267. thumbnailMethod: 'contain',
  268. thumbnailWidth: 480,
  269. thumbnailHeight: 480,
  270. init() {
  271. this.on('success', (file, data) => {
  272. fileUuidDict[file.uuid] = {submitted: false};
  273. const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
  274. $dropzone.find('.files').append(input);
  275. });
  276. this.on('removedfile', (file) => {
  277. $(`#${file.uuid}`).remove();
  278. if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
  279. $.post($dropzone.data('remove-url'), {
  280. file: file.uuid,
  281. _csrf: csrfToken,
  282. });
  283. }
  284. });
  285. this.on('submit', () => {
  286. $.each(fileUuidDict, (fileUuid) => {
  287. fileUuidDict[fileUuid].submitted = true;
  288. });
  289. });
  290. this.on('reload', () => {
  291. $.getJSON($editContentZone.data('attachment-url'), (data) => {
  292. dz.removeAllFiles(true);
  293. $dropzone.find('.files').empty();
  294. $.each(data, function () {
  295. const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
  296. dz.emit('addedfile', this);
  297. dz.emit('thumbnail', this, imgSrc);
  298. dz.emit('complete', this);
  299. dz.files.push(this);
  300. fileUuidDict[this.uuid] = {submitted: true};
  301. $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
  302. const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
  303. $dropzone.find('.files').append(input);
  304. });
  305. });
  306. });
  307. },
  308. });
  309. dz.emit('reload');
  310. }
  311. // Give new write/preview data-tab name to distinguish from others
  312. const $editContentForm = $editContentZone.find('.ui.comment.form');
  313. const $tabMenu = $editContentForm.find('.tabular.menu');
  314. $tabMenu.attr('data-write', $editContentZone.data('write'));
  315. $tabMenu.attr('data-preview', $editContentZone.data('preview'));
  316. $tabMenu.find('.write.item').attr('data-tab', $editContentZone.data('write'));
  317. $tabMenu.find('.preview.item').attr('data-tab', $editContentZone.data('preview'));
  318. $editContentForm.find('.write').attr('data-tab', $editContentZone.data('write'));
  319. $editContentForm.find('.preview').attr('data-tab', $editContentZone.data('preview'));
  320. easyMDE = await createCommentEasyMDE($textarea);
  321. initCompMarkupContentPreviewTab($editContentForm);
  322. if ($dropzone.length === 1) {
  323. initEasyMDEImagePaste(easyMDE, $dropzone[0], $dropzone.find('.files'));
  324. }
  325. const $saveButton = $editContentZone.find('.save.button');
  326. $textarea.on('ce-quick-submit', () => {
  327. $saveButton.trigger('click');
  328. });
  329. $editContentZone.find('.cancel.button').on('click', () => {
  330. $renderContent.show();
  331. $editContentZone.hide();
  332. if (dz) {
  333. dz.emit('reload');
  334. }
  335. });
  336. $saveButton.on('click', () => {
  337. $renderContent.show();
  338. $editContentZone.hide();
  339. const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
  340. return $(this).val();
  341. }).get();
  342. $.post($editContentZone.data('update-url'), {
  343. _csrf: csrfToken,
  344. content: $textarea.val(),
  345. context: $editContentZone.data('context'),
  346. files: $attachments,
  347. }, (data) => {
  348. if (data.length === 0 || data.content.length === 0) {
  349. $renderContent.html($('#no-content').html());
  350. $rawContent.text('');
  351. } else {
  352. $renderContent.html(data.content);
  353. $rawContent.text($textarea.val());
  354. }
  355. const $content = $segment;
  356. if (!$content.find('.dropzone-attachments').length) {
  357. if (data.attachments !== '') {
  358. $content.append(`<div class="dropzone-attachments"></div>`);
  359. $content.find('.dropzone-attachments').replaceWith(data.attachments);
  360. }
  361. } else if (data.attachments === '') {
  362. $content.find('.dropzone-attachments').remove();
  363. } else {
  364. $content.find('.dropzone-attachments').replaceWith(data.attachments);
  365. }
  366. if (dz) {
  367. dz.emit('submit');
  368. dz.emit('reload');
  369. }
  370. initMarkupContent();
  371. initCommentContent();
  372. });
  373. });
  374. } else { // use existing form
  375. $textarea = $segment.find('textarea');
  376. easyMDE = getAttachedEasyMDE($textarea);
  377. }
  378. // Show write/preview tab and copy raw content as needed
  379. $editContentZone.show();
  380. $renderContent.hide();
  381. if ($textarea.val().length === 0) {
  382. $textarea.val($rawContent.text());
  383. easyMDE.value($rawContent.text());
  384. }
  385. requestAnimationFrame(() => {
  386. $textarea.focus();
  387. easyMDE.codemirror.focus();
  388. });
  389. }
  390. export function initRepository() {
  391. if ($('.repository').length === 0) {
  392. return;
  393. }
  394. // File list and commits
  395. if ($('.repository.file.list').length > 0 || $('.branch-dropdown').length > 0 ||
  396. $('.repository.commits').length > 0 || $('.repository.release').length > 0) {
  397. initRepoBranchTagDropdown('.choose.reference .dropdown');
  398. }
  399. // Wiki
  400. if ($('.repository.wiki.view').length > 0) {
  401. initRepoCommonFilterSearchDropdown('.choose.page .dropdown');
  402. }
  403. // Options
  404. if ($('.repository.settings.options').length > 0) {
  405. // Enable or select internal/external wiki system and issue tracker.
  406. $('.enable-system').on('change', function () {
  407. if (this.checked) {
  408. $($(this).data('target')).removeClass('disabled');
  409. if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
  410. } else {
  411. $($(this).data('target')).addClass('disabled');
  412. if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
  413. }
  414. });
  415. $('.enable-system-radio').on('change', function () {
  416. if (this.value === 'false') {
  417. $($(this).data('target')).addClass('disabled');
  418. if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).removeClass('disabled');
  419. } else if (this.value === 'true') {
  420. $($(this).data('target')).removeClass('disabled');
  421. if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).addClass('disabled');
  422. }
  423. });
  424. }
  425. // Labels
  426. initCompLabelEdit('.repository.labels');
  427. // Milestones
  428. if ($('.repository.new.milestone').length > 0) {
  429. $('#clear-date').on('click', () => {
  430. $('#deadline').val('');
  431. return false;
  432. });
  433. }
  434. // Repo Creation
  435. if ($('.repository.new.repo').length > 0) {
  436. $('input[name="gitignores"], input[name="license"]').on('change', () => {
  437. const gitignores = $('input[name="gitignores"]').val();
  438. const license = $('input[name="license"]').val();
  439. if (gitignores || license) {
  440. $('input[name="auto_init"]').prop('checked', true);
  441. }
  442. });
  443. }
  444. // Compare or pull request
  445. const $repoDiff = $('.repository.diff');
  446. if ($repoDiff.length) {
  447. initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
  448. initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
  449. }
  450. initRepoCloneLink();
  451. initRepoCommonLanguageStats();
  452. initRepoSettingBranches();
  453. // Issues
  454. if ($('.repository.view.issue').length > 0) {
  455. initRepoIssueCommentEdit();
  456. initRepoIssueBranchSelect();
  457. initRepoIssueTitleEdit();
  458. initRepoIssueWipToggle();
  459. initRepoIssueComments();
  460. initRepoDiffConversationNav();
  461. initRepoIssueReferenceIssue();
  462. initRepoIssueCommentDelete();
  463. initRepoIssueDependencyDelete();
  464. initRepoIssueCodeCommentCancel();
  465. initRepoIssueStatusButton();
  466. initRepoPullRequestUpdate();
  467. initCompReactionSelector();
  468. initRepoPullRequestMergeForm();
  469. }
  470. // Pull request
  471. const $repoComparePull = $('.repository.compare.pull');
  472. if ($repoComparePull.length > 0) {
  473. // show pull request form
  474. $repoComparePull.find('button.show-form').on('click', function (e) {
  475. e.preventDefault();
  476. $(this).parent().hide();
  477. const $form = $repoComparePull.find('.pullrequest-form');
  478. const easyMDE = getAttachedEasyMDE($form.find('textarea.edit_area'));
  479. $form.show();
  480. easyMDE.codemirror.refresh();
  481. });
  482. }
  483. initUnicodeEscapeButton();
  484. }
  485. function initRepoIssueCommentEdit() {
  486. // Issue/PR Context Menus
  487. $('.comment-header-right .context-dropdown').dropdown({action: 'hide'});
  488. // Edit issue or comment content
  489. $(document).on('click', '.edit-content', onEditContent);
  490. // Quote reply
  491. $(document).on('click', '.quote-reply', function (event) {
  492. $(this).closest('.dropdown').find('.menu').toggle('visible');
  493. const target = $(this).data('target');
  494. const quote = $(`#comment-${target}`).text().replace(/\n/g, '\n> ');
  495. const content = `> ${quote}\n\n`;
  496. let easyMDE;
  497. if ($(this).hasClass('quote-reply-diff')) {
  498. const $parent = $(this).closest('.comment-code-cloud');
  499. $parent.find('button.comment-form-reply').trigger('click');
  500. easyMDE = getAttachedEasyMDE($parent.find('[name="content"]'));
  501. } else {
  502. // for normal issue/comment page
  503. easyMDE = getAttachedEasyMDE($('#comment-form .edit_area'));
  504. }
  505. if (easyMDE) {
  506. if (easyMDE.value() !== '') {
  507. easyMDE.value(`${easyMDE.value()}\n\n${content}`);
  508. } else {
  509. easyMDE.value(`${content}`);
  510. }
  511. requestAnimationFrame(() => {
  512. easyMDE.codemirror.focus();
  513. easyMDE.codemirror.setCursor(easyMDE.codemirror.lineCount(), 0);
  514. });
  515. }
  516. event.preventDefault();
  517. });
  518. }