feat: retire closed issues during Following review (Closes #1305)
CI / lint (pull_request) Successful in 3m30s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 5m22s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-23 12:12:55 +00:00
1 parent fdf38a72ac
commit c4bb7a9be1
20 files changed
+278 -46

No files matched your search

+1
View File
@@ -5774,6 +5774,7 @@
if (!followingQueue.session()) taskOverlayHistory.update({preview:item});
},
onOpened:item => followingQueue.previewLoaded(item),
afterUnwatch:item => followingQueue.retire(item),
navigationRoot:document,
onState: renderSearchPreview,
});
+28 -5
View File
@@ -63,7 +63,7 @@
async function open(index) {
const item = snapshot.items[Number(index)];
if (!item) return false;
await options.onOpen?.({...item, kind:'issue'});
await options.onOpen?.({...item, kind:'issue', following:true});
await acknowledge(item);
return true;
}
@@ -71,7 +71,7 @@
async function startReview() {
const items = snapshot.items
.filter(item => item.has_unseen_change === true)
.map(item => ({...item, kind:'issue'}));
.map(item => ({...item, kind:'issue', following:true}));
if (!items.length) return false;
review = {items, more:false, active:true, acknowledged:new Set()};
await open(snapshot.items.indexOf(snapshot.items.find(item =>
@@ -80,8 +80,8 @@
}
function finishReview() {
const completed = Boolean(review?.active &&
!snapshot.items.some(item => item.has_unseen_change === true));
const completed = Boolean(review?.completed || (review?.active &&
!snapshot.items.some(item => item.has_unseen_change === true)));
if (review) review.active = false;
if (completed && !review.completed) {
review.completed = true;
@@ -91,8 +91,30 @@
return completed;
}
function retire(item) {
const same = candidate => candidate.repository === item?.repository &&
Number(candidate.number) === Number(item?.number);
const index = review?.active ? review.items.findIndex(same) : -1;
snapshot.items = snapshot.items.filter(candidate => !same(candidate));
if (index < 0) {
publish('ready');
return null;
}
review.items.splice(index, 1);
const next = review.items[index] || null;
if (!next) {
review.active = false;
if (!review.completed) {
review.completed = true;
options.onReviewComplete?.();
}
}
publish('ready');
return next ? {...next} : null;
}
return {
load, open, startReview, previewLoaded:acknowledge, finishReview,
load, open, startReview, previewLoaded:acknowledge, finishReview, retire,
session:() => review?.active ? {items:[...review.items], more:false} : null,
items:() => snapshot.items.map(item => ({...item})),
count:() => snapshot.items.length,
@@ -179,6 +201,7 @@
},
session:feature.session,
previewLoaded:feature.previewLoaded,
retire:feature.retire,
returnToFollowing() {
const completed = feature.finishReview();
if (completed) return 'completed-following';
+25 -6
View File
@@ -34,8 +34,12 @@
'/comments?kind=' + encodeURIComponent(item.kind);
root.searchPreviewSubscriptionPath = item => root.searchPreviewPath(item).replace(/\?.*$/, '') +
'/subscription?kind=' + encodeURIComponent(item.kind);
root.searchPreviewSubscriptionOptions = fetchJson => ({
root.searchPreviewSubscriptionOptions = fetchJson => {
const options = {
load:async detail => {
if (detail.kind === 'issue' && detail.state === 'closed' && detail.following === true) {
return {...detail, watching:true};
}
if (!(detail.kind === 'issue' && detail.state === 'open')) return detail;
const result = await fetchJson(root.searchPreviewSubscriptionPath(detail), {headers:{Accept:'application/json'}});
return {...detail, watching:result.watching === true};
@@ -43,7 +47,12 @@
watch:(detail,watching) => fetchJson(root.searchPreviewSubscriptionPath(detail), {
method:watching ? 'PUT' : 'DELETE', headers:{Accept:'application/json'},
}),
});
};
options.preview = async item => options.load({...item, ...await fetchJson(root.searchPreviewPath(item), {
headers:{Accept:'application/json'},
})});
return options;
};
root.searchPreviewWatchStatus = state => ({
watching:'Starting watch…', unwatching:'Stopping watch…',
watched:'Watching · available in Following. Future activity will appear in Updates.',
@@ -52,8 +61,11 @@
'watch-error':(state.error?.message || 'Watch status was not changed.') + ' Retry.',
})[state.status] || '';
root.renderSearchPreviewWatch = (detail, state, button) => {
button.hidden = !(detail.kind === 'issue' && detail.state === 'open');
button.textContent = detail.watching ? 'Stop watching' : 'Watch issue';
const retiring = detail.kind === 'issue' && detail.state === 'closed' &&
detail.following === true && detail.watching === true;
button.hidden = !(detail.kind === 'issue' && detail.state === 'open') && !retiring;
button.textContent = retiring ? 'Stop watching & next' :
(detail.watching ? 'Stop watching' : 'Watch issue');
button.disabled = state.status === 'watching' || state.status === 'unwatching';
};
root.wireSearchPreviewWatch = (button, preview, getDetail) => button.addEventListener('click', () => {
@@ -120,7 +132,7 @@
};
}
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
return function createSearchPreview({ fetchJson, fetchConversation, mutate, watch, share, postReply, queueReply, prepareReply, afterReply, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, onOpened, navigationRoot, onState }) {
return function createSearchPreview({ fetchJson, fetchConversation, mutate, watch, share, postReply, queueReply, prepareReply, afterReply, afterUnwatch, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, onOpened, navigationRoot, onState }) {
if (Array.isArray(session)) {
getSession = session[0];
loadMore = () => session[1].loadMore();
@@ -385,11 +397,18 @@
}
const detail = current;
publish({ status:watching ? 'watching' : 'unwatching', item:current, detail });
watchRequest = watch(detail, watching).then(result => {
watchRequest = watch(detail, watching).then(async result => {
current = { ...current, watching:result?.watching === true };
const status = result?.following_synced === false
? 'watch-partial' : (watching ? 'watched' : 'unwatched');
publish({ status, item:current, detail:current, result });
if (!watching && result?.watching === false && typeof afterUnwatch === 'function') {
const next = await afterUnwatch({...current});
if (next) {
onNavigate?.(next);
await api.open(next);
}
}
return result;
}).catch(error => {
publish({ status:'watch-error', item:current, detail, error });
+1 -1
View File
@@ -1,7 +1,7 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/private-data-registry.js');
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v133';
const CACHE = 'stackchain-dashboard-shell-v134';
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;