diff --git a/frontend/dashboard.css b/frontend/dashboard.css index a5655fd..e1ede67 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -77,6 +77,14 @@ main { position: relative; z-index: 1; display: grid; grid-template-columns: 300 h2 { font-size: 13px; margin: 8px 0; text-transform: uppercase; letter-spacing: .08em; color: #94a3b8; } a { color: #60a5fa; text-decoration: none; } .muted { color: #94a3b8; } +.empty-work-start { display:grid; gap:12px; margin:12px 0; padding:16px; border:1px solid #31577f; border-radius:14px; background:#0b1b2f; } +.empty-work-start[hidden] { display:none; } +.empty-work-start p { margin:6px 0 0; } +.empty-work-start-actions { display:grid; grid-template-columns:minmax(0, 1fr) minmax(0, 1fr); gap:8px; } +.empty-work-start-actions button { min-width:0; min-height:44px; white-space:normal; } +@media (max-width:420px) { + .empty-work-start-actions { grid-template-columns:1fr; } +} .kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; } .kv .label { color: #94a3b8; } .kv .value { color: #e5e7eb; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index f4502f0..edc5fe5 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -49,11 +49,7 @@ attentionInterruption.hidden = !pending; return pending; } - function openMobileWorkFallback() { - qs('[data-work-filter="all"]').click(); - qs('#my-work').scrollIntoView({block:'start'}); - qs('#my-work').focus(); - } + function selectMobileQueue(name) { if (name === 'attention' && workSession.checkpointed()) timer.beginAttention(); renderAttentionInterruption(); @@ -71,7 +67,7 @@ resumeToday: resumeTodaySession, startToday: startTodaySession, planToday: () => openPlanToday(mobileTaskButtons.work), - openFallback: openMobileWorkFallback, + findWork: () => qs('#find-work').click(), }); let mobileQueueCounts = {}; const mobileQueueLauncher = createMobileQueueLauncher({ @@ -122,6 +118,8 @@ }, }); mobileTaskDock.start(); + qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click()); + qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click()); let liveMode = true; let offlineWorkMode = false; const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1'; @@ -2433,6 +2431,10 @@ filterMyWork(activeMyWork, selectedWorkFilter, selectedWorkMilestone); const incomplete = activeWorkStreams().some(stream => workPagination[stream]?.has_more); const visible = findQueueItems(queueItems, queueFindQuery); + const emptyWorkStart = qs('#empty-work-start'); + const showEmptyStart = selectedWorkFilter === 'all' && !queueFindQuery && + !queueItems.length && !incomplete && hasContextSnapshot; + emptyWorkStart.hidden = !showEmptyStart; updateQueueFinder(visible.length, queueItems.length, incomplete); const selection = notificationSelection.snapshot(); const selectedIds = new Set(selection.ids); @@ -2486,9 +2488,9 @@ return '
' + selector + '' + contents + '' + readUpdate + markRead + planningActions + '
'; } return '
' + selector + '' + contents + '' + markRead + planningActions + '
'; - }).join('') : '
' + (incomplete ? + }).join('') : (showEmptyStart ? '' : '
' + (incomplete ? 'More work is available. Load the next page.' : - 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '
'; + 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '
'); cardPlanning.wire(); document.querySelectorAll('[data-select-notification-id]').forEach(input => { input.addEventListener('change', () => { diff --git a/frontend/index.html b/frontend/index.html index 3ad3071..9a0ff2d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -183,6 +183,16 @@
+
diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js index 3d43982..506a8c5 100644 --- a/frontend/mobile-task-dock.js +++ b/frontend/mobile-task-dock.js @@ -69,6 +69,7 @@ resume: ['Resume', 'Resume Today'], start: ['Start', 'Start Today'], plan: ['Plan', 'Plan Today'], + find: ['Find', 'Find work'], work: ['Work', 'Work'], }; const workMode = labels[mode] ? mode : 'work'; diff --git a/frontend/mobile-work-entry.js b/frontend/mobile-work-entry.js index af4aa47..56a1fa8 100644 --- a/frontend/mobile-work-entry.js +++ b/frontend/mobile-work-entry.js @@ -7,7 +7,7 @@ if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume'; if (options.getTodayCount() > 0) return 'start'; if (options.getEligibleCount() > 0) return 'plan'; - return 'work'; + return 'find'; } function open() { @@ -16,7 +16,7 @@ else if (current === 'resume') options.resumeToday(); else if (current === 'start') options.startToday(); else if (current === 'plan') options.planToday(); - else options.openFallback(); + else options.findWork(); return current; } diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index 1d09bcf..2d7574c 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -13,7 +13,7 @@ QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queu TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js" -def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_fallback(): +def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_find(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const state = {{active:true, resumable:true, today:2, eligible:3}}; @@ -27,7 +27,7 @@ const entry = createEntry({{ resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), planToday: () => calls.push('plan'), - openFallback: () => calls.push('fallback'), + findWork: () => calls.push('find'), }}); const modes = []; modes.push(entry.open()); @@ -43,8 +43,8 @@ process.stdout.write(JSON.stringify({{modes, calls}})); assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { - "modes": ["continue", "resume", "start", "plan", "work"], - "calls": ["continue", "resume", "start", "plan", "fallback"], + "modes": ["continue", "resume", "start", "plan", "find"], + "calls": ["continue", "resume", "start", "plan", "find"], } @@ -165,10 +165,10 @@ dock.updateWork('start', 0); const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('plan', 0); const planning = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; -dock.updateWork('work', 0); +dock.updateWork('find', 0); process.stdout.write(JSON.stringify({{ continuing, resuming, starting, planning, - fallback:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }}, + finding:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }}, }})); """ result = subprocess.run( @@ -184,7 +184,7 @@ process.stdout.write(JSON.stringify({{ "resuming": {"text": "Resume", "label": "Resume Today"}, "starting": {"text": "Start", "label": "Start Today"}, "planning": {"text": "Plan", "label": "Plan Today"}, - "fallback": {"text": "Work", "label": "Work"}, + "finding": {"text": "Find", "label": "Find work"}, } diff --git a/tests/test_my_work.py b/tests/test_my_work.py index 3c52a4e..09f7a09 100644 --- a/tests/test_my_work.py +++ b/tests/test_my_work.py @@ -8,6 +8,22 @@ import pytest from tests.dashboard_bundle import dashboard +@pytest.mark.anyio +async def test_empty_all_work_queue_offers_find_and_create_actions(): + markup = (Path(__file__).resolve().parents[1] / "frontend" / "index.html").read_text() + source = await dashboard() + + assert 'id="empty-work-start"' in markup + assert 'Ready for something new?' in markup + assert 'id="empty-work-find"' in markup + assert 'id="empty-work-create"' in markup + assert "qs('#empty-work-find').addEventListener('click'" in source + assert "qs('#find-work').click()" in source + assert "qs('#empty-work-create').addEventListener('click'" in source + assert "qs('#new-issue').click()" in source + assert "emptyWorkStart.hidden = !showEmptyStart" in source + + MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" NOTIFICATION_UNDO = Path(__file__).parents[1] / "frontend" / "notification-undo.js" TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js"