155 lines
5.5 KiB
JavaScript
155 lines
5.5 KiB
JavaScript
(function (root, factory) {
|
||
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||
else root.createTodayHandoff = factory;
|
||
})(typeof self !== 'undefined' ? self : this, function createTodayHandoff(options) {
|
||
const prefix = 'stackchain.today-handoff.v1.';
|
||
const storage = options.storage || null;
|
||
const selected = new Set();
|
||
const excluded = new Set();
|
||
|
||
function key() {
|
||
const login = String(options.getLogin?.() || '').trim().toLowerCase();
|
||
return login ? prefix + encodeURIComponent(login) : '';
|
||
}
|
||
|
||
function pending() {
|
||
const storageKey = key();
|
||
if (!storageKey || !storage) return [];
|
||
try {
|
||
const value = JSON.parse(storage.getItem(storageKey) || '[]');
|
||
return Array.isArray(value) ? value.filter(id => typeof id === 'string' && id) : [];
|
||
} catch (_) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function write(ids) {
|
||
const storageKey = key();
|
||
if (!storageKey || !storage) return false;
|
||
const unique = [...new Set(ids.filter(id => typeof id === 'string' && id))];
|
||
try {
|
||
if (unique.length) storage.setItem(storageKey, JSON.stringify(unique));
|
||
else storage.removeItem(storageKey);
|
||
return true;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function capture(ids) {
|
||
const merged = [...pending()];
|
||
(ids || []).forEach(id => {
|
||
if (typeof id === 'string' && id && !merged.includes(id)) merged.push(id);
|
||
if (typeof id === 'string' && id) {
|
||
selected.add(id);
|
||
excluded.delete(id);
|
||
}
|
||
});
|
||
return write(merged);
|
||
}
|
||
|
||
function review(items) {
|
||
const byIdentity = new Map((items || options.items?.() || []).map(item => [options.identity(item), item]));
|
||
return pending().flatMap(identity => {
|
||
const item = byIdentity.get(identity);
|
||
if (!excluded.has(identity)) selected.add(identity);
|
||
return item ? [{ identity, item, selected:selected.has(identity) }] : [];
|
||
});
|
||
}
|
||
|
||
function choose(identity, include) {
|
||
if (!pending().includes(identity)) return false;
|
||
if (include) {
|
||
selected.add(identity);
|
||
excluded.delete(identity);
|
||
} else {
|
||
selected.delete(identity);
|
||
excluded.add(identity);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function render(items, list, escapeHtml) {
|
||
const rows = review(items);
|
||
list.innerHTML = rows.map(row => {
|
||
const title = String(row.item.title || row.identity).slice(0, 180);
|
||
const context = String(row.item.key || row.item.repository || 'Work item').slice(0, 180);
|
||
return '<div class="today-handoff-item"><span><strong>' + escapeHtml(title) + '</strong>' +
|
||
'<span class="small muted">' + escapeHtml(context) + '</span></span><label><input type="checkbox" ' +
|
||
'data-today-handoff="' + escapeHtml(row.identity) + '"' + (row.selected ? ' checked' : '') +
|
||
'> Today</label></div>';
|
||
}).join('');
|
||
list.querySelectorAll('[data-today-handoff]').forEach(input => input.addEventListener('change', () =>
|
||
choose(input.dataset.todayHandoff, input.checked)));
|
||
return rows;
|
||
}
|
||
|
||
async function commit(items) {
|
||
const rows = review(items);
|
||
const remaining = pending();
|
||
let scheduled = 0;
|
||
let blocked = 0;
|
||
for (const row of rows) {
|
||
if (!selected.has(row.identity)) continue;
|
||
if (options.todayWork.contains(row.item)) {
|
||
const index = remaining.indexOf(row.identity);
|
||
if (index >= 0) remaining.splice(index, 1);
|
||
selected.delete(row.identity);
|
||
continue;
|
||
}
|
||
const added = options.todayWork.add(row.item);
|
||
if (added !== 'added') {
|
||
blocked += 1;
|
||
continue;
|
||
}
|
||
if (!options.todaySync.enqueue('add', row.identity)) {
|
||
options.todayWork.remove?.(row.item);
|
||
blocked += 1;
|
||
continue;
|
||
}
|
||
const index = remaining.indexOf(row.identity);
|
||
if (index >= 0) remaining.splice(index, 1);
|
||
selected.delete(row.identity);
|
||
scheduled += 1;
|
||
}
|
||
write(remaining);
|
||
if (scheduled) await options.todaySync.flush();
|
||
return { scheduled, blocked, remaining:remaining.length };
|
||
}
|
||
|
||
function mount({ qs, escapeHtml, onComplete = () => {} }) {
|
||
const list = qs('#today-handoff-items');
|
||
let prompted = false;
|
||
function open() {
|
||
if (prompted) return false;
|
||
const rows = render(null, list, escapeHtml);
|
||
if (!rows.length) return false;
|
||
qs('#today-handoff-status').textContent = rows.length +
|
||
(rows.length === 1 ? ' commitment is ready to review.' : ' commitments are ready to review.');
|
||
const dialog = qs('#today-handoff-dialog');
|
||
if (!dialog.open) dialog.showModal();
|
||
prompted = true;
|
||
requestAnimationFrame(() => (list.querySelector('input') || qs('#confirm-today-handoff')).focus());
|
||
return true;
|
||
}
|
||
qs('#close-today-handoff').addEventListener('click', () => qs('#today-handoff-dialog').close());
|
||
qs('#confirm-today-handoff').addEventListener('click', async event => {
|
||
const button = event.currentTarget;
|
||
button.disabled = true;
|
||
qs('#today-handoff-status').textContent = 'Saving Today’s plan…';
|
||
try {
|
||
const result = await commit();
|
||
if (result.blocked) qs('#today-handoff-status').textContent =
|
||
'Today is full. ' + result.remaining + ' commitment remains safe in My Work.';
|
||
else qs('#today-handoff-dialog').close();
|
||
onComplete(result);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
return { open, reset:() => { prompted = false; } };
|
||
}
|
||
|
||
return { capture, choose, commit, mount, pending, render, review };
|
||
});
|