diff --git a/frontend/index.html b/frontend/index.html index d16c9c6..16b0332 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1531,6 +1531,7 @@
+ diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js index 1a5ef30..5aa1d50 100644 --- a/frontend/mobile-queue-launcher.js +++ b/frontend/mobile-queue-launcher.js @@ -9,6 +9,7 @@ draft: 'No drafts are ready to open.', }; const continuation = [ + ['delivery', 'Recover Delivery'], ['attention', 'Start Attention'], ['today', 'Continue Today'], ['update', 'Resume Updates'], diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js index 49be4ae..a76fdda 100644 --- a/frontend/mobile-task-dock.js +++ b/frontend/mobile-task-dock.js @@ -66,9 +66,9 @@ function updateWork(mode) { const text = { continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find', - attention:'Attention', update:'Updates', agenda:'Agenda', filed:'Filed', later:'Later', draft:'Drafts', + delivery:'Delivery', attention:'Attention', update:'Updates', agenda:'Agenda', filed:'Filed', later:'Later', draft:'Drafts', }[mode] || 'Work'; - const queue = ['Attention', 'Updates', 'Agenda', 'Filed', 'Later', 'Drafts'].includes(text); + const queue = ['Delivery', 'Attention', 'Updates', 'Agenda', 'Filed', 'Later', 'Drafts'].includes(text); if (options.workLabel) options.workLabel.textContent = text; if (buttons.work) buttons.work.setAttribute('aria-label', queue ? (mode === 'update' ? 'Resume ' : 'Open ') + text @@ -91,9 +91,9 @@ } function updateQueues(counts) { - const names = 'today agenda attention update filed later draft'.split(' '); + const names = 'today agenda delivery attention update filed later draft'.split(' '); const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)])); - const actionableNames = ['today', 'attention', 'update', 'filed', 'later', 'draft']; + const actionableNames = ['today', 'delivery', 'attention', 'update', 'filed', 'later', 'draft']; const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0); Object.entries(options.queueCounts || {}).forEach(([name, element]) => { element.textContent = String(normalized[name] || 0); @@ -117,6 +117,7 @@ ? 'Queues: no active queues; no upcoming deadlines' : 'Queues: Today ' + normalized.today + ', Agenda ' + normalized.agenda + ' due' + + ', Delivery ' + normalized.delivery + ', Attention ' + normalized.attention + ', Updates ' + normalized.update + ', Filed ' + normalized.filed diff --git a/tests/e2e/test_mobile_home_bootstrap_release.py b/tests/e2e/test_mobile_home_bootstrap_release.py index 6af1484..ab94839 100644 --- a/tests/e2e/test_mobile_home_bootstrap_release.py +++ b/tests/e2e/test_mobile_home_bootstrap_release.py @@ -101,6 +101,15 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights( bounds = control.bounding_box() assert bounds and bounds["height"] >= 44 + page.locator('[data-mobile-task="queues"]').click() + delivery_queue = page.locator('[data-mobile-queue="delivery"]') + expect(delivery_queue).to_be_visible() + expect(delivery_queue).to_contain_text("Delivery") + delivery_bounds = delivery_queue.bounding_box() + assert delivery_bounds and delivery_bounds["height"] >= 44 + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + page.locator("#close-mobile-queues").click() + page.locator("#app-menu-toggle").click() page.locator("#open-insights").click() expect(page.locator("#insights-sheet")).to_be_visible() diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index fceff82..023ca97 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -355,7 +355,7 @@ sheet.close = function () {{ this.open = false; this.listeners.close?.(); }}; const close = new FakeElement(); const badge = new FakeElement(); const deadline = new FakeElement(); -const rows = Object.fromEntries(['today','agenda','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()])); +const rows = Object.fromEntries(['today','agenda','delivery','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const selected = []; const utilities = []; @@ -366,7 +366,7 @@ const dock = createDock({{ observe() {{}}, }}); dock.start(); -dock.updateQueues({{today:2, agenda:5, attention:1, update:5, filed:2, later:3, draft:4}}); +dock.updateQueues({{today:2, agenda:5, delivery:1, attention:1, update:5, filed:2, later:3, draft:4}}); queues.click(); const opened = sheet.open; rows.update.click(); @@ -411,9 +411,9 @@ process.stdout.write(JSON.stringify({{ "utilities": ["recaps:trigger"], "badge": "0 active", "populated": { - "badge": "6 active", + "badge": "7 active", "badgeHidden": False, - "badgeLabel": "Queues: Today 2, Agenda 5 due, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 6 active queues", + "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 7 active queues", "deadline": "5 due", "deadlineHidden": False, "agendaDue": "true", @@ -422,13 +422,13 @@ process.stdout.write(JSON.stringify({{ "badge": "1 active", "badgeHidden": False, "deadlineHidden": True, - "badgeLabel": "Queues: Today 0, Agenda 0 due, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue", + "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue", }, "clearedBadgeHidden": True, "clearedDeadlineHidden": True, "clearedAgendaDue": None, "clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines", - "counts": {"today": "0", "agenda": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"}, + "counts": {"today": "0", "agenda": "0", "delivery": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"}, "updateLabel": "Updates, 0 unread conversations", "queueFocuses": 3, "columnState": None, @@ -503,6 +503,37 @@ process.stdout.write(JSON.stringify({{result, calls}})); assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]} +def test_mobile_work_prioritizes_and_revalidates_actionable_delivery_recovery(): + script = f""" +const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); +const calls = []; +let counts = {{delivery:2, attention:3, today:1, update:4}}; +const launcher = createLauncher({{ + getCounts: () => counts, + openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened-delivery'; }}, + openUpdates: () => {{ calls.push('updates'); return 'opened-updates'; }}, + selectFilter: name => calls.push('filter:' + name), + firstAction: () => null, + announce: () => {{}}, + openFindWork: () => calls.push('find'), +}}); +const delivery = launcher.recommend(); +const opened = launcher.continueWork(); +counts = {{delivery:0, attention:0, today:0, update:4}}; +const afterRecovery = launcher.recommend(); +process.stdout.write(JSON.stringify({{delivery, opened, afterRecovery, calls}})); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "delivery": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"}, + "opened": "opened-delivery", + "afterRecovery": {"name": "update", "count": 4, "label": "Resume Updates (4)"}, + "calls": ["delivery-recovery"], + } + + def test_mobile_work_continues_into_filed_follow_up_before_later_work(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); @@ -600,7 +631,7 @@ const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[na const workLabel = {{textContent:''}}; const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}}); const labels = {{}}; -for (const mode of ['attention','update','agenda','filed','later','draft']) {{ +for (const mode of ['delivery','attention','update','agenda','filed','later','draft']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} @@ -610,6 +641,7 @@ process.stdout.write(JSON.stringify(labels)); assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { + "delivery": ["Delivery", "Open Delivery"], "attention": ["Attention", "Open Attention"], "update": ["Updates", "Resume Updates"], "agenda": ["Agenda", "Open Agenda"], @@ -646,6 +678,9 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher(): assert "counts.agenda = agendaItems.length;" in html assert 'data-agenda-group' in html assert 'data-mobile-queue="attention"' in html + assert 'data-mobile-queue="delivery"' in html + assert 'DeliveryNeeds recovery' in html + assert 'data-mobile-queue-count="delivery"' in html assert 'data-mobile-queue="update"' in html assert 'UpdatesUnread conversations' in html assert 'data-mobile-queue="filed"' in html