feat: consolidate mobile work commands (Closes #651)
This commit is contained in:
parent
e35d7dea35
commit
c4ba78c8cb
|
|
@ -560,8 +560,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.my-work { margin:0; }
|
.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)); }
|
.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-header { align-items:flex-start; }
|
||||||
.my-work-actions { width:100%; flex-wrap:nowrap; }
|
.my-work-actions { display:none; }
|
||||||
.my-work-actions button { min-height:44px; flex:1 1 0; padding-inline:6px; }
|
|
||||||
.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 > 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:not([open]) > .work-settings-panel { display:none; }
|
||||||
.work-settings-panel { display:grid; gap:10px; margin-top:8px; }
|
.work-settings-panel { display:grid; gap:10px; margin-top:8px; }
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state));
|
localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state));
|
||||||
} catch (e) {
|
} 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])
|
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 => mobileQueueLauncher.open(name),
|
onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name),
|
||||||
overlays: mobileTaskOverlays,
|
overlays: mobileTaskOverlays,
|
||||||
actions: {
|
actions: {
|
||||||
work: () => mobileWorkEntry.open(),
|
work: () => mobileWorkEntry.open(),
|
||||||
|
|
@ -129,7 +129,7 @@
|
||||||
const savedMilestone = sessionStorage.getItem(WORK_MILESTONE_KEY);
|
const savedMilestone = sessionStorage.getItem(WORK_MILESTONE_KEY);
|
||||||
if (savedMilestone) selectedWorkMilestone = savedMilestone;
|
if (savedMilestone) selectedWorkMilestone = savedMilestone;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not restore My Work filter', e);
|
console.warn('Filter restore failed', e);
|
||||||
}
|
}
|
||||||
let lastMyWork = [];
|
let lastMyWork = [];
|
||||||
let lastDrafts = [];
|
let lastDrafts = [];
|
||||||
|
|
@ -3694,7 +3694,6 @@
|
||||||
} else {
|
} else {
|
||||||
setEventStreamStatus('Update failed · showing last activity');
|
setEventStreamStatus('Update failed · showing last activity');
|
||||||
}
|
}
|
||||||
// Compatibility fallback for snapshots produced before section metadata.
|
|
||||||
if (!eventsFreshness && snapshot.freshness?.degraded && !snapshot.freshness.revalidating) {
|
if (!eventsFreshness && snapshot.freshness?.degraded && !snapshot.freshness.revalidating) {
|
||||||
const retrySeconds = Number(snapshot.freshness.retry_in_seconds) || 0;
|
const retrySeconds = Number(snapshot.freshness.retry_in_seconds) || 0;
|
||||||
setEventStreamStatus('Refresh failed · showing last known data' +
|
setEventStreamStatus('Refresh failed · showing last known data' +
|
||||||
|
|
@ -5555,7 +5554,7 @@
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter);
|
sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not persist My Work filter', e);
|
console.warn('Filter save failed', e);
|
||||||
}
|
}
|
||||||
document.querySelectorAll('[data-work-filter]').forEach(item =>
|
document.querySelectorAll('[data-work-filter]').forEach(item =>
|
||||||
item.setAttribute('aria-pressed', String(item === button))
|
item.setAttribute('aria-pressed', String(item === button))
|
||||||
|
|
@ -5587,7 +5586,7 @@
|
||||||
qs('#work-milestone-filter').addEventListener('change', event => {
|
qs('#work-milestone-filter').addEventListener('change', event => {
|
||||||
selectedWorkMilestone = event.target.value;
|
selectedWorkMilestone = event.target.value;
|
||||||
try { sessionStorage.setItem(WORK_MILESTONE_KEY, selectedWorkMilestone); }
|
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();
|
renderMyWork();
|
||||||
if (workSession.active()) workSession.reconcile();
|
if (workSession.active()) workSession.reconcile();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -869,6 +869,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="recaps" type="button"><span><strong>Recaps</strong><small>History</small></span></button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeQueues() {
|
function closeQueues() {
|
||||||
if (options.queueSheet?.open) options.queueSheet.close();
|
options.queueSheet?.open && options.queueSheet.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
|
|
@ -42,8 +42,7 @@
|
||||||
button.addEventListener('click', event => {
|
button.addEventListener('click', event => {
|
||||||
launcher = event.currentTarget;
|
launcher = event.currentTarget;
|
||||||
select(name);
|
select(name);
|
||||||
if (name === 'queues' && options.queueSheet) openQueues();
|
name === 'queues' && options.queueSheet ? openQueues() : options.actions[name]();
|
||||||
else options.actions[name]();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (options.queueSheet) {
|
if (options.queueSheet) {
|
||||||
|
|
@ -56,7 +55,7 @@
|
||||||
Object.entries(options.queueRows || {}).forEach(([name, row]) => {
|
Object.entries(options.queueRows || {}).forEach(([name, row]) => {
|
||||||
row.addEventListener('click', () => {
|
row.addEventListener('click', () => {
|
||||||
closeQueues();
|
closeQueues();
|
||||||
options.onSelectQueue?.(name);
|
options.onSelectQueue?.(name, row);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +94,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateQueues(counts) {
|
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)]));
|
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);
|
const total = ['today', 'attention', 'later', 'draft'].reduce((sum, name) => sum + normalized[name], 0);
|
||||||
Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
|
Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
|
||||||
element.textContent = String(normalized[name] || 0);
|
element.textContent = String(normalized[name] || 0);
|
||||||
|
|
@ -110,7 +107,7 @@
|
||||||
options.queueBadge.textContent = String(total);
|
options.queueBadge.textContent = String(total);
|
||||||
options.queueBadge.hidden = total === 0;
|
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};
|
return {start, select, refreshVisibility, updateAttention, updateQueues, updateWork};
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,4 @@ async def test_mobile_launch_progressively_discloses_secondary_controls():
|
||||||
assert "header { min-height:56px; max-height:64px;" in html
|
assert "header { min-height:56px; max-height:64px;" in html
|
||||||
assert ".work-settings:not([open]) > .work-settings-panel { display:none; }" 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 ".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
|
||||||
|
|
|
||||||
|
|
@ -293,13 +293,14 @@ sheet.showModal = function () {{ this.open = true; }};
|
||||||
sheet.close = function () {{ this.open = false; this.listeners.close?.(); }};
|
sheet.close = function () {{ this.open = false; this.listeners.close?.(); }};
|
||||||
const close = new FakeElement();
|
const close = new FakeElement();
|
||||||
const badge = 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 counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()]));
|
||||||
const selected = [];
|
const selected = [];
|
||||||
|
const utilities = [];
|
||||||
const dock = createDock({{
|
const dock = createDock({{
|
||||||
nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet],
|
nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet],
|
||||||
queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge,
|
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() {{}},
|
observe() {{}},
|
||||||
}});
|
}});
|
||||||
dock.start();
|
dock.start();
|
||||||
|
|
@ -308,9 +309,11 @@ queues.click();
|
||||||
const opened = sheet.open;
|
const opened = sheet.open;
|
||||||
rows.update.click();
|
rows.update.click();
|
||||||
const closedAfterSelect = !sheet.open;
|
const closedAfterSelect = !sheet.open;
|
||||||
|
queues.click(); rows.recaps.click();
|
||||||
|
const closedAfterUtility = !sheet.open;
|
||||||
queues.click(); close.click();
|
queues.click(); close.click();
|
||||||
process.stdout.write(JSON.stringify({{
|
process.stdout.write(JSON.stringify({{
|
||||||
opened, closedAfterSelect, selected,
|
opened, closedAfterSelect, closedAfterUtility, selected, utilities,
|
||||||
badge:badge.textContent, badgeLabel:queues.attributes['aria-label'],
|
badge:badge.textContent, badgeLabel:queues.attributes['aria-label'],
|
||||||
counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])),
|
counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])),
|
||||||
updateLabel:rows.update.attributes['aria-label'],
|
updateLabel:rows.update.attributes['aria-label'],
|
||||||
|
|
@ -324,13 +327,15 @@ process.stdout.write(JSON.stringify({{
|
||||||
assert json.loads(result.stdout) == {
|
assert json.loads(result.stdout) == {
|
||||||
"opened": True,
|
"opened": True,
|
||||||
"closedAfterSelect": True,
|
"closedAfterSelect": True,
|
||||||
|
"closedAfterUtility": True,
|
||||||
"selected": ["update"],
|
"selected": ["update"],
|
||||||
|
"utilities": ["recaps:trigger"],
|
||||||
# Updates are a drill-down within Attention and must not inflate the aggregate badge.
|
# Updates are a drill-down within Attention and must not inflate the aggregate badge.
|
||||||
"badge": "10",
|
"badge": "10",
|
||||||
"badgeLabel": "Queues, 10 items",
|
"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",
|
"updateLabel": "Updates, 5 unread conversations",
|
||||||
"queueFocuses": 2,
|
"queueFocuses": 3,
|
||||||
"columnState": None,
|
"columnState": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,13 +381,16 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
||||||
assert '<strong>Updates</strong><small>Unread conversations</small>' in html
|
assert '<strong>Updates</strong><small>Unread conversations</small>' in html
|
||||||
assert 'data-mobile-queue="later"' in html
|
assert 'data-mobile-queue="later"' in html
|
||||||
assert 'data-mobile-queue="draft"' in html
|
assert 'data-mobile-queue="draft"' in html
|
||||||
|
assert 'data-mobile-queue="recaps"' in html
|
||||||
|
assert '<strong>Recaps</strong><small>History</small>' in html
|
||||||
assert 'id="mobile-queue-count"' in html
|
assert 'id="mobile-queue-count"' in html
|
||||||
assert 'mobileTaskDock.updateQueues(counts)' in html
|
assert 'mobileTaskDock.updateQueues(counts)' in html
|
||||||
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 => 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 '.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 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
|
||||||
assert '.mobile-task-dock[data-attention="true"]' not 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 '<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 => 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 "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