Merge pull request 'Make Week Ahead review readable and editable' (#1191) from timmy/1190-readable-editable-week-review into main
This commit is contained in:
commit
67a3285412
|
|
@ -246,6 +246,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.week-plan-dates[hidden] { display:none; }
|
||||
.week-plan-dates button { min-height:44px; min-width:86px; flex:0 0 auto; padding:6px 10px; }
|
||||
.week-plan-dates button[aria-current="date"] { color:#bfdbfe; background:#17365a; border-color:#60a5fa; }
|
||||
.back-to-week-review { width:100%; min-height:44px; margin:4px 0 12px; }
|
||||
.tomorrow-conflict-review { margin-top:14px; }
|
||||
.tomorrow-conflict-plans { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
|
||||
.tomorrow-conflict-plans > section { min-width:0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
|
||||
|
|
@ -299,10 +300,16 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.week-review-days { display:grid; gap:12px; }
|
||||
.week-review-day { padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
|
||||
.week-review-day.is-overloaded { border-color:#f59e0b; background:#2a1c12; }
|
||||
.week-review-day header { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||||
.week-review-day header button { flex:0 0 auto; min-height:44px; }
|
||||
.week-review-day h3, .week-review-day p { margin:0 0 8px; }
|
||||
.week-review-load { color:#bfdbfe; font-weight:700; }
|
||||
.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-item-copy { display:grid; min-width:0; gap:3px; }
|
||||
.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; }
|
||||
.week-review-day label { display:grid; gap:4px; font-size:12px; }
|
||||
.week-review-day select, .week-review-day button { min-height:44px; }
|
||||
.week-review-day button { min-height:44px; }
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@
|
|||
getLogin: () => planningOwnerLogin,
|
||||
});
|
||||
const weekWorkflow=createWeekPlanWorkflow({controller:weekPlan,qs,getLogin:()=>planningOwnerLogin,
|
||||
getItem:identity=>[...todayMyWork,...activeMyWork].find(item=>todayWork.identity(item)===identity),
|
||||
openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value),
|
||||
escapeHtml,escapeAttribute:escAttr,todayWork,
|
||||
refresh:refreshMyWorkView,warm:warmTodayOffline});
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@
|
|||
</div>
|
||||
<p class="week-plan-progress" id="week-plan-progress" aria-live="polite" hidden></p>
|
||||
<nav class="week-plan-dates" id="week-plan-dates" aria-label="Week Ahead dates" hidden></nav>
|
||||
<button class="back-to-week-review" id="back-to-week-review" type="button" hidden>Back to week review</button>
|
||||
<section class="tomorrow-conflict-review" id="tomorrow-conflict-review" aria-labelledby="tomorrow-conflict-title" hidden>
|
||||
<div class="small">Cross-device change</div>
|
||||
<h3 id="tomorrow-conflict-title">Choose which Tomorrow plan to keep</h3>
|
||||
|
|
|
|||
|
|
@ -291,10 +291,11 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) {
|
|||
return {adopt,state,dates,day,pass,load,saveDay,stageDay,review,move,pending,flush,conflict,chooseDay,saveMerged,
|
||||
keepLocal,useRemote,promote,summary};
|
||||
}
|
||||
function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
|
||||
function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
|
||||
todayWork,refresh,warm}={}) {
|
||||
let selectedDate=null;
|
||||
let reviewing=false;
|
||||
let editingFromReview=false;
|
||||
let blockedReviewOpen=false;
|
||||
function renderPass() {
|
||||
const progress=qs('#week-plan-progress'),save=qs('#save-today-plan');
|
||||
|
|
@ -308,7 +309,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
const current=controller.pass(selectedDate);
|
||||
progress.hidden=false;
|
||||
progress.textContent=`Day ${current.position} of ${current.total} · ${current.planned} planned`;
|
||||
save.textContent=current.last?'Save week':'Save & next';
|
||||
save.textContent=editingFromReview?'Save & review':(current.last?'Save week':'Save & next');
|
||||
}
|
||||
function conflictDetail(day) {
|
||||
const estimates=day.estimates||{},ids=day.ids||[];
|
||||
|
|
@ -357,13 +358,21 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
function renderReview() {
|
||||
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
|
||||
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),estimate=Number(day.estimates?.[id])||0;
|
||||
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 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>';
|
||||
};
|
||||
root.innerHTML=value.days.map(day=>{
|
||||
const capacity=Number(day.capacity_minutes)||0;
|
||||
const load=day.planned_minutes+(capacity?' of '+capacity:'')+' min'+(day.overloaded?' · over capacity':'');
|
||||
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li><code>'+escapeHtml(id)+'</code><label>Move to <select data-week-move-destination="'+
|
||||
escapeAttribute(id)+'">'+destinations+'</select></label><button type="button" data-week-move="'+escapeAttribute(id)+'">Move</button></li>').join('')+'</ul>':
|
||||
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+'<div class="week-review-move"><label>Move to <select data-week-move-destination="'+
|
||||
escapeAttribute(id)+'">'+destinations+'</select></label><button type="button" data-week-move="'+escapeAttribute(id)+'">Move</button></div></li>').join('')+'</ul>':
|
||||
'<p class="small muted">Nothing planned.</p>';
|
||||
return '<article class="week-review-day'+(day.overloaded?' is-overloaded':'')+'"><h3>'+escapeHtml(day.label)+'</h3><p class="week-review-load">'+
|
||||
return '<article class="week-review-day'+(day.overloaded?' is-overloaded':'')+'"><header><h3>'+escapeHtml(day.label)+'</h3><button type="button" data-week-edit-day="'+
|
||||
escapeAttribute(day.plan_date)+'" aria-label="Edit '+escapeAttribute(day.label)+'">Edit day</button></header><p class="week-review-load">'+
|
||||
escapeHtml(load)+'</p>'+items+'</article>';
|
||||
}).join('');
|
||||
duplicates.hidden=!value.duplicates.length;
|
||||
|
|
@ -381,10 +390,31 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
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.';});
|
||||
}));
|
||||
root.querySelectorAll('[data-week-edit-day]').forEach(button=>button.addEventListener('click',()=>editDay(button.dataset.weekEditDay)));
|
||||
renderPass();
|
||||
}
|
||||
function editDay(planDate) {
|
||||
if(!controller.dates().some(item=>item.date===planDate))return false;
|
||||
reviewing=false;editingFromReview=true;selectedDate=planDate;setReviewMode(false);
|
||||
qs('#week-review').hidden=true;
|
||||
const back=qs('#back-to-week-review');if(back)back.hidden=false;
|
||||
renderDates();openPlanner(null,false);
|
||||
qs('#week-plan-progress')?.focus?.();
|
||||
return true;
|
||||
}
|
||||
function returnToReview() {
|
||||
if(!editingFromReview)return false;
|
||||
const editedDate=selectedDate;
|
||||
editingFromReview=false;reviewing=true;selectedDate=null;setReviewMode(true);
|
||||
qs('#week-review').hidden=false;
|
||||
const back=qs('#back-to-week-review');if(back)back.hidden=true;
|
||||
renderDates();renderReview();openPlanner(null,false);
|
||||
qs('#week-review-days').querySelector(`[data-week-edit-day="${editedDate}"]`)?.focus?.();
|
||||
return true;
|
||||
}
|
||||
async function open(trigger) {
|
||||
trigger.disabled=true;reviewing=false;setReviewMode(false);const reviewRoot=qs('#week-review');if(reviewRoot)reviewRoot.hidden=true;
|
||||
trigger.disabled=true;reviewing=false;editingFromReview=false;setReviewMode(false);const reviewRoot=qs('#week-review');if(reviewRoot)reviewRoot.hidden=true;
|
||||
const back=qs('#back-to-week-review');if(back)back.hidden=true;
|
||||
selectedDate=controller.dates()[0].date;renderDates();openPlanner(trigger);
|
||||
qs('#mobile-week-summary').textContent='Loading Week Ahead…';
|
||||
try{await controller.load();qs('#mobile-week-summary').textContent=controller.summary();renderDates();openPlanner(null,false);return true;}
|
||||
|
|
@ -408,6 +438,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
return true;
|
||||
}
|
||||
function advance() {
|
||||
if(editingFromReview)return returnToReview();
|
||||
const current=selectedDate&&controller.pass(selectedDate);
|
||||
if(current?.last){
|
||||
reviewing=true;selectedDate=null;setReviewMode(true);qs('#week-review').hidden=false;
|
||||
|
|
@ -424,6 +455,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
if(!reviewing||!value.can_confirm||controller.pending()) return false;
|
||||
reviewing=false;setReviewMode(false);qs('#week-review').hidden=true;return true;
|
||||
}
|
||||
qs('#back-to-week-review')?.addEventListener('click',returnToReview);
|
||||
async function promote(plan){
|
||||
try{await controller.load();const promoted=await controller.promote(plan.revision);if(!promoted)return false;blockedReviewOpen=false;todayWork.replace(promoted.ids);todayWork.replacePlanning({capacity_minutes:promoted.capacity_minutes??null,estimates:promoted.estimates||{}});refresh();warm();qs('#mobile-week-summary').textContent=controller.summary();qs('#my-work-action-status').textContent='Your saved Week Ahead plan is now Today.';return true;}
|
||||
catch(error){
|
||||
|
|
@ -440,7 +472,8 @@ function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,setReviewMod
|
|||
reviewing:()=>reviewing,selectedDate:()=>selectedDate,
|
||||
day:()=>selectedDate?controller.day(selectedDate):null,
|
||||
copy:()=>selectedDate?{title:'Plan Week Ahead',heading:selectedDate+', in order',available:'Available this day',build:'Build this day'}:null,
|
||||
clear(){selectedDate=null;reviewing=false;setReviewMode(false);qs('#week-review').hidden=true;renderDates();}};
|
||||
clear(){selectedDate=null;reviewing=false;editingFromReview=false;setReviewMode(false);qs('#week-review').hidden=true;
|
||||
const back=qs('#back-to-week-review');if(back)back.hidden=true;renderDates();}};
|
||||
}
|
||||
if(typeof module!=='undefined'&&module.exports){
|
||||
module.exports=createWeekPlan;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
|||
}))
|
||||
return
|
||||
route.fulfill(status=200, content_type="application/json", body=json.dumps({
|
||||
"revision": 0, "timezone": None, "days": [],
|
||||
"revision": 0, "timezone": None, "days": [{
|
||||
"plan_date": (date.today() + timedelta(days=1)).isoformat(),
|
||||
"ids": ["issue:acme/mobile:41:"], "capacity_minutes": 60,
|
||||
"estimates": {"issue:acme/mobile:41:": 30},
|
||||
}],
|
||||
}))
|
||||
|
||||
page.route("**/api/v1/week", week_route)
|
||||
|
|
@ -67,18 +71,32 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
|||
assert bounds and bounds["height"] >= 44
|
||||
dates.nth(2).click()
|
||||
expect(dates.nth(2)).to_have_attribute("aria-current", "date")
|
||||
expect(page.locator("#week-plan-progress")).to_have_text("Day 3 of 7 · 0 planned")
|
||||
expect(page.locator("#week-plan-progress")).to_have_text("Day 3 of 7 · 1 planned")
|
||||
expect(page.locator("#save-today-plan")).to_have_text("Save & next")
|
||||
page.locator("#save-today-plan").click()
|
||||
expect(page.locator("#plan-today-sheet")).to_be_visible()
|
||||
expect(dates.nth(3)).to_have_attribute("aria-current", "date")
|
||||
expect(page.locator("#week-plan-progress")).to_have_text("Day 4 of 7 · 0 planned")
|
||||
expect(page.locator("#week-plan-progress")).to_have_text("Day 4 of 7 · 1 planned")
|
||||
dates.nth(6).click()
|
||||
expect(page.locator("#save-today-plan")).to_have_text("Save week")
|
||||
page.locator("#save-today-plan").click()
|
||||
expect(page.locator("#plan-today-sheet")).to_be_visible()
|
||||
expect(page.locator("#week-review")).to_be_visible()
|
||||
expect(page.locator("#week-review-days .week-review-day")).to_have_count(7)
|
||||
first_day = page.locator("#week-review-days .week-review-day").first
|
||||
expect(first_day).to_contain_text("Ship mobile capture")
|
||||
expect(first_day).to_contain_text("acme/mobile #41 · Issue · 30 min")
|
||||
expect(first_day.locator("code")).to_have_count(0)
|
||||
edit = first_day.locator("[data-week-edit-day]")
|
||||
bounds = edit.bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
edit.click()
|
||||
expect(page.locator("#week-review")).to_be_hidden()
|
||||
expect(page.locator("#back-to-week-review")).to_be_visible()
|
||||
expect(page.locator("#save-today-plan")).to_have_text("Save & review")
|
||||
page.locator("#back-to-week-review").click()
|
||||
expect(page.locator("#week-review")).to_be_visible()
|
||||
expect(first_day).to_contain_text("Ship mobile capture")
|
||||
page.wait_for_function("() => !document.querySelector('#confirm-week-plan').disabled")
|
||||
confirm = page.locator("#confirm-week-plan")
|
||||
bounds = confirm.bounding_box()
|
||||
|
|
@ -86,7 +104,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
|||
confirm.click()
|
||||
expect(page.locator("#plan-today-sheet")).to_be_hidden()
|
||||
page.wait_for_function("() => document.querySelector('#mobile-week-summary').textContent !== 'Loading Week Ahead…'")
|
||||
assert saved and len(saved[-1]["days"]) == 2
|
||||
assert saved and len(saved[-1]["days"]) == 3
|
||||
assert [day["plan_date"] for day in saved[-1]["days"]] == sorted(
|
||||
day["plan_date"] for day in saved[-1]["days"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -173,6 +173,70 @@ console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode,
|
|||
assert "shared" in result["duplicates"]
|
||||
|
||||
|
||||
def test_week_review_names_work_and_can_edit_a_day_then_return_to_review():
|
||||
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:'',dataset:{},listeners:{},
|
||||
addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},
|
||||
querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
|
||||
const dateRoot=makeElement();
|
||||
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[...value.matchAll(/data-week-plan-date=\"([^\"]+)/g)].map(match=>({
|
||||
dataset:{weekPlanDate:match[1]},addEventListener:()=>{},scrollIntoView:()=>{},focus:()=>{}
|
||||
}));},get(){return this.value||'';}});
|
||||
dateRoot.querySelectorAll=()=>dateRoot.buttons||[];
|
||||
dateRoot.querySelector=selector=>(dateRoot.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null;
|
||||
elements.set('#week-plan-dates',dateRoot);
|
||||
const reviewDays=makeElement();
|
||||
Object.defineProperty(reviewDays,'innerHTML',{set(value){
|
||||
this.value=value;this.editButtons=[...value.matchAll(/data-week-edit-day=\"([^\"]+)/g)].map(match=>{
|
||||
const button=makeElement();button.dataset.weekEditDay=match[1];return button;
|
||||
});
|
||||
},get(){return this.value||'';}});
|
||||
reviewDays.querySelectorAll=selector=>selector==='[data-week-edit-day]'?reviewDays.editButtons||[]:[];
|
||||
reviewDays.querySelector=selector=>(reviewDays.editButtons||[]).find(button=>selector.includes(button.dataset.weekEditDay))||null;
|
||||
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']) {
|
||||
if(!elements.has(selector))elements.set(selector,makeElement());
|
||||
}
|
||||
let reviewMode=false;
|
||||
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
|
||||
ids:index===0?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60,
|
||||
estimates:index===0?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===0?30:0,overloaded:false})),
|
||||
duplicates:[],can_confirm:true};
|
||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
|
||||
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};},
|
||||
load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,move:()=>true,flush:async()=>({})};
|
||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
|
||||
getItem:id=>id==='issue:stackchain/dashboard:42:'?{kind:'issue',title:'Fix mobile sign in',repository:'stackchain/dashboard',number:42}:null,
|
||||
openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||
await workflow.open({disabled:false});
|
||||
for(let index=0;index<7;index++)workflow.advance();
|
||||
const markup=reviewDays.innerHTML;
|
||||
const edit=reviewDays.editButtons[0];edit.listeners.click();
|
||||
const whileEditing={selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode,
|
||||
reviewHidden:elements.get('#week-review').hidden,backHidden:elements.get('#back-to-week-review').hidden};
|
||||
elements.get('#back-to-week-review').listeners.click();
|
||||
console.log(JSON.stringify({markup,whileEditing,after:{selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode,
|
||||
reviewHidden:elements.get('#week-review').hidden,editFocused:reviewDays.editButtons[0].focused}}));
|
||||
""")
|
||||
|
||||
assert "Fix mobile sign in" in result["markup"]
|
||||
assert "stackchain/dashboard #42 · Issue · 30 min" in result["markup"]
|
||||
assert "<code>issue:stackchain/dashboard:42:</code>" not in result["markup"]
|
||||
assert result["whileEditing"] == {
|
||||
"selected": "2026-08-21", "reviewing": False, "reviewMode": False,
|
||||
"reviewHidden": True, "backHidden": False,
|
||||
}
|
||||
assert result["after"] == {
|
||||
"selected": None, "reviewing": True, "reviewMode": True,
|
||||
"reviewHidden": False, "editFocused": True,
|
||||
}
|
||||
|
||||
|
||||
def test_week_controller_preserves_both_versions_when_another_device_changes_the_week():
|
||||
result = run_controller("""
|
||||
let calls=0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user