feat: load pull review feedback on demand (Closes #1342)
This commit is contained in:
1 parent
50817bed9a
commit
611e79fc1e
7 files changed
+364
-28
No files matched your search
@@ -1275,7 +1275,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.pull-review-feedback-file li { margin:6px 0; }
|
||||
.pull-review-feedback-file small { display:block; color:#93c5fd; }
|
||||
.pull-review-feedback-file button { min-height:44px; width:100%; margin-top:4px; }
|
||||
.address-review-feedback { min-height:44px; width:100%; margin-top:8px; }
|
||||
.address-review-feedback, [data-load-review-feedback] { min-height:44px; width:100%; margin-top:8px; }
|
||||
.pull-feedback-pass { margin-top:10px; padding:10px; border:1px solid #3b82f6; border-radius:10px; overflow-wrap:anywhere; }
|
||||
.pull-feedback-pass-heading { display:flex; align-items:start; justify-content:space-between; gap:8px; }
|
||||
.pull-feedback-pass-heading h4 { margin:2px 0 8px; }
|
||||
|
||||
+70
-5
@@ -44,16 +44,26 @@ function renderReviewerFeedback(reviewer, escapeHtml) {
|
||||
'</ul><button type="button" data-review-feedback-file="' + escapeHtml(path) +
|
||||
'">View in changes</button></section>'
|
||||
).join('');
|
||||
if (!summary && !files) return '';
|
||||
const loadable = Number.isInteger(reviewer?.review_id) && reviewer.review_id > 0 &&
|
||||
['commented', 'changes_requested', 'outdated'].includes(reviewer?.status);
|
||||
const feedbackState = Array.isArray(reviewer?.comments) ? 'loaded' : reviewer?.feedback_state;
|
||||
const feedbackAction = !loadable || feedbackState === 'loaded' ? '' :
|
||||
feedbackState === 'loading' ? '<p class="small" role="status">Loading inline feedback…</p>' :
|
||||
'<p class="small" role="status">' + (feedbackState === 'error' ? 'Feedback unavailable. ' : '') +
|
||||
'<button type="button" data-load-review-feedback="' + Number(reviewer.review_id) + '">' +
|
||||
(feedbackState === 'error' ? 'Retry feedback' : 'Load inline feedback') + '</button></p>';
|
||||
const emptyLoaded = loadable && feedbackState === 'loaded' && !files ?
|
||||
'<p class="small">No inline comments on this review.</p>' : '';
|
||||
if (!summary && !files && !feedbackAction && !emptyLoaded) return '';
|
||||
const reviewedHead = reviewer?.status === 'outdated' && reviewer?.head_sha ?
|
||||
'<p class="small">Reviewed head ' + escapeHtml(reviewer.head_sha.slice(0, 8)) + '</p>' : '';
|
||||
const addressable = Number.isInteger(reviewer?.review_id) && reviewer.review_id > 0 &&
|
||||
['changes_requested', 'outdated'].includes(reviewer?.status) &&
|
||||
['changes_requested', 'outdated'].includes(reviewer?.status) && feedbackState === 'loaded' &&
|
||||
(reviewer.comments || []).some(comment => Number.isInteger(comment?.id) && comment.id > 0);
|
||||
const address = addressable ? '<button type="button" class="address-review-feedback" data-address-review-feedback="' +
|
||||
Number(reviewer.review_id) + '">Address feedback</button>' : '';
|
||||
return '<details class="pull-review-feedback"><summary>Review feedback</summary>' +
|
||||
reviewedHead + summary + files + address + '</details>';
|
||||
reviewedHead + summary + feedbackAction + emptyLoaded + files + address + '</details>';
|
||||
}
|
||||
|
||||
function renderReviewerStatuses(reviewers, escapeHtml) {
|
||||
@@ -70,7 +80,9 @@ function renderReviewerStatuses(reviewers, escapeHtml) {
|
||||
'<button type="button" class="cancel-review-request" data-cancel-review-request="' +
|
||||
escapeHtml(reviewer.login) + '">Cancel request</button>' : '';
|
||||
return '<article class="pull-reviewer-status" data-reviewer-status="' +
|
||||
escapeHtml(reviewer?.status || 'unknown') + '"><div class="pull-reviewer-status-heading"><strong>@' +
|
||||
escapeHtml(reviewer?.status || 'unknown') + '" data-review-id="' +
|
||||
(Number.isInteger(reviewer?.review_id) ? Number(reviewer.review_id) : '') +
|
||||
'"><div class="pull-reviewer-status-heading"><strong>@' +
|
||||
escapeHtml(reviewer?.login || 'unknown') + '</strong><span>' + escapeHtml(status) + '</span></div>' +
|
||||
renderReviewerFeedback(reviewer, escapeHtml) + cancel + '</article>';
|
||||
}).join('');
|
||||
@@ -79,7 +91,13 @@ function renderReviewerStatuses(reviewers, escapeHtml) {
|
||||
function renderReviewerPanel(doc, detail, escapeHtml = value => String(value)
|
||||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')) {
|
||||
const reviewers = Array.isArray(detail?.reviewers) ? detail.reviewers : [];
|
||||
const openFeedback = new Set(Array.from(doc.querySelectorAll('.pull-review-feedback[open]'))
|
||||
.map(panel => panel.closest('.pull-reviewer-status')?.dataset.reviewId).filter(Boolean));
|
||||
doc.querySelector('#pull-reviewer-statuses').innerHTML = renderReviewerStatuses(reviewers, escapeHtml);
|
||||
openFeedback.forEach(reviewId => {
|
||||
const panel = doc.querySelector('.pull-reviewer-status[data-review-id="' + reviewId + '"] .pull-review-feedback');
|
||||
if (panel) panel.open = true;
|
||||
});
|
||||
doc.querySelectorAll('[data-review-feedback-file]').forEach(button => {
|
||||
button.addEventListener('click', () => focusPullFile(doc, button.dataset.reviewFeedbackFile));
|
||||
});
|
||||
@@ -336,7 +354,33 @@ function bindFeedbackControls(doc, controller, getSelected, getDetail, getLogin)
|
||||
'Matching change opened below.' : 'Matching change is unavailable in this preview.';
|
||||
panel.scrollIntoView({ block:'center', behavior:'smooth' });
|
||||
};
|
||||
qs('#pull-reviewer-statuses').addEventListener('click', event => {
|
||||
qs('#pull-reviewer-statuses').addEventListener('click', async event => {
|
||||
const loadButton = event.target.closest?.('[data-load-review-feedback]');
|
||||
if (loadButton && !loadButton.disabled) {
|
||||
const detail = getDetail?.();
|
||||
const item = getSelected?.();
|
||||
const reviewId = Number(loadButton.dataset.loadReviewFeedback);
|
||||
const reviewer = (detail?.reviewers || []).find(candidate => candidate.review_id === reviewId);
|
||||
if (!detail || !item || !reviewer) return;
|
||||
const refresh = reviewer.feedback_state === 'error';
|
||||
reviewer.feedback_state = 'loading';
|
||||
renderReviewerPanel(doc, detail);
|
||||
try {
|
||||
const result = await controller.loadFeedback(item, detail, reviewer, { refresh });
|
||||
if (getDetail?.() !== detail || getSelected?.() !== item ||
|
||||
result.head_sha !== detail.head_sha || Number(result.review_id) !== reviewId) return;
|
||||
reviewer.comments = Array.isArray(result.comments) ? result.comments : [];
|
||||
reviewer.feedback_state = 'loaded';
|
||||
renderReviewerPanel(doc, detail);
|
||||
doc.querySelector('[data-address-review-feedback="' + reviewId + '"]')?.focus();
|
||||
} catch (error) {
|
||||
if (getDetail?.() !== detail || getSelected?.() !== item) return;
|
||||
reviewer.feedback_state = 'error';
|
||||
renderReviewerPanel(doc, detail);
|
||||
doc.querySelector('[data-load-review-feedback="' + reviewId + '"]')?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const button = event.target.closest?.('[data-address-review-feedback]');
|
||||
if (!button) return;
|
||||
const detail = getDetail?.();
|
||||
@@ -570,6 +614,8 @@ function createPullSheet({ fetchJson, storage, createConversationPager = globalT
|
||||
let feedbackRequest = null;
|
||||
const reviewRequests = new Map();
|
||||
const reviewCache = new Map();
|
||||
const feedbackRequests = new Map();
|
||||
const feedbackCache = new Map();
|
||||
const pathFor = item => 'api/v1/repos/' + String(item.repository || '').split('/')
|
||||
.map(encodeURIComponent).join('/') + '/pulls/' + encodeURIComponent(item.number);
|
||||
const draftKey = item => 'stackchain.pull-comment.v1:' + item.repository + '#' + item.number;
|
||||
@@ -609,6 +655,25 @@ function createPullSheet({ fetchJson, storage, createConversationPager = globalT
|
||||
reviewRequests.set(key, request);
|
||||
return request;
|
||||
},
|
||||
loadFeedback(item, detail, reviewer, { refresh = false } = {}) {
|
||||
const reviewId = Number(reviewer?.review_id);
|
||||
const headSha = detail?.head_sha || '';
|
||||
const key = item.repository + '#' + item.number + ':' + headSha + ':' + reviewId;
|
||||
if (!refresh && feedbackCache.has(key)) return Promise.resolve(feedbackCache.get(key));
|
||||
if (feedbackRequests.has(key)) return feedbackRequests.get(key);
|
||||
const request = fetchJson(pathFor(item) + '/reviews/' + encodeURIComponent(reviewId) +
|
||||
'/feedback?expected_head_sha=' + encodeURIComponent(headSha), {
|
||||
headers: { Accept: 'application/json' },
|
||||
}).then(result => {
|
||||
if (result?.head_sha !== headSha || Number(result?.review_id) !== reviewId) {
|
||||
throw new Error('Review feedback changed. Reload review data before retrying.');
|
||||
}
|
||||
feedbackCache.set(key, result);
|
||||
return result;
|
||||
}).finally(() => feedbackRequests.delete(key));
|
||||
feedbackRequests.set(key, request);
|
||||
return request;
|
||||
},
|
||||
loadChecks(item) {
|
||||
if (checkRequest) return checkRequest;
|
||||
checkRequest = fetchJson(pathFor(item) + '/checks', {
|
||||
|
||||
Reference in new issue
Block a user