Allow to mark files in a PR as viewed (#19007)
Users can now mark files in PRs as viewed, resulting in them not being shown again by default when they reopen the PR again.
This commit is contained in:
parent
59b30f060a
commit
5ca224a789
16 changed files with 492 additions and 44 deletions
18
web_src/js/features/file-fold.js
Normal file
18
web_src/js/features/file-fold.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {svg} from '../svg.js';
|
||||
|
||||
|
||||
// Hides the file if newFold is true, and shows it otherwise. The actual hiding is performed using CSS.
|
||||
//
|
||||
// The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
|
||||
// The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
|
||||
//
|
||||
export function setFileFolding(fileContentBox, foldArrow, newFold) {
|
||||
foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
|
||||
fileContentBox.setAttribute('data-folded', newFold);
|
||||
}
|
||||
|
||||
// Like `setFileFolding`, except that it automatically inverts the current file folding state.
|
||||
export function invertFileFolding(fileContentBox, foldArrow) {
|
||||
setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
|
||||
}
|
||||
|
71
web_src/js/features/pull-view-file.js
Normal file
71
web_src/js/features/pull-view-file.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import {setFileFolding} from './file-fold.js';
|
||||
|
||||
const {csrfToken, pageData} = window.config;
|
||||
const prReview = pageData.prReview || {};
|
||||
const viewedStyleClass = 'viewed-file-checked-form';
|
||||
const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found
|
||||
|
||||
|
||||
// Refreshes the summary of viewed files if present
|
||||
// The data used will be window.config.pageData.prReview.numberOf{Viewed}Files
|
||||
function refreshViewedFilesSummary() {
|
||||
const viewedFilesMeter = document.getElementById('viewed-files-summary');
|
||||
viewedFilesMeter?.setAttribute('value', prReview.numberOfViewedFiles);
|
||||
const summaryLabel = document.getElementById('viewed-files-summary-label');
|
||||
if (summaryLabel) summaryLabel.innerHTML = summaryLabel.getAttribute('data-text-changed-template')
|
||||
.replace('%[1]d', prReview.numberOfViewedFiles)
|
||||
.replace('%[2]d', prReview.numberOfFiles);
|
||||
}
|
||||
|
||||
// Explicitly recounts how many files the user has currently reviewed by counting the number of checked "viewed" checkboxes
|
||||
// Additionally, the viewed files summary will be updated if it exists
|
||||
export function countAndUpdateViewedFiles() {
|
||||
// The number of files is constant, but the number of viewed files can change because files can be loaded dynamically
|
||||
prReview.numberOfViewedFiles = document.querySelectorAll(`${viewedCheckboxSelector} > input[type=checkbox][checked]`).length;
|
||||
refreshViewedFilesSummary();
|
||||
}
|
||||
|
||||
// Initializes a listener for all children of the given html element
|
||||
// (for example 'document' in the most basic case)
|
||||
// to watch for changes of viewed-file checkboxes
|
||||
export function initViewedCheckboxListenerFor() {
|
||||
for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) {
|
||||
// To prevent double addition of listeners
|
||||
form.setAttribute('data-has-viewed-checkbox-listener', true);
|
||||
|
||||
// The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
|
||||
// hence the actual checkbox first has to be found
|
||||
const checkbox = form.querySelector('input[type=checkbox]');
|
||||
checkbox.addEventListener('change', function() {
|
||||
// Mark the file as viewed visually - will especially change the background
|
||||
if (this.checked) {
|
||||
form.classList.add(viewedStyleClass);
|
||||
prReview.numberOfViewedFiles++;
|
||||
} else {
|
||||
form.classList.remove(viewedStyleClass);
|
||||
prReview.numberOfViewedFiles--;
|
||||
}
|
||||
|
||||
// Update viewed-files summary and remove "has changed" label if present
|
||||
refreshViewedFilesSummary();
|
||||
const hasChangedLabel = form.parentNode.querySelector('.changed-since-last-review');
|
||||
hasChangedLabel?.parentNode.removeChild(hasChangedLabel);
|
||||
|
||||
// Unfortunately, actual forms cause too many problems, hence another approach is needed
|
||||
const files = {};
|
||||
files[checkbox.getAttribute('name')] = this.checked;
|
||||
const data = {files};
|
||||
const headCommitSHA = form.getAttribute('data-headcommit');
|
||||
if (headCommitSHA) data.headCommitSHA = headCommitSHA;
|
||||
fetch(form.getAttribute('data-link'), {
|
||||
method: 'POST',
|
||||
headers: {'X-Csrf-Token': csrfToken},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
// Fold the file accordingly
|
||||
const parentBox = form.closest('.diff-file-header');
|
||||
setFileFolding(parentBox.closest('.file-content'), parentBox.querySelector('.fold-file'), this.checked);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import {svg} from '../svg.js';
|
||||
import {invertFileFolding} from './file-fold.js';
|
||||
|
||||
function changeHash(hash) {
|
||||
if (window.history.pushState) {
|
||||
|
@ -148,10 +149,7 @@ export function initRepoCodeView() {
|
|||
}).trigger('hashchange');
|
||||
}
|
||||
$(document).on('click', '.fold-file', ({currentTarget}) => {
|
||||
const box = currentTarget.closest('.file-content');
|
||||
const folded = box.getAttribute('data-folded') !== 'true';
|
||||
currentTarget.innerHTML = svg(`octicon-chevron-${folded ? 'right' : 'down'}`, 18);
|
||||
box.setAttribute('data-folded', String(folded));
|
||||
invertFileFolding(currentTarget.closest('.file-content'), currentTarget);
|
||||
});
|
||||
$(document).on('click', '.blob-excerpt', async ({currentTarget}) => {
|
||||
const url = currentTarget.getAttribute('data-url');
|
||||
|
|
|
@ -2,6 +2,7 @@ import $ from 'jquery';
|
|||
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
||||
import {initRepoIssueContentHistory} from './repo-issue-content.js';
|
||||
import {validateTextareaNonEmpty} from './comp/EasyMDE.js';
|
||||
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles} from './pull-view-file.js';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
|
@ -104,6 +105,13 @@ export function initRepoDiffConversationNav() {
|
|||
});
|
||||
}
|
||||
|
||||
// Will be called when the show more (files) button has been pressed
|
||||
function onShowMoreFiles() {
|
||||
initRepoIssueContentHistory();
|
||||
initViewedCheckboxListenerFor();
|
||||
countAndUpdateViewedFiles();
|
||||
}
|
||||
|
||||
export function initRepoDiffShowMore() {
|
||||
$('#diff-files, #diff-file-boxes').on('click', '#diff-show-more-files, #diff-show-more-files-stats', (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -125,7 +133,7 @@ export function initRepoDiffShowMore() {
|
|||
$('#diff-too-many-files-stats').remove();
|
||||
$('#diff-files').append($(resp).find('#diff-files li'));
|
||||
$('#diff-incomplete').replaceWith($(resp).find('#diff-file-boxes').children());
|
||||
initRepoIssueContentHistory();
|
||||
onShowMoreFiles();
|
||||
}).fail(() => {
|
||||
$('#diff-show-more-files, #diff-show-more-files-stats').removeClass('disabled');
|
||||
});
|
||||
|
@ -151,7 +159,7 @@ export function initRepoDiffShowMore() {
|
|||
}
|
||||
|
||||
$target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
|
||||
initRepoIssueContentHistory();
|
||||
onShowMoreFiles();
|
||||
}).fail(() => {
|
||||
$target.removeClass('disabled');
|
||||
});
|
||||
|
|
|
@ -70,6 +70,7 @@ import {
|
|||
initRepoSettingsCollaboration,
|
||||
initRepoSettingSearchTeamBox,
|
||||
} from './features/repo-settings.js';
|
||||
import {initViewedCheckboxListenerFor} from './features/pull-view-file.js';
|
||||
import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.js';
|
||||
import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.js';
|
||||
import {initRepoRelease, initRepoReleaseEditor} from './features/repo-release.js';
|
||||
|
@ -178,6 +179,6 @@ $(document).ready(() => {
|
|||
initUserAuthWebAuthn();
|
||||
initUserAuthWebAuthnRegister();
|
||||
initUserSettings();
|
||||
|
||||
initViewedCheckboxListenerFor();
|
||||
checkAppUrl();
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue