138 lines
5.5 KiB
JavaScript
138 lines
5.5 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
|
else root.createUpdateTriageSession = factory;
|
|
})(typeof self !== 'undefined' ? self : this, function createUpdateTriageSession(options) {
|
|
const key = options.key || 'stackchain.update-triage.v1';
|
|
const identity = item => Number.isInteger(item?.notification_id) ? String(item.notification_id) : '';
|
|
const login = () => String(options.getLogin() || '').trim();
|
|
let state = null;
|
|
let running = false;
|
|
let lastOutcome = null;
|
|
|
|
function read() {
|
|
const owner = login();
|
|
if (!owner) return null;
|
|
try {
|
|
const value = JSON.parse(options.storage.getItem(key) || 'null');
|
|
if (![1, 2].includes(value?.version) || value.login !== owner || !Array.isArray(value.identities) ||
|
|
!value.identities.length || typeof value.current !== 'string' || !Array.isArray(value.completed)) return null;
|
|
const identities = value.identities.filter(value => typeof value === 'string' && value);
|
|
const completed = value.completed.filter(value => identities.includes(value));
|
|
const kept = Array.isArray(value.kept) ? value.kept.filter(value => identities.includes(value)) : [];
|
|
if (!identities.length || !identities.includes(value.current)) return null;
|
|
return { version:2, login:owner, identities, current:value.current, completed, kept };
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function persist() {
|
|
if (!state) return;
|
|
options.storage.setItem(key, JSON.stringify(state));
|
|
}
|
|
|
|
function available() {
|
|
const byIdentity = new Map((options.getItems() || []).map(item => [identity(item), item]));
|
|
return state.identities.filter(id => !state.completed.includes(id) && byIdentity.has(id))
|
|
.map(id => byIdentity.get(id));
|
|
}
|
|
|
|
function nextAvailable() {
|
|
if (!running || !state) return null;
|
|
const candidates = available().filter(item => identity(item) !== state.current);
|
|
return candidates.find(item => state.identities.indexOf(identity(item)) > state.identities.indexOf(state.current)) || candidates[0] || null;
|
|
}
|
|
|
|
function finish() {
|
|
const unread = new Set((options.getItems() || []).map(identity).filter(Boolean));
|
|
const keptIdentities = state ? state.kept.filter(id => unread.has(id)) : [];
|
|
const reviewed = state ? state.completed.length : 0;
|
|
const outcome = { reviewed, resolved:reviewed - keptIdentities.length, kept:keptIdentities.length, keptIdentities };
|
|
lastOutcome = outcome;
|
|
running = false;
|
|
state = null;
|
|
options.storage.removeItem(key);
|
|
options.onFinish(outcome);
|
|
return false;
|
|
}
|
|
|
|
function openCurrent(preferredIndex = null) {
|
|
const items = available();
|
|
if (!items.length) return finish();
|
|
let index = items.findIndex(item => identity(item) === state.current);
|
|
if (index < 0) index = Math.min(preferredIndex ?? 0, items.length - 1);
|
|
state.current = identity(items[index]);
|
|
persist();
|
|
const originalIndex = state.identities.indexOf(state.current);
|
|
const progress = {
|
|
index:originalIndex + 1,
|
|
total:state.identities.length,
|
|
};
|
|
if (items[index]?.update_reason) progress.reason = String(items[index].update_reason);
|
|
options.onProgress(progress);
|
|
options.onOpen(items[index]);
|
|
return true;
|
|
}
|
|
|
|
function advance(openNext = true) {
|
|
if (!running || !state) return false;
|
|
const originalIndex = state.identities.indexOf(state.current);
|
|
if (!state.completed.includes(state.current)) state.completed.push(state.current);
|
|
const candidates = available();
|
|
const next = candidates.find(item => state.identities.indexOf(identity(item)) > originalIndex) || candidates[0];
|
|
if (!next) return finish();
|
|
state.current = identity(next);
|
|
if (openNext) return openCurrent();
|
|
persist();
|
|
const progress = {
|
|
index:state.identities.indexOf(state.current) + 1,
|
|
total:state.identities.length,
|
|
};
|
|
if (next?.update_reason) progress.reason = String(next.update_reason);
|
|
options.onProgress(progress);
|
|
return true;
|
|
}
|
|
|
|
return {
|
|
active: () => running,
|
|
resumable: () => Boolean(read()),
|
|
start() {
|
|
const identities = (options.getItems() || []).map(identity).filter(Boolean);
|
|
if (!identities.length) return finish();
|
|
state = { version:2, login:login(), identities, current:identities[0], completed:[], kept:[] };
|
|
if (!state.login) return false;
|
|
running = true;
|
|
return openCurrent();
|
|
},
|
|
resume() {
|
|
state = read();
|
|
if (!state) return false;
|
|
running = true;
|
|
return openCurrent(state.identities.indexOf(state.current));
|
|
},
|
|
reconcile() {
|
|
if (!running || !state) return false;
|
|
return openCurrent(state.identities.indexOf(state.current));
|
|
},
|
|
completeAndNext: advance,
|
|
acceptCompleted: () => advance(false),
|
|
keepUnreadAndNext() {
|
|
if (!running || !state) return false;
|
|
if (!state.kept.includes(state.current)) state.kept.push(state.current);
|
|
return advance();
|
|
},
|
|
reviewKept(identities = lastOutcome?.keptIdentities || []) {
|
|
const unread = new Set((options.getItems() || []).map(identity).filter(Boolean));
|
|
const kept = (identities || []).filter(id => unread.has(id));
|
|
if (!kept.length) return false;
|
|
state = { version:2, login:login(), identities:kept, current:kept[0], completed:[], kept:[] };
|
|
if (!state.login) return false;
|
|
running = true;
|
|
return openCurrent();
|
|
},
|
|
next: nextAvailable,
|
|
items: () => state ? available().slice() : [],
|
|
end: finish,
|
|
};
|
|
});
|