feat: open Week Ahead planned work (Closes #1222)
All checks were successful
CI / lint (pull_request) Successful in 3m47s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m53s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-21 13:25:36 +00:00
parent a3861da125
commit 9f5baaef6e
8 changed files with 119 additions and 25 deletions

View File

@ -330,6 +330,9 @@ textarea { resize: vertical; min-height: 120px; }
.week-review-day ul { display:grid; gap:10px; margin:0; padding:0; list-style:none; } .week-review-day ul { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
.week-review-day li { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:end; padding-top:8px; border-top:1px solid #31577f; overflow-wrap:anywhere; } .week-review-day li { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:end; padding-top:8px; border-top:1px solid #31577f; overflow-wrap:anywhere; }
.week-review-item-copy { display:grid; min-width:0; gap:3px; } .week-review-item-copy { display:grid; min-width:0; gap:3px; }
.week-review-item-open { display:block; width:100%; min-width:0; min-height:44px; padding:8px; border:0; border-radius:8px; background:transparent; color:inherit; text-align:left; }
.week-review-item-open:hover { background:#173453; }
.week-review-item-open:focus-visible { outline:3px solid #93c5fd; outline-offset:2px; }
.week-review-item-copy strong, .week-review-item-copy small { overflow-wrap:anywhere; } .week-review-item-copy strong, .week-review-item-copy small { overflow-wrap:anywhere; }
.week-review-item-copy small { color:#a9bdd3; } .week-review-item-copy small { color:#a9bdd3; }
.week-review-move { display:flex; align-items:end; gap:8px; } .week-review-move { display:flex; align-items:end; gap:8px; }
@ -767,6 +770,7 @@ textarea { resize: vertical; min-height: 120px; }
.update-decision-bar button, .update-decision-bar summary { min-height:44px; min-width:0; padding-inline:6px; } .update-decision-bar button, .update-decision-bar summary { min-height:44px; min-width:0; padding-inline:6px; }
.update-retry { min-height:44px; width:100%; margin-top:10px; } .update-retry { min-height:44px; width:100%; margin-top:10px; }
.issue-sheet { position:fixed; inset:0; z-index:56; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); } .issue-sheet { position:fixed; inset:0; z-index:56; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
.task-overlay-open .issue-sheet.open, .task-overlay-open .pull-sheet.open { z-index:76; }
.issue-sheet.open { display:flex; } .issue-sheet.open { display:flex; }
.issue-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; } .issue-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; } .issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }

View File

@ -393,7 +393,7 @@
fetchJson:fetchReviewJson,localDate:todayRollover.localDate,timeZone:todayRollover.timeZone, fetchJson:fetchReviewJson,localDate:todayRollover.localDate,timeZone:todayRollover.timeZone,
storage:localStorage,getLogin:() => planningOwnerLogin, storage:localStorage,getLogin:() => planningOwnerLogin,
}); });
function openWeekPlanner(trigger) { planningTomorrow=false; return weekWorkflow.open(trigger); } function openWeekPlanner(trigger) { planningTomorrow=false; return weekFlow.open(trigger); }
function renderTomorrowQueueSummary(value) { function renderTomorrowQueueSummary(value) {
qs('#mobile-tomorrow-summary').textContent = value ? tomorrowPlan.summary(value) : tomorrowPlan.summary(); qs('#mobile-tomorrow-summary').textContent = value ? tomorrowPlan.summary(value) : tomorrowPlan.summary();
} }
@ -452,14 +452,14 @@
storage: localStorage, storage: localStorage,
getLogin: () => planningOwnerLogin, getLogin: () => planningOwnerLogin,
}); });
const weekItem=identity=>[...todayMyWork,...activeMyWork].find(item=>todayWork.identity(item)===identity); const weekItem=id=>[...todayMyWork,...activeMyWork].find(item=>todayWork.identity(item)===id);
const weekWorkflow=createWeekPlanWorkflow({controller:weekPlan,qs,getLogin:()=>planningOwnerLogin, const weekFlow=createWeekPlanWorkflow({controller:weekPlan,qs,
getItem:weekItem, getItem:weekItem,openItem:openRoutedWork,
openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value), openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value),
escapeHtml,escapeAttribute:escAttr,todayWork, escapeHtml,escapeAttribute:escAttr,todayWork,
refresh:refreshMyWorkView,warm:warmTodayOffline}); refresh:refreshMyWorkView,warm:warmTodayOffline});
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr, const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
onDone:()=>{weekWorkflow.finish();taskOverlayHistory.leave();}, onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
}); });
const todaySync = createTodaySync({ const todaySync = createTodaySync({
storage: localStorage, storage: localStorage,
@ -475,7 +475,7 @@
if (!planningOwnerLogin) return; if (!planningOwnerLogin) return;
latestTodayPlan = plan; latestTodayPlan = plan;
const startDayLaunch = window.location.hash === '#/my-work/start-day'; const startDayLaunch = window.location.hash === '#/my-work/start-day';
weekWorkflow.promote(plan).finally(() => { weekFlow.promote(plan).finally(() => {
if (!startDayLaunch) return; if (!startDayLaunch) return;
window.history.replaceState({}, '', '#/my-work/today'); window.history.replaceState({}, '', '#/my-work/today');
openMobileStartDay(); openMobileStartDay();
@ -519,7 +519,7 @@
check:() => Promise.all([ check:() => Promise.all([
syncPendingTomorrow(), syncPendingTomorrow(),
weekPlan.pending() ? weekPlan.flush().catch(() => false) : false, weekPlan.pending() ? weekPlan.flush().catch(() => false) : false,
latestTodayPlan ? weekWorkflow.promote(latestTodayPlan) : false, latestTodayPlan ? weekFlow.promote(latestTodayPlan) : false,
]), ]),
}); });
const todayHandoff = createTodayHandoff({ const todayHandoff = createTodayHandoff({
@ -2706,7 +2706,7 @@
planToday.cancel(); planToday.cancel();
planningTomorrow = false; planningTomorrow = false;
weekCalendar.close(); weekCalendar.close();
weekWorkflow.clear(); weekFlow.clear();
qs('#tomorrow-conflict-review').hidden = true; qs('#tomorrow-conflict-review').hidden = true;
qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode'); qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
qs('#week-conflict-review').hidden = true; qs('#week-conflict-review').hidden = true;
@ -2765,7 +2765,7 @@
const planToday = createPlanToday({ const planToday = createPlanToday({
identity: item => todayWork.identity(item), identity: item => todayWork.identity(item),
limit: todayWork.limit, limit: todayWork.limit,
save: plan => weekWorkflow.active() ? weekWorkflow.save(plan) : save: plan => weekFlow.active() ? weekFlow.save(plan) :
(planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)), (planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)),
start: () => { start: () => {
qs('[data-work-filter="today"]').click(); qs('[data-work-filter="today"]').click();
@ -3072,7 +3072,7 @@
function showWeekConflict(conflict) { function showWeekConflict(conflict) {
qs('#plan-today-title').textContent='Resolve Week Ahead conflict'; qs('#plan-today-title').textContent='Resolve Week Ahead conflict';
const focus=weekWorkflow.renderConflict(conflict); const focus=weekFlow.renderConflict(conflict);
qs('#week-conflict-status').textContent=''; qs('#week-conflict-status').textContent='';
qs('#week-conflict-review').hidden=false; qs('#week-conflict-review').hidden=false;
qs('#plan-today-sheet').classList.add('week-conflict-mode'); qs('#plan-today-sheet').classList.add('week-conflict-mode');
@ -3082,12 +3082,12 @@
} }
function openPlanToday(trigger, navigate = true, actualMinutes = null) { function openPlanToday(trigger, navigate = true, actualMinutes = null) {
if (!planningOwnerLogin && !weekWorkflow.active()) { if (!planningOwnerLogin && !weekFlow.active()) {
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.'; qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
return; return;
} }
if (trigger) planTodayTrigger = trigger; if (trigger) planTodayTrigger = trigger;
const weekCopy=weekWorkflow.copy(); const weekCopy=weekFlow.copy();
qs('#plan-today-title').textContent = weekCopy ? weekCopy.title : (planningTomorrow ? 'Plan Tomorrow' : qs('#plan-today-title').textContent = weekCopy ? weekCopy.title : (planningTomorrow ? 'Plan Tomorrow' :
(pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today'))); (pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today')));
if (actualMinutes) pendingPlanActualMinutes = actualMinutes; if (actualMinutes) pendingPlanActualMinutes = actualMinutes;
@ -3100,7 +3100,7 @@
showTomorrowConflict(conflict); showTomorrowConflict(conflict);
return; return;
} }
const weekConflict=weekWorkflow.active()?weekPlan.conflict():null; const weekConflict=weekFlow.active()?weekPlan.conflict():null;
if(weekConflict){showWeekConflict(weekConflict);return;} if(weekConflict){showWeekConflict(weekConflict);return;}
qs('#tomorrow-conflict-review').hidden = true; qs('#tomorrow-conflict-review').hidden = true;
qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode'); qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
@ -3110,7 +3110,7 @@
pendingPlanActualMinutes = null; pendingPlanActualMinutes = null;
const protectProposal = pendingProtectToday; const protectProposal = pendingProtectToday;
const tomorrow = planningTomorrow ? tomorrowPlan.state() : null; const tomorrow = planningTomorrow ? tomorrowPlan.state() : null;
const weekDay = weekWorkflow.day(); const weekDay = weekFlow.day();
const availablePlanningItems = [...todayMyWork, ...activeMyWork].filter((item, index, items) => const availablePlanningItems = [...todayMyWork, ...activeMyWork].filter((item, index, items) =>
items.findIndex(candidate => todayWork.identity(candidate) === todayWork.identity(item)) === index items.findIndex(candidate => todayWork.identity(candidate) === todayWork.identity(item)) === index
); );
@ -3128,7 +3128,7 @@
qs('#plan-today-sheet').hidden = false; qs('#plan-today-sheet').hidden = false;
document.body.classList.add('task-overlay-open'); document.body.classList.add('task-overlay-open');
renderPlanToday(); renderPlanToday();
weekWorkflow.renderDates(); weekFlow.renderDates();
if (protectProposal) qs('#plan-today-build-status').textContent = protectProposal.summary + if (protectProposal) qs('#plan-today-build-status').textContent = protectProposal.summary +
(protectProposal.displaced.length ? '. Displaced work remains unchanged until you save.' : '. Review estimates and capacity before saving.'); (protectProposal.displaced.length ? '. Displaced work remains unchanged until you save.' : '. Review estimates and capacity before saving.');
qs('#cancel-plan-today').focus(); qs('#cancel-plan-today').focus();
@ -4421,7 +4421,7 @@
selectedIssueOffline = false; selectedIssueOffline = false;
selectedIssueDetail = null; selectedIssueDetail = null;
issueConversation = null; issueConversation = null;
if (issueTrigger?.isConnected) issueTrigger.focus(); setTimeout(()=>issueTrigger?.focus());
} }
function closeIssueEditorFromHistory() { function closeIssueEditorFromHistory() {
@ -7920,7 +7920,7 @@
}); });
qs('#cancel-plan-today').addEventListener('click', closePlanToday); qs('#cancel-plan-today').addEventListener('click', closePlanToday);
qs('#confirm-week-plan').addEventListener('click', () => { qs('#confirm-week-plan').addEventListener('click', () => {
if(!weekWorkflow.confirm()){ if(!weekFlow.confirm()){
qs('#week-review-status').textContent=weekPlan.pending()?'Wait for Week Ahead to finish syncing before confirming.':'Move duplicated work to one date before confirming.'; qs('#week-review-status').textContent=weekPlan.pending()?'Wait for Week Ahead to finish syncing before confirming.':'Move duplicated work to one date before confirming.';
return; return;
} }
@ -7950,7 +7950,7 @@
const result = planToday.commit({ start, confirmOverCapacity: button.dataset.confirmOverCapacity === 'true' }); const result = planToday.commit({ start, confirmOverCapacity: button.dataset.confirmOverCapacity === 'true' });
if (result === 'saved') { if (result === 'saved') {
button.dataset.confirmOverCapacity = ''; button.dataset.confirmOverCapacity = '';
if (!weekWorkflow.active() || !weekWorkflow.advance()) taskOverlayHistory.leave(); if (!weekFlow.active() || !weekFlow.advance()) taskOverlayHistory.leave();
} else if (result === 'confirm-over-capacity') { } else if (result === 'confirm-over-capacity') {
button.dataset.confirmOverCapacity = 'true'; button.dataset.confirmOverCapacity = 'true';
qs('#plan-today-error').textContent = 'This plan exceeds your available time. Press again to save over capacity.'; qs('#plan-today-error').textContent = 'This plan exceeds your available time. Press again to save over capacity.';

View File

@ -429,7 +429,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
item:id=>pendingItems[id]||confirmedItems[id]||null, item:id=>pendingItems[id]||confirmedItems[id]||null,
offline:()=>offlineSnapshot}; offline:()=>offlineSnapshot};
} }
function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute, function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
todayWork,refresh,warm}={}) { todayWork,refresh,warm}={}) {
let selectedDate=null; let selectedDate=null;
let reviewing=false; let reviewing=false;
@ -528,7 +528,9 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
if(!item)return '<div class="week-review-item-copy"><strong>Work details unavailable</strong><small>'+escapeHtml(String(id).slice(0,96))+'</small></div>'; if(!item)return '<div class="week-review-item-copy"><strong>Work details unavailable</strong><small>'+escapeHtml(String(id).slice(0,96))+'</small></div>';
const kind=item.kind==='pull'?'Pull request':'Issue'; const kind=item.kind==='pull'?'Pull request':'Issue';
const reference=item.repository&&item.number!=null?item.repository+' #'+item.number:kind; const reference=item.repository&&item.number!=null?item.repository+' #'+item.number:kind;
return '<div class="week-review-item-copy"><strong>'+escapeHtml(item.title||'Untitled work')+'</strong><small>'+escapeHtml(reference+' · '+kind+(estimate?' · '+estimate+' min':''))+'</small></div>'; const copy='<span class="week-review-item-copy"><strong>'+escapeHtml(item.title||'Untitled work')+'</strong><small>'+escapeHtml(reference+' · '+kind+(estimate?' · '+estimate+' min':''))+'</small></span>';
const actionable=overviewing&&['issue','pull'].includes(item.kind)&&item.repository&&item.number>0;
return actionable?'<button type="button" class="week-review-item-open" data-week-open-item="'+escapeAttribute(id)+'">'+copy+'</button>':copy;
}; };
root.innerHTML=value.days.map(day=>{ root.innerHTML=value.days.map(day=>{
const capacity=Number(day.capacity_minutes)||0; const capacity=Number(day.capacity_minutes)||0;
@ -563,6 +565,10 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
controller.flush().then(()=>{renderReview();qs('#mobile-week-summary').textContent=controller.summary();}) controller.flush().then(()=>{renderReview();qs('#mobile-week-summary').textContent=controller.summary();})
.catch(error=>{qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Changes remain on this phone.';}); .catch(error=>{qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Changes remain on this phone.';});
})); }));
root.querySelectorAll('[data-week-open-item]').forEach(button=>button.addEventListener('click',event=>{
const id=button.dataset.weekOpenItem,item=getItem(id)||controller.item?.(id);
if(item)openItem?.(item,event.currentTarget);
}));
root.querySelectorAll('[data-week-edit-day]').forEach(button=>button.addEventListener('click',()=>editDay(button.dataset.weekEditDay))); root.querySelectorAll('[data-week-edit-day]').forEach(button=>button.addEventListener('click',()=>editDay(button.dataset.weekEditDay)));
renderPass(); renderPass();
} }

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import os import os
import re
import threading import threading
from datetime import date, timedelta from datetime import date, timedelta
from pathlib import Path from pathlib import Path
@ -75,6 +76,18 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(page.locator("#confirm-week-plan")).to_be_hidden() expect(page.locator("#confirm-week-plan")).to_be_hidden()
edit_week = page.locator("#edit-week-plan") edit_week = page.locator("#edit-week-plan")
expect(edit_week).to_be_visible() expect(edit_week).to_be_visible()
planned_item = cards.first.locator("[data-week-open-item]")
expect(planned_item).to_have_count(1)
planned_bounds = planned_item.bounding_box()
assert planned_bounds and planned_bounds["height"] >= 44
planned_item.click()
expect(page.locator("#issue-sheet")).to_have_class(re.compile(r"\bopen\b"))
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
expect(overview).to_be_visible()
page.locator("#close-issue-sheet").click()
expect(page.locator("#issue-sheet")).not_to_have_class(re.compile(r"\bopen\b"))
expect(overview).to_be_visible()
expect(planned_item).to_be_focused()
assert saved == [], "opening and inspecting Week Ahead must not write" assert saved == [], "opening and inspecting Week Ahead must not write"
for control in (cards.first.locator("[data-week-edit-day]"), edit_week): for control in (cards.first.locator("[data-week-edit-day]"), edit_week):
bounds = control.bounding_box() bounds = control.bounding_box()

View File

@ -7788,7 +7788,7 @@ async def test_assigned_issues_open_accessible_mobile_action_sheet_with_safe_mut
assert "window.confirm(confirmation)" in html assert "window.confirm(confirmation)" in html
assert "issueController.close(selectedIssue)" in html assert "issueController.close(selectedIssue)" in html
assert "lastMyWork.filter" in html assert "lastMyWork.filter" in html
assert "if (issueTrigger?.isConnected) issueTrigger.focus()" in html assert "setTimeout(()=>issueTrigger?.focus())" in html
assert "e.key === 'Escape' && selectedIssue" in html assert "e.key === 'Escape' && selectedIssue" in html

View File

@ -312,6 +312,6 @@ def test_mobile_settings_and_launch_route_wire_start_day_into_existing_promotion
assert "startDayControl:qs('#push-start-day')" in dashboard assert "startDayControl:qs('#push-start-day')" in dashboard
assert "startDayHour:qs('#push-start-day-hour')" in dashboard assert "startDayHour:qs('#push-start-day-hour')" in dashboard
assert "window.location.hash === '#/my-work/start-day'" in dashboard assert "window.location.hash === '#/my-work/start-day'" in dashboard
assert "weekWorkflow.promote(plan).finally(() =>" in dashboard assert "weekFlow.promote(plan).finally(() =>" in dashboard
assert "onRemotePlan: async plan =>" not in dashboard assert "onRemotePlan: async plan =>" not in dashboard
assert "openMobileStartDay()" in dashboard assert "openMobileStartDay()" in dashboard

View File

@ -419,5 +419,5 @@ def test_tomorrow_plan_has_a_touch_safe_mobile_entry_and_reuses_the_ordered_plan
assert "qs('#plan-tomorrow').addEventListener('click'" in dashboard assert "qs('#plan-tomorrow').addEventListener('click'" in dashboard
assert "planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)" in dashboard assert "planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)" in dashboard
assert "tomorrowPlan.load()" in dashboard assert "tomorrowPlan.load()" in dashboard
assert "weekWorkflow.promote(plan)" in dashboard assert "weekFlow.promote(plan)" in dashboard
assert "tomorrowPlan.startLifecycle" in dashboard assert "tomorrowPlan.startLifecycle" in dashboard

View File

@ -260,6 +260,77 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
assert result["editWeekLabel"] == "Edit week" assert result["editWeekLabel"] == "Edit week"
def test_week_overview_opens_known_work_with_the_originating_control_and_leaves_unknown_work_static():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const dateRoot=makeElement();
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;},get(){return this.value||'';}});
elements.set('#week-plan-dates',dateRoot);
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){
this.value=value;
this.openButtons=[...value.matchAll(/data-week-open-item=\"([^\"]+)/g)].map(match=>({
dataset:{weekOpenItem:match[1]},listeners:{},isConnected:true,focused:false,
addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
}));
},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-open-item]'?reviewDays.openButtons||[]:[];
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
'#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review',
'#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import'])
if(!elements.has(selector))elements.set(selector,makeElement());
const known={kind:'issue',title:'Inspect planned work',repository:'stackchain/dashboard',number:42};
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
ids:index===0?['known','missing']:[],capacity_minutes:90,
estimates:index===0?{known:30,missing:20}:{},planned_minutes:index===0?50:0,overloaded:false})),
duplicates:[],blockers:[],can_confirm:true};
let opened=null,writes=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
pass:()=>({}),load:async()=>({}),summary:()=> '2 items across 1 day',review:()=>review,pending:()=>false,offline:()=>false,
move:()=>true,flush:async()=>{writes+=1;}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
getItem:id=>id==='known'?known:null,openItem:(item,trigger)=>{opened={item,sameTrigger:trigger===reviewDays.openButtons[0]};},
openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const button=reviewDays.openButtons[0];
if(button)button.listeners.click({currentTarget:button});
console.log(JSON.stringify({markup:reviewDays.innerHTML,buttonCount:reviewDays.openButtons.length,opened,writes}));
""")
assert result["buttonCount"] == 1
assert 'class="week-review-item-open"' in result["markup"]
assert 'data-week-open-item="known"' in result["markup"]
assert 'data-week-open-item="missing"' not in result["markup"]
assert result["opened"] == {
"item": {
"kind": "issue",
"title": "Inspect planned work",
"repository": "stackchain/dashboard",
"number": 42,
},
"sameTrigger": True,
}
assert result["writes"] == 0
def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_with_mobile_targets():
dashboard = (FRONTEND / "dashboard.js").read_text()
css = (FRONTEND / "dashboard.css").read_text()
workflow_mount = dashboard.split("const weekFlow=createWeekPlanWorkflow({", 1)[1].split("});", 1)[0]
assert "openItem:openRoutedWork" in workflow_mount
assert ".week-review-item-open" in css
rule = css.split(".week-review-item-open", 1)[1].split("}", 1)[0]
assert "min-height:44px" in rule
assert "width:100%" in rule
def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data(): def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data():
result = run_controller(""" result = run_controller("""
const createWorkflow=createWeekPlan.Workflow; const createWorkflow=createWeekPlan.Workflow;
@ -933,7 +1004,7 @@ def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe():
assert 'id="week-plan-dates"' in index assert 'id="week-plan-dates"' in index
assert '<script src="static/week-plan.js"></script>' in index assert '<script src="static/week-plan.js"></script>' in index
assert "name === 'week' ? openWeekPlanner" in dashboard assert "name === 'week' ? openWeekPlanner" in dashboard
assert "weekWorkflow.save" in dashboard assert "weekFlow.save" in dashboard
assert ".week-plan-dates button { min-height:44px;" in css assert ".week-plan-dates button { min-height:44px;" in css
assert "overflow-x:auto" in css assert "overflow-x:auto" in css
@ -948,7 +1019,7 @@ def test_mobile_week_ahead_review_is_rendered_touch_safe_and_confirmed_explicitl
assert 'id="week-review-duplicates"' in index assert 'id="week-review-duplicates"' in index
assert 'id="confirm-week-plan"' in index assert 'id="confirm-week-plan"' in index
assert "setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value)" in dashboard assert "setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value)" in dashboard
assert "weekWorkflow.confirm()" in dashboard assert "weekFlow.confirm()" in dashboard
assert ".week-review-day.is-overloaded" in css assert ".week-review-day.is-overloaded" in css
assert ".week-review-day button { min-height:44px;" in css assert ".week-review-day button { min-height:44px;" in css
assert ".week-review-mode .mobile-plan-today-nav" in css assert ".week-review-mode .mobile-plan-today-nav" in css