Make the mobile Work action launch the highest-priority queue #756
|
|
@ -58,17 +58,6 @@
|
||||||
qs('#my-work').scrollIntoView({block:'start'});
|
qs('#my-work').scrollIntoView({block:'start'});
|
||||||
qs('#my-work').focus();
|
qs('#my-work').focus();
|
||||||
}
|
}
|
||||||
const mobileWorkEntry = createMobileWorkEntry({
|
|
||||||
isTodayActive: () => workSession.checkpointed(),
|
|
||||||
isTodayResumable: () => workSession.resumable(),
|
|
||||||
getTodayCount: () => todayMyWork.length,
|
|
||||||
getEligibleCount: () => activeMyWork.length,
|
|
||||||
continueToday: continueTodaySession,
|
|
||||||
resumeToday: resumeTodaySession,
|
|
||||||
startToday: startTodaySession,
|
|
||||||
planToday: () => openPlanToday(mobileTaskButtons.work),
|
|
||||||
findWork: () => qs('#find-work').click(),
|
|
||||||
});
|
|
||||||
let mobileQueueCounts = {};
|
let mobileQueueCounts = {};
|
||||||
const mobileQueueLauncher = createMobileQueueLauncher({
|
const mobileQueueLauncher = createMobileQueueLauncher({
|
||||||
openToday: () => mobileWorkEntry.open(),
|
openToday: () => mobileWorkEntry.open(),
|
||||||
|
|
@ -80,6 +69,19 @@
|
||||||
getCounts: () => mobileQueueCounts,
|
getCounts: () => mobileQueueCounts,
|
||||||
openFindWork: () => qs('#find-work').click(),
|
openFindWork: () => qs('#find-work').click(),
|
||||||
});
|
});
|
||||||
|
const mobileWorkEntry = createMobileWorkEntry({
|
||||||
|
isTodayActive: () => workSession.checkpointed(),
|
||||||
|
isTodayResumable: () => workSession.resumable(),
|
||||||
|
getTodayCount: () => todayMyWork.length,
|
||||||
|
getEligibleCount: () => activeMyWork.length,
|
||||||
|
queueLauncher: mobileQueueLauncher,
|
||||||
|
continueToday: continueTodaySession,
|
||||||
|
resumeToday: resumeTodaySession,
|
||||||
|
startToday: startTodaySession,
|
||||||
|
planToday: () => openPlanToday(mobileTaskButtons.work),
|
||||||
|
|
||||||
|
findWork: () => qs('#find-work').click(),
|
||||||
|
});
|
||||||
function showMobileQueueCompletion(completedName) {
|
function showMobileQueueCompletion(completedName) {
|
||||||
const next = mobileQueueLauncher.recommend();
|
const next = mobileQueueLauncher.recommend();
|
||||||
qs('#mobile-queue-heading').textContent = completedName + ' cleared';
|
qs('#mobile-queue-heading').textContent = completedName + ' cleared';
|
||||||
|
|
@ -141,9 +143,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('Filter restore failed', e);
|
|
||||||
}
|
|
||||||
let lastMyWork = [];
|
let lastMyWork = [];
|
||||||
let lastDrafts = [];
|
let lastDrafts = [];
|
||||||
let lastNotifications = [];
|
let lastNotifications = [];
|
||||||
|
|
@ -6256,9 +6256,7 @@
|
||||||
launchFilterResolved = true;
|
launchFilterResolved = true;
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter);
|
sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter);
|
||||||
} catch (e) {
|
} catch (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))
|
||||||
);
|
);
|
||||||
|
|
@ -6296,7 +6294,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('Milestone save failed', e); }
|
catch (e) {}
|
||||||
renderMyWork();
|
renderMyWork();
|
||||||
if (workSession.active()) workSession.reconcile();
|
if (workSession.active()) workSession.reconcile();
|
||||||
});
|
});
|
||||||
|
|
@ -6347,7 +6345,6 @@
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
qs('#push-updates').disabled = true;
|
qs('#push-updates').disabled = true;
|
||||||
qs('#push-update-status').textContent = 'Update notification settings unavailable.';
|
qs('#push-update-status').textContent = 'Update notification settings unavailable.';
|
||||||
console.warn('Push notifications unavailable', error);
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
const continuation = [
|
const continuation = [
|
||||||
['attention', 'Start Attention'],
|
['attention', 'Start Attention'],
|
||||||
['today', 'Continue Today'],
|
['today', 'Continue Today'],
|
||||||
|
['update', 'Resume Updates'],
|
||||||
['agenda', 'Open Agenda'],
|
['agenda', 'Open Agenda'],
|
||||||
['later', 'Start Later'],
|
['later', 'Start Later'],
|
||||||
['draft', 'Open Drafts'],
|
['draft', 'Open Drafts'],
|
||||||
|
|
|
||||||
|
|
@ -64,19 +64,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWork(mode) {
|
function updateWork(mode) {
|
||||||
const labels = {
|
const text = {
|
||||||
continue: ['Continue', 'Continue Today'],
|
continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find',
|
||||||
resume: ['Resume', 'Resume Today'],
|
attention:'Attention', update:'Updates', agenda:'Agenda', later:'Later', draft:'Drafts',
|
||||||
start: ['Start', 'Start Today'],
|
}[mode] || 'Work';
|
||||||
plan: ['Plan', 'Plan Today'],
|
const queue = ['Attention', 'Updates', 'Agenda', 'Later', 'Drafts'].includes(text);
|
||||||
find: ['Find', 'Find work'],
|
if (options.workLabel) options.workLabel.textContent = text;
|
||||||
work: ['Work', 'Work'],
|
if (buttons.work) buttons.work.setAttribute('aria-label', queue
|
||||||
};
|
? (mode === 'update' ? 'Resume ' : 'Open ') + text
|
||||||
const workMode = labels[mode] ? mode : 'work';
|
: mode === 'find' ? 'Find work' : text + (mode === 'work' ? '' : ' Today'));
|
||||||
if (options.workLabel) options.workLabel.textContent = labels[workMode][0];
|
|
||||||
if (buttons.work) {
|
|
||||||
buttons.work.setAttribute('aria-label', labels[workMode][1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAttention(count) {
|
function updateAttention(count) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
})(typeof self !== 'undefined' ? self : this, function createMobileWorkEntry(options) {
|
})(typeof self !== 'undefined' ? self : this, function createMobileWorkEntry(options) {
|
||||||
function mode() {
|
function mode() {
|
||||||
if (options.isTodayActive()) return 'continue';
|
if (options.isTodayActive()) return 'continue';
|
||||||
|
const recommended = options.queueLauncher && options.queueLauncher.recommend();
|
||||||
|
if (recommended && recommended.name !== 'today') return recommended.name;
|
||||||
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
|
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
|
||||||
if (options.getTodayCount() > 0) return 'start';
|
if (options.getTodayCount() > 0) return 'start';
|
||||||
if (options.getEligibleCount() > 0) return 'plan';
|
if (options.getEligibleCount() > 0) return 'plan';
|
||||||
|
|
@ -16,7 +18,8 @@
|
||||||
else if (current === 'resume') options.resumeToday();
|
else if (current === 'resume') options.resumeToday();
|
||||||
else if (current === 'start') options.startToday();
|
else if (current === 'start') options.startToday();
|
||||||
else if (current === 'plan') options.planToday();
|
else if (current === 'plan') options.planToday();
|
||||||
else options.findWork();
|
else if (current === 'find') options.findWork();
|
||||||
|
else options.queueLauncher.open(current);
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,43 @@ process.stdout.write(JSON.stringify({{modes, calls}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_work_entry_preserves_active_today_then_launches_highest_priority_queue():
|
||||||
|
script = f"""
|
||||||
|
const createEntry = require({json.dumps(str(ENTRY))});
|
||||||
|
const calls = [];
|
||||||
|
const state = {{active:true, today:2, resumable:true, eligible:3, next:{{name:'attention'}}}};
|
||||||
|
const entry = createEntry({{
|
||||||
|
isTodayActive: () => state.active,
|
||||||
|
isTodayResumable: () => state.resumable,
|
||||||
|
getTodayCount: () => state.today,
|
||||||
|
getEligibleCount: () => state.eligible,
|
||||||
|
queueLauncher: {{recommend: () => state.next, open: name => calls.push('queue:' + name)}},
|
||||||
|
continueToday: () => calls.push('continue'),
|
||||||
|
resumeToday: () => calls.push('resume'),
|
||||||
|
startToday: () => calls.push('start'),
|
||||||
|
planToday: () => calls.push('plan'),
|
||||||
|
|
||||||
|
findWork: () => calls.push('find'),
|
||||||
|
}});
|
||||||
|
const modes = [entry.open()];
|
||||||
|
state.active = false; modes.push(entry.open());
|
||||||
|
state.next = {{name:'today'}}; modes.push(entry.open());
|
||||||
|
state.today = 0; state.eligible = 0; state.next = {{name:'update'}}; modes.push(entry.open());
|
||||||
|
state.next = {{name:'agenda'}}; modes.push(entry.open());
|
||||||
|
state.next = {{name:'later'}}; modes.push(entry.open());
|
||||||
|
state.next = {{name:'draft'}}; modes.push(entry.open());
|
||||||
|
state.next = {{name:'find'}}; modes.push(entry.open());
|
||||||
|
process.stdout.write(JSON.stringify({{modes, calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"modes": ["continue", "attention", "resume", "update", "agenda", "later", "draft", "find"],
|
||||||
|
"calls": ["continue", "queue:attention", "resume", "queue:update", "queue:agenda", "queue:later", "queue:draft", "find"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
|
def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createDock = require({json.dumps(str(DOCK))});
|
const createDock = require({json.dumps(str(DOCK))});
|
||||||
|
|
@ -437,6 +474,59 @@ process.stdout.write(JSON.stringify({{first, agenda, opened, fallback, calls}}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
const calls = [];
|
||||||
|
let counts = {{attention:0, today:0, update:6, agenda:2, later:1, draft:1}};
|
||||||
|
const launcher = createLauncher({{
|
||||||
|
getCounts: () => counts,
|
||||||
|
openUpdates: () => {{ calls.push('updates'); return 'resumed'; }},
|
||||||
|
openAgenda: () => calls.push('agenda'),
|
||||||
|
selectFilter: name => calls.push('filter:' + name),
|
||||||
|
firstAction: () => null,
|
||||||
|
announce: () => {{}},
|
||||||
|
openFindWork: () => calls.push('find'),
|
||||||
|
}});
|
||||||
|
const recommendation = launcher.recommend();
|
||||||
|
const opened = launcher.continueWork();
|
||||||
|
process.stdout.write(JSON.stringify({{recommendation, opened, calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"recommendation": {"name": "update", "count": 6, "label": "Resume Updates (6)"},
|
||||||
|
"opened": "resumed",
|
||||||
|
"calls": ["updates"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_task_dock_labels_each_recommended_work_destination_accessibly():
|
||||||
|
script = f"""
|
||||||
|
const createDock = require({json.dumps(str(DOCK))});
|
||||||
|
const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}}};
|
||||||
|
const workLabel = {{textContent:''}};
|
||||||
|
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
|
||||||
|
const labels = {{}};
|
||||||
|
for (const mode of ['attention','update','agenda','later','draft']) {{
|
||||||
|
dock.updateWork(mode);
|
||||||
|
labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
|
||||||
|
}}
|
||||||
|
process.stdout.write(JSON.stringify(labels));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"attention": ["Attention", "Open Attention"],
|
||||||
|
"update": ["Updates", "Resume Updates"],
|
||||||
|
"agenda": ["Agenda", "Open Agenda"],
|
||||||
|
"later": ["Later", "Open Later"],
|
||||||
|
"draft": ["Drafts", "Open Drafts"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_renders_accessible_mobile_queue_completion_handoff():
|
async def test_dashboard_renders_accessible_mobile_queue_completion_handoff():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -507,6 +597,17 @@ async def test_dashboard_wires_mobile_work_dock_into_today_session_lifecycle():
|
||||||
assert "runTodayTransition('continue')" in html
|
assert "runTodayTransition('continue')" in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_dashboard_wires_work_dock_and_app_shortcut_to_live_queue_recommendation():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert "queueLauncher: mobileQueueLauncher" in html
|
||||||
|
assert "work: () => mobileWorkEntry.open()" in html
|
||||||
|
assert "continueWork: () => mobileWorkEntry.open()" in html
|
||||||
|
assert "mobileQueueCounts = counts;" in html
|
||||||
|
assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user