feat: sync completed Filed reviews across devices (Closes #880)
This commit is contained in:
1 parent
8e7a21f316
commit
6acc8866ed
7 files changed
+380
-9
No files matched your search
+34
-1
@@ -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
@@ -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; }
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user