Merge pull request 'Continue mobile work across queue boundaries' (#684) from timmy/683-mobile-queue-continuation into main
This commit is contained in:
commit
16b1d8670f
|
|
@ -576,6 +576,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.pull-retry { min-height:44px; width:100%; margin-top:10px; }
|
.pull-retry { min-height:44px; width:100%; margin-top:10px; }
|
||||||
.mobile-task-dock { display:none; }
|
.mobile-task-dock { display:none; }
|
||||||
.mobile-task-dock[hidden] { display:none; }
|
.mobile-task-dock[hidden] { display:none; }
|
||||||
|
|
||||||
.mobile-today-hud { display:none; }
|
.mobile-today-hud { display:none; }
|
||||||
.mobile-today-hud[hidden], .mobile-today-hud[data-overlay-hidden="true"] { display:none; }
|
.mobile-today-hud[hidden], .mobile-today-hud[data-overlay-hidden="true"] { display:none; }
|
||||||
.mobile-task-action { min-width:0; min-height:44px; padding:6px 2px; border:0; border-radius:8px; background:transparent; display:grid; place-items:center; gap:2px; font-size:12px; }
|
.mobile-task-action { min-width:0; min-height:44px; padding:6px 2px; border:0; border-radius:8px; background:transparent; display:grid; place-items:center; gap:2px; font-size:12px; }
|
||||||
|
|
@ -649,6 +650,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
||||||
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
||||||
.mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
.mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||||
|
|
||||||
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
|
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
|
||||||
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
||||||
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||||
|
|
@ -658,6 +660,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.mobile-queue-list button > span:first-child { display:grid; gap:2px; }
|
.mobile-queue-list button > span:first-child { display:grid; gap:2px; }
|
||||||
.mobile-queue-list small { color:var(--muted); }
|
.mobile-queue-list small { color:var(--muted); }
|
||||||
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
||||||
|
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
||||||
.mobile-today-hud { position:fixed; left:8px; right:8px; bottom:calc(56px + env(safe-area-inset-bottom)); z-index:44; display:grid; grid-template-columns:minmax(0,1fr) minmax(112px,auto); grid-template-areas:"summary complete" "progress toggle"; gap:4px 8px; max-width:100%; padding:8px; border:1px solid #31577f; border-radius:12px 12px 0 0; background:rgba(16,38,65,.98); box-shadow:0 -8px 24px rgba(0,0,0,.28); }
|
.mobile-today-hud { position:fixed; left:8px; right:8px; bottom:calc(56px + env(safe-area-inset-bottom)); z-index:44; display:grid; grid-template-columns:minmax(0,1fr) minmax(112px,auto); grid-template-areas:"summary complete" "progress toggle"; gap:4px 8px; max-width:100%; padding:8px; border:1px solid #31577f; border-radius:12px 12px 0 0; background:rgba(16,38,65,.98); box-shadow:0 -8px 24px rgba(0,0,0,.28); }
|
||||||
.mobile-today-summary { grid-area:summary; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; font-weight:700; }
|
.mobile-today-summary { grid-area:summary; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; font-weight:700; }
|
||||||
.mobile-today-hud [data-work-session-progress] { grid-area:progress; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.mobile-today-hud [data-work-session-progress] { grid-area:progress; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,24 @@
|
||||||
planToday: () => openPlanToday(mobileTaskButtons.work),
|
planToday: () => openPlanToday(mobileTaskButtons.work),
|
||||||
openFallback: openMobileWorkFallback,
|
openFallback: openMobileWorkFallback,
|
||||||
});
|
});
|
||||||
|
let mobileQueueCounts = {};
|
||||||
const mobileQueueLauncher = createMobileQueueLauncher({
|
const mobileQueueLauncher = createMobileQueueLauncher({
|
||||||
openToday: () => mobileWorkEntry.open(),
|
openToday: () => mobileWorkEntry.open(),
|
||||||
selectFilter: selectMobileQueue,
|
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'),
|
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; },
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
|
getCounts: () => mobileQueueCounts,
|
||||||
|
openFindWork: () => qs('#find-work').click(),
|
||||||
});
|
});
|
||||||
|
function showMobileQueueCompletion(completedName) {
|
||||||
|
const next = mobileQueueLauncher.recommend();
|
||||||
|
qs('#mobile-queue-heading').textContent = completedName + ' cleared';
|
||||||
|
document.querySelectorAll('[data-mobile-queue]').forEach(row => row.removeAttribute('data-recommended'));
|
||||||
|
const row = qs('[data-mobile-queue="' + next.name + '"]');
|
||||||
|
row.setAttribute('data-recommended', 'true');
|
||||||
|
qs('#mobile-queue-sheet').showModal();
|
||||||
|
row.focus();
|
||||||
|
}
|
||||||
const mobileTaskDock = createMobileTaskDock({
|
const mobileTaskDock = createMobileTaskDock({
|
||||||
nav: qs('#mobile-task-dock'),
|
nav: qs('#mobile-task-dock'),
|
||||||
sessionHud: qs('[data-mobile-today-hud]'),
|
sessionHud: qs('[data-mobile-today-hud]'),
|
||||||
|
|
@ -93,7 +105,8 @@
|
||||||
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
|
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
|
||||||
),
|
),
|
||||||
queueBadge: qs('#mobile-queue-count'),
|
queueBadge: qs('#mobile-queue-count'),
|
||||||
onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name),
|
onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() :
|
||||||
|
name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name),
|
||||||
overlays: mobileTaskOverlays,
|
overlays: mobileTaskOverlays,
|
||||||
actions: {
|
actions: {
|
||||||
work: () => mobileWorkEntry.open(),
|
work: () => mobileWorkEntry.open(),
|
||||||
|
|
@ -882,7 +895,10 @@
|
||||||
onStatus: message => {
|
onStatus: message => {
|
||||||
qs('#update-sheet-status').textContent = message;
|
qs('#update-sheet-status').textContent = message;
|
||||||
qs('#retry-update-load').hidden = !message.startsWith('Could not load update.');
|
qs('#retry-update-load').hidden = !message.startsWith('Could not load update.');
|
||||||
if (message === 'Inbox cleared.') qs('#my-work-action-status').textContent = message;
|
if (message === 'Inbox cleared.') {
|
||||||
|
qs('#my-work-action-status').textContent = message;
|
||||||
|
showMobileQueueCompletion('Updates');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClose: () => closeUpdateSheet(false),
|
onClose: () => closeUpdateSheet(false),
|
||||||
});
|
});
|
||||||
|
|
@ -2158,6 +2174,7 @@
|
||||||
const element = qs('[data-work-count="' + filter + '"]');
|
const element = qs('[data-work-count="' + filter + '"]');
|
||||||
if (element) element.textContent = count;
|
if (element) element.textContent = count;
|
||||||
});
|
});
|
||||||
|
mobileQueueCounts = counts;
|
||||||
mobileTaskDock.updateQueues(counts);
|
mobileTaskDock.updateQueues(counts);
|
||||||
mobileTaskDock.updateWork(mobileWorkEntry.mode());
|
mobileTaskDock.updateWork(mobileWorkEntry.mode());
|
||||||
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
|
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
|
||||||
|
|
|
||||||
|
|
@ -925,6 +925,7 @@
|
||||||
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
||||||
<button data-mobile-queue="later" type="button"><span><strong>Later</strong><small>Deferred work</small></span><span data-mobile-queue-count="later">0</span></button>
|
<button data-mobile-queue="later" type="button"><span><strong>Later</strong><small>Deferred work</small></span><span data-mobile-queue-count="later">0</span></button>
|
||||||
<button data-mobile-queue="draft" type="button"><span><strong>Drafts</strong><small>Unfiled captures</small></span><span data-mobile-queue-count="draft">0</span></button>
|
<button data-mobile-queue="draft" type="button"><span><strong>Drafts</strong><small>Unfiled captures</small></span><span data-mobile-queue-count="draft">0</span></button>
|
||||||
|
<button data-mobile-queue="find" type="button"><span><strong>Find Work</strong><small>Claim something new</small></span></button>
|
||||||
<button data-mobile-queue="recaps" type="button"><span><strong>Recaps</strong><small>History</small></span></button>
|
<button data-mobile-queue="recaps" type="button"><span><strong>Recaps</strong><small>History</small></span></button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,21 @@
|
||||||
later: 'No deferred work is ready to open.',
|
later: 'No deferred work is ready to open.',
|
||||||
draft: 'No drafts are ready to open.',
|
draft: 'No drafts are ready to open.',
|
||||||
};
|
};
|
||||||
|
const continuation = [
|
||||||
|
['attention', 'Start Attention'],
|
||||||
|
['today', 'Continue Today'],
|
||||||
|
['later', 'Start Later'],
|
||||||
|
['draft', 'Open Drafts'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function recommend() {
|
||||||
|
const counts = options.getCounts ? options.getCounts() : {};
|
||||||
|
const match = continuation.find(([name]) => Number(counts[name]) > 0);
|
||||||
|
if (!match) return {name: 'find', count: 0, label: 'Find Work'};
|
||||||
|
const [name, label] = match;
|
||||||
|
const count = Math.max(0, Number(counts[name]) || 0);
|
||||||
|
return {name, count, label: label + ' (' + count + ')'};
|
||||||
|
}
|
||||||
|
|
||||||
function open(name) {
|
function open(name) {
|
||||||
if (name === 'today') return options.openToday();
|
if (name === 'today') return options.openToday();
|
||||||
|
|
@ -21,5 +36,14 @@
|
||||||
return 'opened';
|
return 'opened';
|
||||||
}
|
}
|
||||||
|
|
||||||
return { open };
|
function continueWork() {
|
||||||
|
const next = recommend();
|
||||||
|
if (next.name === 'find') {
|
||||||
|
options.openFindWork();
|
||||||
|
return 'find';
|
||||||
|
}
|
||||||
|
return open(next.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { open, recommend, continueWork };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ FEATURE_SOURCES = {
|
||||||
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
||||||
"security-center": ("static/security-center.js",),
|
"security-center": ("static/security-center.js",),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/mobile-task-dock.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
"static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||||
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
||||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||||
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,50 @@ process.stdout.write(JSON.stringify({{opened, empty, calls}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
const calls = [];
|
||||||
|
let counts = {{attention: 2, today: 1, later: 4, draft: 3}};
|
||||||
|
const launcher = createLauncher({{
|
||||||
|
openToday: () => calls.push('today'),
|
||||||
|
selectFilter: name => calls.push('filter:' + name),
|
||||||
|
firstAction: name => (counts[name] || 0) ? {{click() {{ calls.push('open:' + name); }}}} : null,
|
||||||
|
announce: message => calls.push('announce:' + message),
|
||||||
|
getCounts: () => counts,
|
||||||
|
openFindWork: () => calls.push('find'),
|
||||||
|
}});
|
||||||
|
const first = launcher.recommend();
|
||||||
|
counts = {{attention: 0, today: 0, later: 4, draft: 3}};
|
||||||
|
const opened = launcher.continueWork();
|
||||||
|
counts = {{attention: 0, today: 0, later: 0, draft: 0}};
|
||||||
|
const fallback = launcher.recommend();
|
||||||
|
launcher.continueWork();
|
||||||
|
process.stdout.write(JSON.stringify({{first, opened, fallback, calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"first": {"name": "attention", "count": 2, "label": "Start Attention (2)"},
|
||||||
|
"opened": "opened",
|
||||||
|
"fallback": {"name": "find", "count": 0, "label": "Find Work"},
|
||||||
|
"calls": ["filter:later", "open:later", "find"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_dashboard_renders_accessible_mobile_queue_completion_handoff():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'id="mobile-queue-heading"' in html
|
||||||
|
assert 'data-mobile-queue="find"' in html
|
||||||
|
assert "mobileQueueLauncher.recommend()" in html
|
||||||
|
assert "showMobileQueueCompletion('Updates')" in html
|
||||||
|
assert "row.setAttribute('data-recommended', 'true')" in html
|
||||||
|
assert "row.focus()" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -388,7 +432,7 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
||||||
assert '<script src="static/mobile-queue-launcher.js"></script>' in html
|
assert '<script src="static/mobile-queue-launcher.js"></script>' in html
|
||||||
assert "const mobileQueueLauncher = createMobileQueueLauncher({" 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 "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 => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html
|
assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
|
||||||
assert '.mobile-queue-sheet' in html
|
assert '.mobile-queue-sheet' in html
|
||||||
assert '.my-work-actions { display:none;' in html
|
assert '.my-work-actions { display:none;' in html
|
||||||
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
|
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
|
||||||
|
|
@ -434,7 +478,7 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
||||||
assert '<script src="static/mobile-task-dock.js"></script>' in html
|
assert '<script src="static/mobile-task-dock.js"></script>' in html
|
||||||
assert "createMobileTaskDock({" in html
|
assert "createMobileTaskDock({" in html
|
||||||
assert "work: () => mobileWorkEntry.open()" in html
|
assert "work: () => mobileWorkEntry.open()" in html
|
||||||
assert "onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html
|
assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
|
||||||
assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
|
assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
|
||||||
assert "find: () => qs('#find-work').click()" in html
|
assert "find: () => qs('#find-work').click()" in html
|
||||||
assert "new: () => qs('#new-issue').click()" in html
|
assert "new: () => qs('#new-issue').click()" in html
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user