diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index f6d9fd4..7ef8d6f 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -56,7 +56,7 @@
qs('#my-work').scrollIntoView({block:'start'});
qs('#my-work').focus();
}
- function openMobileQueue(name) {
+ function selectMobileQueue(name) {
if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();
renderAttentionInterruption();
timerView.render();
@@ -75,6 +75,12 @@
planToday: () => openPlanToday(mobileTaskButtons.work),
openFallback: openMobileWorkFallback,
});
+ const mobileQueueLauncher = createMobileQueueLauncher({
+ openToday: () => mobileWorkEntry.open(),
+ selectFilter: selectMobileQueue,
+ 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'),
+ announce: message => { qs('#my-work-action-status').textContent = message; },
+ });
const mobileTaskDock = createMobileTaskDock({
nav: qs('#mobile-task-dock'),
sessionHud: qs('[data-mobile-today-hud]'),
@@ -89,7 +95,7 @@
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
),
queueBadge: qs('#mobile-queue-count'),
- onSelectQueue: openMobileQueue,
+ onSelectQueue: name => mobileQueueLauncher.open(name),
overlays: mobileTaskOverlays,
actions: {
work: () => mobileWorkEntry.open(),
diff --git a/frontend/index.html b/frontend/index.html
index 5da019f..ef3aa98 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -934,6 +934,7 @@
+
diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js
new file mode 100644
index 0000000..8cf7faf
--- /dev/null
+++ b/frontend/mobile-queue-launcher.js
@@ -0,0 +1,25 @@
+(function (root, factory) {
+ if (typeof module === 'object' && module.exports) module.exports = factory;
+ else root.createMobileQueueLauncher = factory;
+})(typeof self !== 'undefined' ? self : this, function createMobileQueueLauncher(options) {
+ const emptyMessages = {
+ attention: 'No items need attention.',
+ update: 'No unread updates are ready to open.',
+ later: 'No deferred work is ready to open.',
+ draft: 'No drafts are ready to open.',
+ };
+
+ function open(name) {
+ if (name === 'today') return options.openToday();
+ options.selectFilter(name);
+ const action = options.firstAction(name);
+ if (!action) {
+ options.announce(emptyMessages[name] || 'No work is ready to open.');
+ return 'empty';
+ }
+ action.click();
+ return 'opened';
+ }
+
+ return { open };
+});
diff --git a/frontend/service-worker.js b/frontend/service-worker.js
index 2fa4270..9edcab1 100644
--- a/frontend/service-worker.js
+++ b/frontend/service-worker.js
@@ -67,6 +67,7 @@ const SHELL = [
BASE + 'static/context-poller.js',
BASE + 'static/mobile-task-dock.js',
BASE + 'static/mobile-work-entry.js',
+ BASE + 'static/mobile-queue-launcher.js',
BASE + 'static/mobile-launch.js',
BASE + 'static/install-app.js',
BASE + 'static/mobile-device-setup.js',
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index 0e88dfd..895a683 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
+QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js"
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
@@ -334,6 +335,34 @@ process.stdout.write(JSON.stringify({{
}
+def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue():
+ script = f"""
+const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
+const calls = [];
+const launcher = createLauncher({{
+ selectFilter: name => calls.push('filter:' + name),
+ firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null,
+ announce: message => calls.push('announce:' + message),
+}});
+const opened = launcher.open('attention');
+const empty = launcher.open('later');
+process.stdout.write(JSON.stringify({{opened, empty, calls}}));
+"""
+ result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout) == {
+ "opened": "opened",
+ "empty": "empty",
+ "calls": [
+ "filter:attention",
+ "open:first",
+ "filter:later",
+ "announce:No deferred work is ready to open.",
+ ],
+ }
+
+
@pytest.mark.anyio
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
html = await dashboard()
@@ -349,7 +378,10 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
assert 'data-mobile-queue="draft"' in html
assert 'id="mobile-queue-count"' in html
assert 'mobileTaskDock.updateQueues(counts)' in html
- assert "onSelectQueue: openMobileQueue" 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 '.mobile-queue-sheet' in html
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
assert '.mobile-task-dock[data-attention="true"]' not in html
@@ -394,7 +426,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: openMobileQueue" in html
+ assert "onSelectQueue: name => 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
diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py
index 762a37e..1204daa 100644
--- a/tests/test_service_worker.py
+++ b/tests/test_service_worker.py
@@ -730,6 +730,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/context-poller.js",
"/dashboard/static/mobile-task-dock.js",
"/dashboard/static/mobile-work-entry.js",
+ "/dashboard/static/mobile-queue-launcher.js",
"/dashboard/static/mobile-launch.js",
"/dashboard/static/install-app.js",
"/dashboard/static/mobile-device-setup.js",