function createProgressiveMyWork({ document, fetchSnapshot, pollerOptions = {}, lifecycleTarget = globalThis, }) { const list = document.querySelector('#my-work-list'); const status = document.querySelector('#my-work-status'); const detail = document.querySelector('#progressive-work-detail'); const detailTitle = document.querySelector('#progressive-work-detail-title'); const detailMeta = document.querySelector('#progressive-work-detail-meta'); const detailReason = document.querySelector('#progressive-work-detail-reason'); const closeDetail = document.querySelector('#close-progressive-work-detail'); const openGitea = document.querySelector('#open-progressive-work-gitea'); const filters = Array.from(document.querySelectorAll('[data-work-filter]')); const listeners = []; const lifecycleListeners = []; let items = []; let active = 'all'; let stopped = false; let selectedByUser = false; let liveSnapshot = null; let liveSnapshotPromise = null; let confirmedLogin = ''; let poller = null; let detailTrigger = null; let openWork = null; const deferredQueues = { today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts', }; const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''', })[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 matches = (item, filter) => filter === 'review' ? item.is_review : filter === 'update' ? item.has_update : filter === 'attention' ? item.needs_attention : filter === 'filed' ? item.is_filed : filter === 'authored' ? item.is_authored : filter === 'pull' ? item.kind === 'pull' && !item.is_review : item.kind === filter; const visibleItems = () => active === 'all' ? items : items.filter(item => matches(item, active)); const closeProgressiveDetail = ({ restoreFocus = true } = {}) => { if (!detail || detail.hidden) return; detail.hidden = true; if (restoreFocus) detailTrigger?.focus?.(); detailTrigger = null; openWork = null; }; const openProgressiveDetail = (item, trigger) => { if (!detail || !item) return; detailTrigger = trigger; openWork = item; detailTitle.textContent = item.title || item.key || 'Untitled work'; detailMeta.textContent = (item.key || 'Unknown work item') + ' · ' + (item.kind === 'pull' ? 'Pull request' : 'Issue'); detailReason.textContent = item.reason || 'Assigned to you'; const href = safeUrl(item.url); openGitea.hidden = !href; if (href) openGitea.href = href; else openGitea.removeAttribute?.('href'); detail.hidden = false; closeDetail?.focus?.(); }; const updateCounts = () => { ['all','attention','filed','authored','issue','pull','review','update'].forEach(filter => { const count = filter === 'all' ? items.length : items.filter(item => matches(item, filter)).length; const element = document.querySelector('[data-work-count="' + filter + '"]'); if (element) element.textContent = String(count); }); }; const render = () => { if (stopped || !list) return; const visible = visibleItems(); list.innerHTML = deferredQueues[active] ? '
' + deferredQueues[active] + ' is still loading…
' : visible.length ? visible.map((item, index) => { const title = escapeHtml(item.title || item.key || 'Untitled work'); const context = escapeHtml(item.key || ''); const reason = escapeHtml(item.reason || 'Assigned to you'); return '
' + '
'; }).join('') : '
No work in this queue.
'; filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active))); }; filters.forEach(button => { const listener = () => { active = button.dataset.workFilter || 'all'; selectedByUser = true; render(); }; button.addEventListener('click', listener); listeners.push([button, listener]); }); const openListener = event => { const trigger = event.target?.closest?.('[data-progressive-work-index]'); if (!trigger) return; const item = visibleItems()[Number(trigger.dataset.progressiveWorkIndex)]; if (!item) return; event.preventDefault?.(); openProgressiveDetail(item, trigger); }; const closeListener = () => closeProgressiveDetail(); const keyListener = event => { if (event.key !== 'Escape' || detail?.hidden) return; event.preventDefault?.(); closeProgressiveDetail(); }; list?.addEventListener?.('click', openListener); closeDetail?.addEventListener?.('click', closeListener); lifecycleTarget.addEventListener?.('keydown', keyListener); const applySnapshot = snapshot => { if (stopped) return false; const transferable = snapshot && typeof snapshot === 'object' && Object.prototype.hasOwnProperty.call(snapshot, 'context'); if (transferable) liveSnapshot = snapshot; const context = snapshot?.context || snapshot || {}; confirmedLogin = String(context.user?.login || '').trim(); items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] }); updateCounts(); render(); const assigned = items.filter(item => item.is_assigned).length; if (status) status.textContent = assigned + ' assigned work item' + (assigned === 1 ? '' : 's') + ' ready.'; return true; }; if (typeof createContextPoller === 'function') { poller = createContextPoller({ ...pollerOptions, fetchContext: fetchSnapshot, onSnapshot: applySnapshot, onError: () => { if (status && !stopped) status.textContent = 'Assigned work is reconnecting…'; }, isHidden: pollerOptions.isHidden || (() => Boolean(document.hidden)), }); const recover = () => { if (!document.hidden) void poller.refresh(); }; ['online', 'visibilitychange'].forEach(eventName => { lifecycleTarget.addEventListener?.(eventName, recover); lifecycleListeners.push([eventName, recover]); }); } return { login() { return confirmedLogin; }, handoff() { const state = { selectedFilter:selectedByUser ? active : null }; if (openWork) { state.openWork = { kind:openWork.kind, key:openWork.key, number:openWork.number, repository:openWork.repository, }; openWork = null; } if (liveSnapshot) { state.liveSnapshot = liveSnapshot; liveSnapshot = null; liveSnapshotPromise = null; } else if (liveSnapshotPromise) { state.liveSnapshotPromise = liveSnapshotPromise; liveSnapshotPromise = null; } return state; }, async start() { if (status) status.textContent = 'Loading assigned work…'; if (poller) { const request = poller.start(); liveSnapshotPromise = request.then(snapshot => ( snapshot && typeof snapshot === 'object' && Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null )); return Boolean(await request); } const request = Promise.resolve().then(() => fetchSnapshot()); liveSnapshotPromise = request.then(snapshot => ( snapshot && typeof snapshot === 'object' && Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null ), () => null); try { const snapshot = await request; if (stopped) return false; const applied = applySnapshot(snapshot); if (!liveSnapshot) liveSnapshotPromise = null; return applied; } catch (_error) { if (status && !stopped) status.textContent = 'Assigned work is reconnecting…'; return false; } }, stop() { stopped = true; poller?.stop(); listeners.forEach(([button, listener]) => button.removeEventListener?.('click', listener)); list?.removeEventListener?.('click', openListener); closeDetail?.removeEventListener?.('click', closeListener); lifecycleTarget.removeEventListener?.('keydown', keyListener); lifecycleListeners.forEach(([eventName, listener]) => lifecycleTarget.removeEventListener?.(eventName, listener)); }, }; } if (typeof window !== 'undefined' && typeof document !== 'undefined') { window.stackchainProgressiveMyWork = createProgressiveMyWork({ document, lifecycleTarget: window, fetchSnapshot: async (revisions = {}, { signal } = {}) => { const query = createContextPoller.buildRevisionQuery(revisions); const response = await fetch('api/v1/live' + (query ? '?' + query : ''), { headers:{Accept:'application/json'}, signal, }); if (!response.ok) { const error = new Error('HTTP ' + response.status); error.retryAfterMs = createContextPoller.retryAfterMs(response.headers.get('Retry-After')); throw error; } return response.json(); }, }); void window.stackchainProgressiveMyWork.start(); } if (typeof module !== 'undefined' && module.exports) module.exports = createProgressiveMyWork;