feat: start Week Ahead days early (Closes #1224)
This commit is contained in:
parent
25597d1f05
commit
f64ce3acc4
|
|
@ -168,8 +168,11 @@ foreground, and midnight lifecycle checks share one delivery flight. A successfu
|
|||
the pending copy, while a revision conflict preserves both the phone plan and fresh server snapshot for
|
||||
review. Unsynced Tomorrow work is never promoted into Today.
|
||||
**Week Ahead** opens as a read-first seven-day mobile overview, so operators can inspect the next planned day,
|
||||
work titles and references, load versus capacity, overloads, and pending sync without staging a change. **Edit day**
|
||||
enters one date and returns to the refreshed overview; **Edit week** starts the continuous planning pass.
|
||||
work titles and references, load versus capacity, overloads, and pending sync without staging a change. When Today
|
||||
is empty and no work session is active, **Start this day early** confirms the next planned date, item count, minutes,
|
||||
and capacity before atomically moving only that day into Today; offline, pending, stale, or non-empty plans remain
|
||||
unchanged. **Edit day** enters one date and returns to the refreshed overview; **Edit week** starts the continuous
|
||||
planning pass.
|
||||
**Plan Week Ahead** continues through seven local dates and now finishes on a mobile review step instead of
|
||||
closing after the seventh save. The review shows planned minutes against each day’s capacity, marks overloads,
|
||||
and flags work assigned to more than one date. Operators can move an item to another date without copying it;
|
||||
|
|
|
|||
|
|
@ -333,6 +333,8 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.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-start-early { display:block; width:100%; min-height:44px; margin-top:12px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
|
||||
.week-start-early:focus-visible { outline:3px solid #bfdbfe; outline-offset:2px; }
|
||||
.week-review-item-copy strong, .week-review-item-copy small { overflow-wrap:anywhere; }
|
||||
.week-review-item-copy small { color:#a9bdd3; }
|
||||
.week-review-move { display:flex; align-items:end; gap:8px; }
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@
|
|||
getItem:weekItem,openItem:openRoutedWork,
|
||||
openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value),
|
||||
escapeHtml,escapeAttribute:escAttr,todayWork,
|
||||
refresh:refreshMyWorkView,warm:warmTodayOffline});
|
||||
t:()=>latestTodayPlan,r:refreshMyWorkView,w:warmTodayOffline});
|
||||
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
|
||||
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
|
||||
});
|
||||
|
|
@ -7881,7 +7881,7 @@
|
|||
try {
|
||||
await weekPlan.keepLocal();
|
||||
qs('#mobile-week-summary').textContent=weekPlan.summary();
|
||||
qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved to your account. Today was not changed.';
|
||||
qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved. Today was not changed.';
|
||||
closePlanToday();
|
||||
} catch(error) {
|
||||
const conflict=weekPlan.conflict();
|
||||
|
|
|
|||
|
|
@ -406,6 +406,17 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
|||
plan_date:due.plan_date,today_revision:todayRevision,
|
||||
})});
|
||||
}
|
||||
async function startEarly(planDate,todayRevision) {
|
||||
if(offlineSnapshot||pending()||conflict()||!Number.isInteger(todayRevision))return false;
|
||||
const day=week.days.find(item=>item.plan_date===planDate&&item.ids.length);
|
||||
if(!day)return false;
|
||||
const promoted=await fetchJson('api/v1/week/start-early',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
||||
promotion_id:`early-${day.plan_date}-r${week.revision}`,week_revision:week.revision,
|
||||
plan_date:day.plan_date,today_revision:todayRevision,
|
||||
})});
|
||||
await load();
|
||||
return promoted;
|
||||
}
|
||||
async function reconcilePromotion(preserved,selection) {
|
||||
if(offlineSnapshot||pending()||conflict()) return false;
|
||||
const due=(preserved?.week?.days||week.days).find(item=>item.plan_date<=localDate()&&item.ids.length);
|
||||
|
|
@ -425,12 +436,13 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
|||
return label+(pending()?' · sync pending':'');
|
||||
}
|
||||
return {adopt,state,dates,day,pass,load,saveDay,stageDay,stageCapacities,review,move,placement,place,pending,flush,conflict,chooseDay,saveMerged,
|
||||
keepLocal,useRemote,promote,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
|
||||
keepLocal,useRemote,promote,startEarly,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
|
||||
item:id=>pendingItems[id]||confirmedItems[id]||null,
|
||||
offline:()=>offlineSnapshot};
|
||||
}
|
||||
function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
|
||||
todayWork,refresh,warm}={}) {
|
||||
todayWork,refresh:r=()=>{},warm:w=()=>{},today:t=()=>null,r:refresh=r,w:warm=w,t:getTodayPlan=t,
|
||||
confirmEarly=message=>globalThis.confirm?.(message)??false}={}) {
|
||||
let selectedDate=null;
|
||||
let reviewing=false;
|
||||
let overviewing=false;
|
||||
|
|
@ -521,7 +533,10 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
|||
function renderReview() {
|
||||
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
|
||||
const readOnly=Boolean(controller.offline?.());
|
||||
const pending=Boolean(controller.pending?.());
|
||||
const nextUp=overviewing?value.days.find(day=>day.ids.length):null;
|
||||
const today=getTodayPlan?.();
|
||||
const canStartEarly=Boolean(nextUp&&!readOnly&&!pending&&Number.isInteger(today?.revision)&&!today.ids?.length);
|
||||
const destinations=value.days.map(day=>'<option value="'+escapeAttribute(day.plan_date)+'">'+escapeHtml(day.label)+'</option>').join('');
|
||||
const itemMarkup=(id,day)=>{
|
||||
const item=getItem(id)||controller.item?.(id),estimate=Number(day.estimates?.[id])||0;
|
||||
|
|
@ -540,14 +555,14 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
|||
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+move(id)+'</li>').join('')+'</ul>':
|
||||
'<p class="small muted">Nothing planned.</p>';
|
||||
const edit=readOnly?'':'<button type="button" data-week-edit-day="'+escapeAttribute(day.plan_date)+'" aria-label="Edit '+escapeAttribute(day.label)+'">Edit day</button>';
|
||||
const start=day===nextUp&&canStartEarly?'<button type="button" class="week-start-early" data-week-start-early="'+escapeAttribute(day.plan_date)+'">Start this day early</button>':'';
|
||||
return '<article class="week-review-day'+(day.overloaded?' is-overloaded':'')+(day===nextUp?' is-next-up':'')+'"><header><h3>'+escapeHtml(day.label)+(day===nextUp?' <span class="week-next-up">Next up</span>':'')+'</h3>'+edit+'</header><p class="week-review-load">'+
|
||||
escapeHtml(load)+'</p>'+items+'</article>';
|
||||
escapeHtml(load)+'</p>'+items+start+'</article>';
|
||||
}).join('');
|
||||
duplicates.hidden=!value.duplicates.length;
|
||||
duplicates.innerHTML=value.duplicates.length?'<strong>Choose one date for duplicated work before confirming.</strong><ul>'+value.duplicates.map(item=>
|
||||
'<li><code>'+escapeHtml(item.id)+'</code> · '+escapeHtml(item.dates.join(', '))+'</li>').join('')+'</ul>':'';
|
||||
const confirm=qs('#confirm-week-plan');
|
||||
const pending=Boolean(controller.pending?.());
|
||||
confirm.disabled=!value.can_confirm||pending;
|
||||
const blocker=value.blockers?.[0];
|
||||
qs('#week-review-status').textContent=readOnly?'Offline snapshot · viewing only. Retry for live editing.':(overviewing?'Week Ahead overview · '+(pending?'sync pending.':'no changes made.'):(value.duplicates.length?'Duplicate work must be moved to one date.':
|
||||
|
|
@ -569,6 +584,26 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
|||
const id=button.dataset.weekOpenItem,item=getItem(id)||controller.item?.(id);
|
||||
if(item)openItem?.(item,event.currentTarget);
|
||||
}));
|
||||
root.querySelectorAll('[data-week-start-early]').forEach(button=>button.addEventListener('click',async event=>{
|
||||
const day=value.days.find(item=>item.plan_date===event.currentTarget.dataset.weekStartEarly);
|
||||
const current=getTodayPlan?.();
|
||||
if(!day||!Number.isInteger(current?.revision)||current.ids?.length)return;
|
||||
const capacity=Number(day.capacity_minutes)||0;
|
||||
const detail=day.ids.length+' item'+(day.ids.length===1?'':'s')+' · '+day.planned_minutes+(capacity?' of '+capacity:'')+' min';
|
||||
if(!confirmEarly('Start '+day.label+' early?\n\n'+detail+' will move into Today.'))return;
|
||||
event.currentTarget.disabled=true;
|
||||
try{
|
||||
const promoted=await controller.startEarly(day.plan_date,current.revision);
|
||||
if(!promoted)return;
|
||||
todayWork.replace(promoted.ids);todayWork.replacePlanning(promoted);refresh();warm();
|
||||
qs('#mobile-week-summary').textContent=controller.summary();
|
||||
qs('#my-work-action-status').textContent=day.label+' is now Today.';
|
||||
finish();
|
||||
}catch(error){
|
||||
qs('#my-work-action-status').textContent=(error.message||'Week Ahead changed before it could start.')+' Reopen Week Ahead and review.';
|
||||
event.currentTarget.disabled=false;
|
||||
}
|
||||
}));
|
||||
root.querySelectorAll('[data-week-edit-day]').forEach(button=>button.addEventListener('click',()=>editDay(button.dataset.weekEditDay)));
|
||||
renderPass();
|
||||
}
|
||||
|
|
|
|||
27
src/main.py
27
src/main.py
|
|
@ -1468,7 +1468,7 @@ async def require_operator_session(request: Request, call_next):
|
|||
async def prevent_live_api_caching(request, call_next):
|
||||
response = await call_next(request)
|
||||
path = dashboard_auth.application_path(request)
|
||||
if path in {"/api/v1/context", "/api/v1/background-identity", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues", "/api/v1/search", "/api/v1/work-route", "/api/v1/today", "/api/v1/tomorrow", "/api/v1/tomorrow/promote", "/api/v1/week", "/api/v1/week/promote", "/api/v1/week/reconcile", "/api/v1/today/session", "/api/v1/later", "/api/v1/saved-searches", "/api/v1/completed-filed-reviews", "/api/v1/security-events", "/api/v1/push-subscription"} or path.startswith("/api/v1/work/") or (
|
||||
if path in {"/api/v1/context", "/api/v1/background-identity", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues", "/api/v1/search", "/api/v1/work-route", "/api/v1/today", "/api/v1/tomorrow", "/api/v1/tomorrow/promote", "/api/v1/week", "/api/v1/week/promote", "/api/v1/week/start-early", "/api/v1/week/reconcile", "/api/v1/today/session", "/api/v1/later", "/api/v1/saved-searches", "/api/v1/completed-filed-reviews", "/api/v1/security-events", "/api/v1/push-subscription"} or path.startswith("/api/v1/work/") or (
|
||||
path.startswith("/api/v1/repos/")
|
||||
and path.endswith("/review")
|
||||
) or path.startswith("/api/v1/notifications") or (
|
||||
|
|
@ -2857,6 +2857,31 @@ async def promote_week_plan(payload: WeekPromotion):
|
|||
)
|
||||
|
||||
|
||||
@app.post("/api/v1/week/start-early")
|
||||
async def start_week_day_early(payload: WeekPromotion):
|
||||
login = await _confirmed_login()
|
||||
try:
|
||||
return await asyncio.to_thread(
|
||||
_today_store().promote_week, login, **payload.model_dump(), allow_future=True
|
||||
)
|
||||
except WeekPlanConflict as error:
|
||||
raise HTTPException(
|
||||
status_code=409, detail={"code": "week_changed", "snapshot": error.snapshot}
|
||||
)
|
||||
except TodayPromotionConflict as error:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={"code": "week_today_in_progress", "today": error.today, "week": error.week},
|
||||
)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=422, detail=str(error))
|
||||
except (OSError, sqlite3.Error, PrivateStateEncryptionError):
|
||||
raise HTTPException(
|
||||
status_code=503, detail="Starting Week Ahead early is unavailable",
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/v1/week/reconcile")
|
||||
async def reconcile_week_plan(payload: WeekReconciliation):
|
||||
login = await _confirmed_login()
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ class TodayStore:
|
|||
|
||||
def promote_week(
|
||||
self, login: str, *, promotion_id: str, week_revision: int,
|
||||
plan_date: str, today_revision: int,
|
||||
plan_date: str, today_revision: int, allow_future: bool = False,
|
||||
) -> dict:
|
||||
login = self._normalize_login(login)
|
||||
if not isinstance(promotion_id, str) or not promotion_id.strip() or len(promotion_id) > 100:
|
||||
|
|
@ -599,7 +599,7 @@ class TodayStore:
|
|||
if day is None:
|
||||
raise WeekPlanConflict(week)
|
||||
local_date = datetime.fromtimestamp(self.clock(), ZoneInfo(week["timezone"])).date().isoformat()
|
||||
if local_date < plan_date:
|
||||
if local_date < plan_date and not allow_future:
|
||||
raise TomorrowPlanNotDue(plan_date)
|
||||
today_row = connection.execute(
|
||||
"SELECT revision, ids, capacity_minutes, estimates, plan_date, timezone "
|
||||
|
|
|
|||
|
|
@ -165,6 +165,49 @@ def test_week_promotion_moves_only_due_date_to_today_exactly_once(tmp_path):
|
|||
assert remaining["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
def test_week_early_start_moves_a_future_day_to_empty_today_exactly_once(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
encryption_key=b"e" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
promoted = store.promote_week(
|
||||
"timmy", promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"], allow_future=True,
|
||||
)
|
||||
replay = store.promote_week(
|
||||
"timmy", promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"], allow_future=True,
|
||||
)
|
||||
|
||||
assert replay == promoted
|
||||
assert promoted["ids"] == sample_days()[0]["ids"]
|
||||
assert promoted["plan_date"] == "2026-08-21"
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
def test_normal_week_promotion_still_rejects_a_future_day_without_writes(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
encryption_key=b"e" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
with pytest.raises(main.TomorrowPlanNotDue):
|
||||
store.promote_week(
|
||||
"timmy", promotion_id="normal-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"],
|
||||
)
|
||||
|
||||
assert store.get("timmy") == today
|
||||
assert store.get_week("timmy") == week
|
||||
|
||||
|
||||
def test_week_promotion_preserves_nonempty_today_and_the_due_week(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
|
|
@ -261,6 +304,29 @@ async def test_week_promotion_api_returns_both_preserved_plans_for_review(monkey
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_week_early_start_api_promotes_a_future_day(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
return {"login": "timmy"}
|
||||
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3", encryption_key=b"a" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "_today_store", lambda: store)
|
||||
|
||||
result = await main.start_week_day_early(main.WeekPromotion(
|
||||
promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"],
|
||||
))
|
||||
|
||||
assert result["ids"] == sample_days()[0]["ids"]
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_week_reconciliation_api_returns_atomic_today_and_remaining_week(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
|
|
|
|||
|
|
@ -66,6 +66,37 @@ console.log(JSON.stringify({dates,saved,requests}));
|
|||
assert result["requests"][1]["body"]["days"][2]["ids"] == ["issue:r:3:"]
|
||||
|
||||
|
||||
def test_week_controller_starts_a_selected_future_day_early_and_refreshes_the_week():
|
||||
result = run_controller("""
|
||||
const requests=[];
|
||||
let loadCount=0;
|
||||
const day={plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}};
|
||||
const fetchJson=async(url,options={})=>{
|
||||
requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
|
||||
if(url==='api/v1/week/start-early')return {revision:3,...day,timezone:'UTC'};
|
||||
loadCount+=1;
|
||||
return loadCount===1?{revision:4,timezone:'UTC',days:[day]}:{revision:5,timezone:'UTC',days:[]};
|
||||
};
|
||||
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
await week.load();
|
||||
const promoted=await week.startEarly('2026-08-21',2);
|
||||
console.log(JSON.stringify({promoted,requests,state:week.state()}));
|
||||
""")
|
||||
|
||||
assert result["promoted"]["ids"] == ["issue:r:1:"]
|
||||
assert result["requests"][1] == {
|
||||
"url": "api/v1/week/start-early",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"promotion_id": "early-2026-08-21-r4",
|
||||
"week_revision": 4,
|
||||
"plan_date": "2026-08-21",
|
||||
"today_revision": 2,
|
||||
},
|
||||
}
|
||||
assert result["state"]["days"] == []
|
||||
|
||||
|
||||
def test_week_controller_advances_through_one_continuous_seven_day_pass():
|
||||
result = run_controller("""
|
||||
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
|
|
@ -260,6 +291,39 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
|
|||
assert result["editWeekLabel"] == "Edit week"
|
||||
|
||||
|
||||
def test_week_overview_confirms_and_starts_the_next_day_early_from_empty_today():
|
||||
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 root=makeElement();
|
||||
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.startButtons=[...value.matchAll(/data-week-start-early=\"([^\"]+)/g)].map(match=>({dataset:{weekStartEarly:match[1]},listeners:{},disabled:false,addEventListener(name,listener){this.listeners[name]=listener;}}));},get(){return this.value||'';}});
|
||||
root.querySelectorAll=selector=>selector==='[data-week-start-early]'?root.startButtons||[]:[];
|
||||
elements.set('#week-review-days',root);
|
||||
elements.set('#week-plan-dates',makeElement());
|
||||
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 day={plan_date:'2026-08-21',label:'Friday, Aug 21',ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45},planned_minutes:75,overloaded:false};
|
||||
const review={days:dates.map((date,index)=>index===0?day:{plan_date:date,label:'Day '+(index+1),ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false}),duplicates:[],blockers:[],can_confirm:true};
|
||||
let request=null,confirmation='',replaced=null,refreshed=0;
|
||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(value=>value.plan_date===date),pass:()=>({}),load:async()=>({}),summary:()=> 'Nothing planned',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>{},startEarly:async(date,revision)=>{request={date,revision};return {ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45}};}};
|
||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,today:()=>({revision:7,ids:[]}),confirmEarly:message=>{confirmation=message;return true;},todayWork:{replace:ids=>{replaced=ids;},replacePlanning:()=>{}},refresh:()=>{refreshed+=1;},warm:()=>{}});
|
||||
await workflow.open({disabled:false});
|
||||
const button=root.startButtons[0];
|
||||
await button.listeners.click({currentTarget:button});
|
||||
console.log(JSON.stringify({buttonCount:root.startButtons.length,markup:root.innerHTML,confirmation,request,replaced,refreshed,status:elements.get('#my-work-action-status').textContent}));
|
||||
""")
|
||||
|
||||
assert result["buttonCount"] == 1
|
||||
assert "Start this day early" in result["markup"]
|
||||
assert "Friday, Aug 21" in result["confirmation"]
|
||||
assert "2 items · 75 of 90 min" in result["confirmation"]
|
||||
assert result["request"] == {"date": "2026-08-21", "revision": 7}
|
||||
assert result["replaced"] == ["one", "two"]
|
||||
assert result["refreshed"] == 1
|
||||
assert result["status"] == "Friday, Aug 21 is now Today."
|
||||
|
||||
|
||||
def test_week_overview_opens_known_work_with_the_originating_control_and_leaves_unknown_work_static():
|
||||
result = run_controller("""
|
||||
const createWorkflow=createWeekPlan.Workflow;
|
||||
|
|
@ -325,10 +389,16 @@ def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_wi
|
|||
|
||||
workflow_mount = dashboard.split("const weekFlow=createWeekPlanWorkflow({", 1)[1].split("});", 1)[0]
|
||||
assert "openItem:openRoutedWork" in workflow_mount
|
||||
assert "t:()=>latestTodayPlan" 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
|
||||
assert ".week-start-early" in css
|
||||
start_rule = css.split(".week-start-early", 1)[1].split("}", 1)[0]
|
||||
assert "min-height:44px" in start_rule
|
||||
assert "width:100%" in start_rule
|
||||
|
||||
|
||||
def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data():
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user