From f78d69fba5df7420577a6c5a762d22aac54e37e4 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 12 Aug 2026 11:58:11 +0000 Subject: [PATCH] fix: open push digests in Updates inbox (Closes #647) --- frontend/dashboard.js | 51 ++++++++++++++++++++++++++++-------------- frontend/work-route.js | 15 +++++++++++++ tests/test_my_work.py | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 17 deletions(-) diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 7ef8d6f..3163d24 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -927,6 +927,7 @@ else if (item.kind === 'issue') openIssueSheet(item, issueTrigger); else if (item.kind === 'pull') openPullSheet(item, pullTrigger); }, + onQueue: openWorkQueueRoute, onClose: () => { if (selectedIssue) closeIssueSheet(false); if (selectedPull) closePullSheet(false); @@ -5521,25 +5522,41 @@ document.querySelectorAll('[data-work-filter]').forEach(button => { button.setAttribute('aria-pressed', String(button.dataset.workFilter === selectedWorkFilter)); button.addEventListener('click', () => { - const leavingUpdates = selectedWorkFilter === 'update' && button.dataset.workFilter !== 'update'; - selectedWorkFilter = button.dataset.workFilter; - if (leavingUpdates && notificationSelection.snapshot().active) notificationSelection.cancel(); - savedWorkFilter = selectedWorkFilter; - launchFilterResolved = true; - try { - sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter); - } catch (e) { - console.warn('Could not persist My Work filter', e); - } - document.querySelectorAll('[data-work-filter]').forEach(item => - item.setAttribute('aria-pressed', String(item === button)) - ); - const selectedCount = button.querySelector('[data-work-count]').textContent; - qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')'; - renderMyWork(); - updateWorkPaginationControls(); + selectWorkQueue(button.dataset.workFilter); }); }); + function selectWorkQueue(filter, { preserveRoute = false } = {}) { + const button = qs('[data-work-filter="' + filter + '"]'); + if (!button) return false; + const leavingUpdates = selectedWorkFilter === 'update' && filter !== 'update'; + selectedWorkFilter = filter; + if (leavingUpdates && notificationSelection.snapshot().active) notificationSelection.cancel(); + savedWorkFilter = selectedWorkFilter; + launchFilterResolved = true; + try { + sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter); + } catch (e) { + console.warn('Could not persist My Work filter', e); + } + document.querySelectorAll('[data-work-filter]').forEach(item => + item.setAttribute('aria-pressed', String(item === button)) + ); + const selectedCount = button.querySelector('[data-work-count]').textContent; + qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')'; + renderMyWork(); + updateWorkPaginationControls(); + if (!preserveRoute && filter === 'update' && window.location.hash !== '#/my-work/updates') { + window.history.pushState({ workQueue:'update' }, '', '#/my-work/updates'); + } else if (!preserveRoute && filter !== 'update' && window.location.hash === '#/my-work/updates') { + window.history.replaceState(null, '', window.location.pathname + window.location.search); + } + return true; + } + function openWorkQueueRoute(filter) { + if (!selectWorkQueue(filter, { preserveRoute:true })) return; + qs('#my-work').scrollIntoView({block:'start'}); + qs('#my-work').focus(); + } function openDeliveryReceiptRoute() { if (window.location.hash !== '#/my-work/drafts') return; qs('[data-work-filter="draft"]').click(); diff --git a/frontend/work-route.js b/frontend/work-route.js index 6d5bf37..99b2f03 100644 --- a/frontend/work-route.js +++ b/frontend/work-route.js @@ -15,6 +15,9 @@ function parse(fragment) { const parts = String(fragment || '').split('/'); if (parts[0] !== '#' || parts[1] !== 'my-work') return null; + if (parts[2] === 'updates' && parts.length === 3) { + return { kind: 'queue', filter: 'update' }; + } if (parts[2] === 'update' && parts.length === 4) { const notificationId = positiveInteger(parts[3]); return notificationId ? { kind: 'update', notification_id: notificationId } : null; @@ -38,6 +41,7 @@ } function sameRoute(item, route) { + if (route.kind === 'queue') return false; if (route.kind === 'update') { return Number(item.notification_id) === route.notification_id; } @@ -48,6 +52,7 @@ function createController({ location, history, eventTarget, onOpen, onClose, onInvalid, + onQueue = function () {}, resolve, onResolving = function () {}, onError = function () {}, }) { let items = []; @@ -100,6 +105,16 @@ active = ''; return; } + if (route.kind === 'queue') { + if (!ready) return; + resolution += 1; + resolving = ''; + if (active && active !== fragment) onClose(); + if (active === fragment) return; + active = fragment; + onQueue(route.filter); + return; + } const item = items.find(candidate => sameRoute(candidate, route)); if (!item) { if (ready) resolveMissing(fragment, route); diff --git a/tests/test_my_work.py b/tests/test_my_work.py index ee0e9cd..17b2bba 100644 --- a/tests/test_my_work.py +++ b/tests/test_my_work.py @@ -395,6 +395,49 @@ process.stdout.write(JSON.stringify({{calls, hash:location.hash}})); assert output["hash"] == "#/my-work/review/stackchain/dashboard/10" +def test_updates_inbox_route_survives_hydration_and_detail_back_navigation(): + script = f""" +const routes = require({json.dumps(str(WORK_ROUTE))}); +const listeners = {{}}; +const location = {{hash:'#/my-work/updates'}}; +const calls = []; +const stack = ['#/my-work/updates']; +let cursor = 0; +const history = {{ + pushState(state, _, hash) {{ stack.splice(cursor + 1); stack.push(hash); cursor += 1; location.hash = hash; }}, + replaceState(state, _, hash) {{ stack[cursor] = hash; location.hash = hash; }}, + back() {{ cursor -= 1; location.hash = stack[cursor]; listeners.popstate(); }}, +}}; +const controller = routes.createController({{ + location, history, + eventTarget: {{addEventListener(name, fn) {{ listeners[name] = fn; }}}}, + onQueue: queue => calls.push(['queue', queue]), + onOpen: item => calls.push(['open', item.notification_id]), + onClose: () => calls.push(['close']), + onInvalid: () => calls.push(['invalid']), +}}); +controller.start(); +controller.setItems([]); +controller.open({{kind:'update', notification_id:42}}); +controller.close(); +process.stdout.write(JSON.stringify({{calls, hash:location.hash, parsed:routes.parse(location.hash)}})); +""" + result = subprocess.run( + ["node", "-e", script], check=True, capture_output=True, text=True + ) + + assert json.loads(result.stdout) == { + "calls": [ + ["queue", "update"], + ["open", 42], + ["close"], + ["queue", "update"], + ], + "hash": "#/my-work/updates", + "parsed": {"kind": "queue", "filter": "update"}, + } + + def test_work_route_controller_resolves_cold_routes_without_erasing_the_fragment(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); @@ -507,6 +550,10 @@ async def test_dashboard_wires_addressable_work_sheets_back_navigation_and_share assert html.count('class="share-work-route"') == 4 assert 'createWorkRoute.share(window.location.href, navigator, navigator.clipboard)' in html assert 'Route unavailable ยท this item is no longer in My Work.' in html + assert "onQueue: openWorkQueueRoute" in html + assert "selectWorkQueue(filter, { preserveRoute:true })" in html + assert "filter === 'update' && window.location.hash !== '#/my-work/updates'" in html + assert "window.history.pushState({ workQueue:'update' }, '', '#/my-work/updates')" in html def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity(): -- 2.43.0