From c4ba78c8cb0c90ebd3ed4c46474d09ee099d8653 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 12 Aug 2026 13:01:41 +0000 Subject: [PATCH] feat: consolidate mobile work commands (Closes #651) --- frontend/dashboard.css | 3 +-- frontend/dashboard.js | 11 +++++------ frontend/index.html | 1 + frontend/mobile-task-dock.js | 13 +++++-------- tests/test_mobile_launch.py | 2 +- tests/test_mobile_task_dock.py | 22 +++++++++++++++------- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 0cfd1a4..663fb90 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -560,8 +560,7 @@ textarea { resize: vertical; min-height: 120px; } .my-work { margin:0; } .attention-interruption:not([hidden]) { position:sticky; top:64px; z-index:6; margin-inline:max(0px,env(safe-area-inset-left)) max(0px,env(safe-area-inset-right)); } .my-work-header { align-items:flex-start; } - .my-work-actions { width:100%; flex-wrap:nowrap; } - .my-work-actions button { min-height:44px; flex:1 1 0; padding-inline:6px; } + .my-work-actions { display:none; } .work-settings > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; padding:0 10px; border:1px solid #2a496e; border-radius:10px; font-weight:700; } .work-settings:not([open]) > .work-settings-panel { display:none; } .work-settings-panel { display:grid; gap:10px; margin-top:8px; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 69cd98c..18e0502 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -37,7 +37,7 @@ try { localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state)); } catch (e) { - console.warn('Could not persist panel state', e); + console.warn('Panel state save failed', e); } }); }); @@ -95,7 +95,7 @@ Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element]) ), queueBadge: qs('#mobile-queue-count'), - onSelectQueue: name => mobileQueueLauncher.open(name), + onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name), overlays: mobileTaskOverlays, actions: { work: () => mobileWorkEntry.open(), @@ -129,7 +129,7 @@ const savedMilestone = sessionStorage.getItem(WORK_MILESTONE_KEY); if (savedMilestone) selectedWorkMilestone = savedMilestone; } catch (e) { - console.warn('Could not restore My Work filter', e); + console.warn('Filter restore failed', e); } let lastMyWork = []; let lastDrafts = []; @@ -3694,7 +3694,6 @@ } else { setEventStreamStatus('Update failed · showing last activity'); } - // Compatibility fallback for snapshots produced before section metadata. if (!eventsFreshness && snapshot.freshness?.degraded && !snapshot.freshness.revalidating) { const retrySeconds = Number(snapshot.freshness.retry_in_seconds) || 0; setEventStreamStatus('Refresh failed · showing last known data' + @@ -5555,7 +5554,7 @@ try { sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter); } catch (e) { - console.warn('Could not persist My Work filter', e); + console.warn('Filter save failed', e); } document.querySelectorAll('[data-work-filter]').forEach(item => item.setAttribute('aria-pressed', String(item === button)) @@ -5587,7 +5586,7 @@ qs('#work-milestone-filter').addEventListener('change', event => { selectedWorkMilestone = event.target.value; try { sessionStorage.setItem(WORK_MILESTONE_KEY, selectedWorkMilestone); } - catch (e) { console.warn('Could not persist My Work milestone lane', e); } + catch (e) { console.warn('Milestone save failed', e); } renderMyWork(); if (workSession.active()) workSession.reconcile(); }); diff --git a/frontend/index.html b/frontend/index.html index ffc6ab3..789f2c8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -869,6 +869,7 @@ + diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js index 022d50d..34efa04 100644 --- a/frontend/mobile-task-dock.js +++ b/frontend/mobile-task-dock.js @@ -34,7 +34,7 @@ } function closeQueues() { - if (options.queueSheet?.open) options.queueSheet.close(); + options.queueSheet?.open && options.queueSheet.close(); } function start() { @@ -42,8 +42,7 @@ button.addEventListener('click', event => { launcher = event.currentTarget; select(name); - if (name === 'queues' && options.queueSheet) openQueues(); - else options.actions[name](); + name === 'queues' && options.queueSheet ? openQueues() : options.actions[name](); }); }); if (options.queueSheet) { @@ -56,7 +55,7 @@ Object.entries(options.queueRows || {}).forEach(([name, row]) => { row.addEventListener('click', () => { closeQueues(); - options.onSelectQueue?.(name); + options.onSelectQueue?.(name, row); }); }); } @@ -95,10 +94,8 @@ } function updateQueues(counts) { - const names = ['today', 'attention', 'update', 'later', 'draft']; + const names = 'today attention update later draft'.split(' '); const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)])); - // Updates are already represented by the Attention destination, so keep - // the dock aggregate non-overlapping while exposing the useful drill-down. const total = ['today', 'attention', 'later', 'draft'].reduce((sum, name) => sum + normalized[name], 0); Object.entries(options.queueCounts || {}).forEach(([name, element]) => { element.textContent = String(normalized[name] || 0); @@ -110,7 +107,7 @@ options.queueBadge.textContent = String(total); options.queueBadge.hidden = total === 0; } - if (buttons.queues) buttons.queues.setAttribute('aria-label', 'Queues, ' + total + ' items'); + buttons.queues?.setAttribute('aria-label', 'Queues, ' + total + ' items'); } return {start, select, refreshVisibility, updateAttention, updateQueues, updateWork}; diff --git a/tests/test_mobile_launch.py b/tests/test_mobile_launch.py index 1a2a7b0..e9252dd 100644 --- a/tests/test_mobile_launch.py +++ b/tests/test_mobile_launch.py @@ -69,4 +69,4 @@ async def test_mobile_launch_progressively_discloses_secondary_controls(): assert "header { min-height:56px; max-height:64px;" in html assert ".work-settings:not([open]) > .work-settings-panel { display:none; }" in html assert ".app-menu:not([open]) > .app-menu-panel { display:none; }" in html - assert ".my-work-actions button { min-height:44px;" in html + assert ".my-work-actions { display:none; }" in html diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index 895a683..2d22ec3 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -293,13 +293,14 @@ sheet.showModal = function () {{ this.open = true; }}; sheet.close = function () {{ this.open = false; this.listeners.close?.(); }}; const close = new FakeElement(); const badge = new FakeElement(); -const rows = Object.fromEntries(['today','attention','update','later','draft'].map(name => [name, new FakeElement()])); +const rows = Object.fromEntries(['today','attention','update','later','draft','recaps'].map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const selected = []; +const utilities = []; const dock = createDock({{ nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet], queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, - onSelectQueue:name=>selected.push(name), + onSelectQueue:(name,row)=>name === 'recaps' ? utilities.push(row === rows.recaps ? 'recaps:trigger' : 'recaps:missing-trigger') : selected.push(name), observe() {{}}, }}); dock.start(); @@ -308,9 +309,11 @@ queues.click(); const opened = sheet.open; rows.update.click(); const closedAfterSelect = !sheet.open; +queues.click(); rows.recaps.click(); +const closedAfterUtility = !sheet.open; queues.click(); close.click(); process.stdout.write(JSON.stringify({{ - opened, closedAfterSelect, selected, + opened, closedAfterSelect, closedAfterUtility, selected, utilities, badge:badge.textContent, badgeLabel:queues.attributes['aria-label'], counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])), updateLabel:rows.update.attributes['aria-label'], @@ -324,13 +327,15 @@ process.stdout.write(JSON.stringify({{ assert json.loads(result.stdout) == { "opened": True, "closedAfterSelect": True, + "closedAfterUtility": True, "selected": ["update"], + "utilities": ["recaps:trigger"], # Updates are a drill-down within Attention and must not inflate the aggregate badge. "badge": "10", "badgeLabel": "Queues, 10 items", - "counts": {"today": "2", "attention": "1", "update": "5", "later": "3", "draft": "4"}, + "counts": {"today": "2", "attention": "1", "update": "5", "later": "3", "draft": "4", "recaps": "0"}, "updateLabel": "Updates, 5 unread conversations", - "queueFocuses": 2, + "queueFocuses": 3, "columnState": None, } @@ -376,13 +381,16 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher(): assert 'UpdatesUnread conversations' in html assert 'data-mobile-queue="later"' in html assert 'data-mobile-queue="draft"' in html + assert 'data-mobile-queue="recaps"' in html + assert 'RecapsHistory' in html assert 'id="mobile-queue-count"' in html assert 'mobileTaskDock.updateQueues(counts)' in html assert '' in html assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html - assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html + assert "onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html assert '.mobile-queue-sheet' in html + assert '.my-work-actions { display:none;' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html assert '.mobile-task-dock[data-attention="true"]' not in html @@ -426,7 +434,7 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock(): assert '' in html assert "createMobileTaskDock({" in html assert "work: () => mobileWorkEntry.open()" in html - assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html + assert "onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html assert "find: () => qs('#find-work').click()" in html assert "new: () => qs('#new-issue').click()" in html -- 2.43.0