stackchain-dashboard/frontend/progressive-my-work.js
timmy 297ae7550a
All checks were successful
CI / lint (pull_request) Successful in 3m33s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 6m40s
CI / release-candidate (pull_request) Has been skipped
feat: hydrate mobile My Work before optional tools
Closes #1401
2026-08-25 16:41:27 +00:00

82 lines
3.5 KiB
JavaScript

function createProgressiveMyWork({ document, fetchSnapshot }) {
const list = document.querySelector('#my-work-list');
const status = document.querySelector('#my-work-status');
const filters = Array.from(document.querySelectorAll('[data-work-filter]'));
const listeners = [];
let items = [];
let active = 'all';
let stopped = false;
const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({
'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;',
})[character]);
const safeUrl = value => {
try {
const url = new URL(String(value || ''), globalThis.location?.href || 'https://invalid.example/');
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
} catch (_error) { return ''; }
};
const visibleItems = () => active === 'all' ? items : items.filter(item =>
active === 'review' ? item.is_review :
active === 'update' ? item.has_update :
active === 'attention' ? item.needs_attention : item.kind === active
);
const render = () => {
if (stopped || !list) return;
const visible = visibleItems();
list.innerHTML = visible.length ? visible.map(item => {
const href = safeUrl(item.url);
const title = escapeHtml(item.title || item.key || 'Untitled work');
const context = escapeHtml(item.key || '');
const reason = escapeHtml(item.reason || 'Assigned to you');
return '<article class="my-work-card progressive-my-work-card">' +
(href ? '<a class="my-work-card-main" href="' + escapeHtml(href) + '">' : '<div class="my-work-card-main">') +
'<strong>' + title + '</strong><span class="small">' + context + ' · ' + reason + '</span>' +
(href ? '</a>' : '</div>') + '</article>';
}).join('') : '<div class="muted">No work in this queue.</div>';
filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active)));
};
filters.forEach(button => {
const listener = () => { active = button.dataset.workFilter || 'all'; render(); };
button.addEventListener('click', listener);
listeners.push([button, listener]);
});
return {
async start() {
if (status) status.textContent = 'Loading assigned work…';
try {
const snapshot = await fetchSnapshot();
if (stopped) return false;
const context = snapshot?.context || snapshot || {};
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
render();
const assigned = items.filter(item => item.is_assigned).length;
if (status) status.textContent = assigned + ' assigned work item' + (assigned === 1 ? '' : 's') + ' ready.';
return true;
} catch (_error) {
if (status && !stopped) status.textContent = 'Assigned work is reconnecting…';
return false;
}
},
stop() {
stopped = true;
listeners.forEach(([button, listener]) => button.removeEventListener?.('click', listener));
},
};
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
window.stackchainProgressiveMyWork = createProgressiveMyWork({
document,
fetchSnapshot: async () => {
const response = await fetch('api/v1/live', { headers:{Accept:'application/json'} });
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
},
});
void window.stackchainProgressiveMyWork.start();
}
if (typeof module !== 'undefined' && module.exports) module.exports = createProgressiveMyWork;