feat: sync completed Filed reviews across devices (Closes #880)
CI / lint (pull_request) Successful in 1m49s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 1m0s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-15 08:27:19 +00:00
1 parent 8e7a21f316
commit 6acc8866ed
7 files changed
+380 -9

No files matched your search

+34 -1
View File
@@ -220,6 +220,7 @@
storage: localStorage,
getLogin() { return planningOwnerLogin; },
});
let completedFiledSyncFlight = null;
let activeMyWork = [];
let laterMyWork = [];
let todayMyWork = [];
@@ -330,6 +331,35 @@
return payload;
}
function syncCompletedFiledReviews() {
if (!planningOwnerLogin) return Promise.resolve(false);
if (completedFiledSyncFlight) return completedFiledSyncFlight;
const pending = completedFiledReview.pending();
const options = pending.length ? {
method:'POST',
headers:{Accept:'application/json', 'Content-Type':'application/json'},
body:JSON.stringify({ receipts:pending }),
} : { headers:{Accept:'application/json'} };
completedFiledSyncFlight = fetchReviewJson('api/v1/completed-filed-reviews', options)
.then(snapshot => {
const changed = completedFiledReview.adopt(snapshot);
if (lastContextSnapshot && changed) {
lastMyWork = completedFiledReview.visible(buildMyWork(lastContextSnapshot));
refreshMyWorkView();
}
if (pending.length) qs('#my-work-action-status').textContent =
'Completed Filed review saved to account.';
return true;
})
.catch(() => {
if (pending.length) qs('#my-work-action-status').textContent =
'Acknowledged on this device · sync pending.';
return false;
})
.finally(() => { completedFiledSyncFlight = null; });
return completedFiledSyncFlight;
}
let commentActions = { isOwned: () => false, wire: () => {} };
function loadMentionCandidates(repository, query) {
@@ -2517,6 +2547,7 @@
function paintMyWork(data) {
lastMyWork = completedFiledReview.visible(buildMyWork(data));
refreshMyWorkView();
void syncCompletedFiledReviews();
}
function listDrafts() {
@@ -3446,7 +3477,9 @@
refreshMyWorkView();
const target = filedFollowUpTarget(lastMyWork);
qs('#my-work-action-status').textContent = 'Reviewed ' + acknowledged.key + '.' +
(target ? ' Opening the next Filed item.' : ' Filed review is complete.');
(target ? ' Opening the next Filed item. Acknowledgement sync pending.' :
' Filed review is complete. Acknowledgement sync pending.');
void syncCompletedFiledReviews();
if (target) {
const index = lastMyWork.indexOf(target.item);
const trigger = qs('#my-work-list [data-' + target.kind + '-index="' + index + '"]');
+43 -7
View File
@@ -678,28 +678,64 @@ function createCompletedFiledReview({
const ownerKey = () => key + ':' + login();
const identity = item => String(item?.repository || '') + '#' + String(item?.number || '');
const read = () => {
if (!login()) return {};
if (!login()) return { items:{}, pending:{} };
try {
const value = JSON.parse(storage?.getItem(ownerKey()) || '{}');
return value && value.version === 1 && value.items && typeof value.items === 'object' ?
value.items : {};
} catch (_error) { return {}; }
return value && value.version === 1 && value.items && typeof value.items === 'object' ? {
items:value.items,
pending:value.pending && typeof value.pending === 'object' ? value.pending : {},
} : { items:{}, pending:{} };
} catch (_error) { return { items:{}, pending:{} }; }
};
const receipt = (item, stamp = String(item?.updated_at || '')) => ({
repository:String(item?.repository || ''), number:item?.number, updated_at:stamp,
});
const save = value => storage?.setItem(ownerKey(), JSON.stringify({ version:1, ...value }));
return {
visible(items) {
const acknowledged = read();
const acknowledged = read().items;
return (items || []).filter(item =>
!item?.is_completed || acknowledged[identity(item)] !== String(item.updated_at || '')
);
},
pending() {
const state = read();
return Object.entries(state.pending).map(([itemIdentity, stamp]) => {
const boundary = itemIdentity.lastIndexOf('#');
return receipt({
repository:itemIdentity.slice(0, boundary),
number:Number(itemIdentity.slice(boundary + 1)),
}, stamp);
});
},
adopt(snapshot) {
const state = read();
let changed = false;
(snapshot?.receipts || []).forEach(remote => {
const itemIdentity = identity(remote);
const stamp = String(remote?.updated_at || '');
if (!remote?.repository || !Number.isInteger(remote?.number) || !stamp) return;
if (!state.items[itemIdentity] || state.items[itemIdentity] < stamp) {
state.items[itemIdentity] = stamp;
changed = true;
}
if (state.pending[itemIdentity] && state.pending[itemIdentity] <= stamp) {
delete state.pending[itemIdentity];
}
});
try { save(state); } catch (_error) { return false; }
return changed;
},
acknowledge(item) {
const stamp = String(item?.updated_at || '');
if (!login() || !item?.is_completed || !item?.repository || !Number.isInteger(item?.number) || !stamp) {
return false;
}
const entries = Object.entries({ ...read(), [identity(item)]: stamp }).slice(-200);
const state = read();
const entries = Object.entries({ ...state.items, [identity(item)]: stamp }).slice(-200);
const pending = Object.entries({ ...state.pending, [identity(item)]: stamp }).slice(-200);
try {
storage?.setItem(ownerKey(), JSON.stringify({ version:1, items:Object.fromEntries(entries) }));
save({ items:Object.fromEntries(entries), pending:Object.fromEntries(pending) });
return true;
} catch (_error) { return false; }
},