Merge pull request 'Run the mobile Agenda as a sequential work session' (#732)
All checks were successful
CI / lint (push) Successful in 1m24s
CI / build-release (push) Successful in 5s
CI / release-candidate (push) Successful in 5s

Closes #731
This commit is contained in:
timmy 2026-08-13 10:57:28 +00:00
commit 1fc70f6890
3 changed files with 79 additions and 9 deletions

View File

@ -1148,7 +1148,9 @@
}
function updateWorkSessionActions() {
const active = workSession.active();
qs('#start-work-session').hidden = active;
const startWorkSession = qs('#start-work-session');
startWorkSession.hidden = active;
startWorkSession.textContent = selectedWorkFilter === 'agenda' ? 'Start Agenda' : 'Start work';
qs('#resume-today-session').hidden = active || !todayMyWork.length || !workSession.resumable();
qs('#end-today-session').hidden = !active;
updateDetailDeferLabels(active);
@ -1157,9 +1159,11 @@
}
const workSession = createWorkSession({
getItems: () => selectedWorkFilter === 'today' ? todayMyWork : activeMyWork,
getFilter: () => selectedWorkFilter === 'today' ? 'all' : selectedWorkFilter,
getMilestone: () => selectedWorkMilestone,
getItems: () => selectedWorkFilter === 'today' ? todayMyWork :
selectedWorkFilter === 'agenda' ? agendaMyWork(activeMyWork) : activeMyWork,
getFilter: () => selectedWorkFilter === 'today' ? 'all' :
selectedWorkFilter === 'agenda' ? 'all' : selectedWorkFilter,
getMilestone: () => selectedWorkFilter === 'agenda' ? 'all' : selectedWorkMilestone,
checkpoint: sessionCheckpoint,
checkpointEnabled: () => selectedWorkFilter === 'today',
onOpen: openWorkSessionItem,
@ -1178,7 +1182,15 @@
button.hidden = !workSession.checkpointed();
});
},
onFinish: () => todayRecapView.finish(selectedWorkFilter),
onFinish: () => {
updateWorkSessionActions();
if (selectedWorkFilter === 'agenda') {
closeOpenWorkSheets();
qs('#my-work-action-status').textContent = 'Agenda complete.';
return;
}
todayRecapView.finish(selectedWorkFilter);
},
});
function selectTodayWork() {
qs('[data-work-filter="today"]').click();
@ -5868,7 +5880,7 @@
qs('#save-today-plan').addEventListener('click', event => commitPlanToday(false, event.currentTarget));
qs('#save-and-start-today').addEventListener('click', event => commitPlanToday(true, event.currentTarget));
qs('#start-work-session').addEventListener('click', () => {
const sessionItems = selectedWorkFilter === 'today' ? todayMyWork : filterMyWork(lastMyWork, selectedWorkFilter);
const sessionItems = workSession.items();
if (!sessionItems.length) {
qs('#my-work-action-status').textContent = 'No visible work to start.';
return;
@ -6053,6 +6065,7 @@
const selectedCount = button.querySelector('[data-work-count]').textContent;
qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')';
renderMyWork();
updateWorkSessionActions();
updateWorkPaginationControls();
if (selectedWorkFilter === 'agenda') completeAgendaIssues();
if (!preserveRoute) workRoute.queue(filter);

View File

@ -195,6 +195,62 @@ process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-1
assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"]
def test_mobile_agenda_runs_as_an_ordered_non_durable_work_session():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const values = new Map([['stackchain.today-session.v1', 'preserve-today']]);
const checkpoint = {{
read:() => null,
save:() => {{ throw new Error('Agenda must not save Today'); }},
clear:() => {{ throw new Error('Agenda must not clear Today'); }},
}};
const active = [
{{kind:'issue',repository:'o/r',number:3,title:'Tomorrow',is_assigned:true,due_date:'2026-08-14'}},
{{kind:'issue',repository:'o/r',number:1,title:'Overdue',is_assigned:true,due_date:'2026-08-12'}},
{{kind:'issue',repository:'o/r',number:2,title:'Today',is_assigned:true,due_date:'2026-08-13'}},
{{kind:'issue',repository:'o/r',number:4,title:'No deadline',is_assigned:true}},
];
const opened = [];
let finished = 0;
const session = buildMyWork.createWorkSession({{
getItems:() => buildMyWork.agendaMyWork(active, new Date('2026-08-13T12:00:00')),
getFilter:() => 'all', checkpoint, checkpointEnabled:() => false,
onOpen:item => opened.push(item.title), onProgress:() => {{}},
onFinish:() => {{ finished += 1; }},
}});
const started = session.start();
session.next();
session.next();
const completed = session.next();
process.stdout.write(JSON.stringify({{
started, completed, opened, finished, active:session.active(), today:values.get('stackchain.today-session.v1'),
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"started": True,
"completed": False,
"opened": ["Overdue", "Today", "Tomorrow"],
"finished": 1,
"active": False,
"today": "preserve-today",
}
@pytest.mark.anyio
async def test_mobile_agenda_launcher_starts_the_rendered_queue_and_finishes_in_agenda():
html = await dashboard()
assert "selectedWorkFilter === 'agenda' ? agendaMyWork(activeMyWork)" in html
assert "selectedWorkFilter === 'agenda' ? 'all'" in html
assert "startWorkSession.textContent = selectedWorkFilter === 'agenda' ? 'Start Agenda' : 'Start work'" in html
assert "const sessionItems = workSession.items()" in html
assert "qs('#my-work-action-status').textContent = 'Agenda complete.'" in html
assert "checkpointEnabled: () => selectedWorkFilter === 'today'" in html
@pytest.mark.parametrize("timezone_name", ["Asia/Tokyo", "America/Los_Angeles"])
def test_mobile_agenda_preserves_the_gitea_calendar_day_in_every_timezone(timezone_name):
script = f"""
@ -1475,7 +1531,7 @@ async def test_mobile_my_work_exposes_touch_safe_milestone_lane_and_issue_editor
assert 'id="save-issue-milestone"' in html
assert 'class="pill milestone-badge"' in html
assert '.work-milestone-filter, .issue-milestone-editor select, .issue-milestone-editor button { min-height:44px;' in html
assert 'getMilestone: () => selectedWorkMilestone' in html
assert "getMilestone: () => selectedWorkFilter === 'agenda' ? 'all' : selectedWorkMilestone" in html
assert 'buildMyWork.replaceIssueMilestone(' in html
@ -5821,7 +5877,7 @@ async def test_mobile_dashboard_puts_filterable_my_work_before_auxiliary_panels(
assert '<script src="static/my-work.js"></script>' in html
assert "buildMyWork(data)" in html
assert "markMyWorkStale()" in html
assert "filterMyWork(lastMyWork, selectedWorkFilter)" in html
assert "const sessionItems = workSession.items()" in html
@pytest.mark.anyio

View File

@ -344,7 +344,8 @@ async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
assert "const todayWork = createTodayWork({" in html
assert "getTodayCount: () => todayMyWork.length" in html
assert "startToday: startTodaySession" in html
assert "selectedWorkFilter === 'today' ? todayMyWork : activeMyWork" in html
assert "selectedWorkFilter === 'today' ? todayMyWork :" in html
assert "selectedWorkFilter === 'agenda' ? agendaMyWork(activeMyWork) : activeMyWork" in html
assert 'data-today-add' in html
assert 'data-today-remove' in html
assert 'data-today-move="up"' in html