You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo-legacy.js 20KB

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