summaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-diff.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--web_src/js/features/repo-diff.js232
1 files changed, 232 insertions, 0 deletions
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
new file mode 100644
index 00000000..e5723af0
--- /dev/null
+++ b/web_src/js/features/repo-diff.js
@@ -0,0 +1,232 @@
+import $ from 'jquery';
+import {initCompReactionSelector} from './comp/ReactionSelector.js';
+import {initRepoIssueContentHistory} from './repo-issue-content.js';
+import {initDiffFileTree} from './repo-diff-filetree.js';
+import {initDiffCommitSelect} from './repo-diff-commitselect.js';
+import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
+import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
+import {initImageDiff} from './imagediff.js';
+import {showErrorToast} from '../modules/toast.js';
+import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
+import {POST, GET} from '../modules/fetch.js';
+
+const {pageData, i18n} = window.config;
+
+function initRepoDiffReviewButton() {
+ const reviewBox = document.getElementById('review-box');
+ if (!reviewBox) return;
+
+ const counter = reviewBox.querySelector('.review-comments-counter');
+ if (!counter) return;
+
+ $(document).on('click', 'button[name="pending_review"]', (e) => {
+ const $form = $(e.target).closest('form');
+ // Watch for the form's submit event.
+ $form.on('submit', () => {
+ const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
+ counter.setAttribute('data-pending-comment-number', num);
+ counter.textContent = num;
+
+ reviewBox.classList.remove('pulse');
+ requestAnimationFrame(() => {
+ reviewBox.classList.add('pulse');
+ });
+ });
+ });
+}
+
+function initRepoDiffFileViewToggle() {
+ $('.file-view-toggle').on('click', function () {
+ for (const el of queryElemSiblings(this)) {
+ el.classList.remove('active');
+ }
+ this.classList.add('active');
+
+ const target = document.querySelector(this.getAttribute('data-toggle-selector'));
+ if (!target) return;
+
+ hideElem(queryElemSiblings(target));
+ showElem(target);
+ });
+}
+
+function initRepoDiffConversationForm() {
+ $(document).on('submit', '.conversation-holder form', async (e) => {
+ e.preventDefault();
+
+ const $form = $(e.target);
+ const textArea = e.target.querySelector('textarea');
+ if (!validateTextareaNonEmpty(textArea)) {
+ return;
+ }
+
+ if (e.target.classList.contains('is-loading')) return;
+ try {
+ e.target.classList.add('is-loading');
+ const formData = new FormData($form[0]);
+
+ // If the form is submitted by a button, append the button's name and value to the form data.
+ // originalEvent can be undefined, such as an event that's caused by Ctrl+Enter, in that case
+ // sent the event itself.
+ const submitter = submitEventSubmitter(e.originalEvent ?? e);
+ const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
+ if (isSubmittedByButton && submitter.name) {
+ formData.append(submitter.name, submitter.value);
+ }
+
+ const response = await POST(e.target.getAttribute('action'), {data: formData});
+ const $newConversationHolder = $(await response.text());
+ const {path, side, idx} = $newConversationHolder.data();
+
+ $form.closest('.conversation-holder').replaceWith($newConversationHolder);
+ let selector;
+ if ($form.closest('tr').data('line-type') === 'same') {
+ selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
+ } else {
+ selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
+ }
+ for (const el of document.querySelectorAll(selector)) {
+ el.classList.add('tw-invisible');
+ }
+ $newConversationHolder.find('.dropdown').dropdown();
+ initCompReactionSelector($newConversationHolder);
+ } catch { // here the caught error might be a jQuery AJAX error (thrown by await $.post), which is not good to use for error message handling
+ console.error('error when submitting conversation', e);
+ showErrorToast(i18n.network_error);
+ } finally {
+ e.target.classList.remove('is-loading');
+ }
+ });
+
+ $(document).on('click', '.resolve-conversation', async function (e) {
+ e.preventDefault();
+ const comment_id = $(this).data('comment-id');
+ const origin = $(this).data('origin');
+ const action = $(this).data('action');
+ const url = $(this).data('update-url');
+
+ try {
+ const response = await POST(url, {data: new URLSearchParams({origin, action, comment_id})});
+ const data = await response.text();
+
+ if ($(this).closest('.conversation-holder').length) {
+ const $conversation = $(data);
+ $(this).closest('.conversation-holder').replaceWith($conversation);
+ $conversation.find('.dropdown').dropdown();
+ initCompReactionSelector($conversation);
+ } else {
+ window.location.reload();
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ });
+}
+
+export function initRepoDiffConversationNav() {
+ // Previous/Next code review conversation
+ $(document).on('click', '.previous-conversation', (e) => {
+ const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
+ const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
+ const index = $conversations.index($conversation);
+ const previousIndex = index > 0 ? index - 1 : $conversations.length - 1;
+ const $previousConversation = $conversations.eq(previousIndex);
+ const anchor = $previousConversation.find('.comment').first()[0].getAttribute('id');
+ window.location.href = `#${anchor}`;
+ });
+ $(document).on('click', '.next-conversation', (e) => {
+ const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
+ const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
+ const index = $conversations.index($conversation);
+ const nextIndex = index < $conversations.length - 1 ? index + 1 : 0;
+ const $nextConversation = $conversations.eq(nextIndex);
+ const anchor = $nextConversation.find('.comment').first()[0].getAttribute('id');
+ window.location.href = `#${anchor}`;
+ });
+}
+
+// Will be called when the show more (files) button has been pressed
+function onShowMoreFiles() {
+ initRepoIssueContentHistory();
+ initViewedCheckboxListenerFor();
+ countAndUpdateViewedFiles();
+ initImageDiff();
+}
+
+export async function loadMoreFiles(url) {
+ const target = document.querySelector('a#diff-show-more-files');
+ if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
+ return;
+ }
+
+ pageData.diffFileInfo.isLoadingNewData = true;
+ target?.classList.add('disabled');
+
+ try {
+ const response = await GET(url);
+ const resp = await response.text();
+ const $resp = $(resp);
+ // the response is a full HTML page, we need to extract the relevant contents:
+ // 1. append the newly loaded file list items to the existing list
+ $('#diff-incomplete').replaceWith($resp.find('#diff-file-boxes').children());
+ // 2. re-execute the script to append the newly loaded items to the JS variables to refresh the DiffFileTree
+ $('body').append($resp.find('script#diff-data-script'));
+
+ onShowMoreFiles();
+ } catch (error) {
+ console.error('Error:', error);
+ showErrorToast('An error occurred while loading more files.');
+ } finally {
+ target?.classList.remove('disabled');
+ pageData.diffFileInfo.isLoadingNewData = false;
+ }
+}
+
+function initRepoDiffShowMore() {
+ $(document).on('click', 'a#diff-show-more-files', (e) => {
+ e.preventDefault();
+
+ const linkLoadMore = e.target.getAttribute('data-href');
+ loadMoreFiles(linkLoadMore);
+ });
+
+ $(document).on('click', 'a.diff-load-button', async (e) => {
+ e.preventDefault();
+ const $target = $(e.target);
+
+ if (e.target.classList.contains('disabled')) {
+ return;
+ }
+
+ e.target.classList.add('disabled');
+
+ const url = $target.data('href');
+
+ try {
+ const response = await GET(url);
+ const resp = await response.text();
+
+ if (!resp) {
+ return;
+ }
+ $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
+ onShowMoreFiles();
+ } catch (error) {
+ console.error('Error:', error);
+ } finally {
+ e.target.classList.remove('disabled');
+ }
+ });
+}
+
+export function initRepoDiffView() {
+ initRepoDiffConversationForm();
+ if (!$('#diff-file-list').length) return;
+ initDiffFileTree();
+ initDiffCommitSelect();
+ initRepoDiffShowMore();
+ initRepoDiffReviewButton();
+ initRepoDiffFileViewToggle();
+ initViewedCheckboxListenerFor();
+ initExpandAndCollapseFilesButton();
+}