Merge pull request 'Export a saved Week Ahead plan directly from the overview' (#1261) from timmy/1260-export-saved-week-calendar into main
This commit is contained in:
commit
76f89df609
|
|
@ -371,6 +371,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
#retry-week-live { min-height:44px; width:100%; margin-bottom:8px; border-color:#f59e0b; }
|
||||
#confirm-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
|
||||
#edit-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
|
||||
#export-saved-week-calendar { width:100%; min-height:48px; margin:0 0 8px; }
|
||||
#open-week-capacity-import { width:100%; min-height:44px; margin:8px 0 12px; }
|
||||
#open-week-reflow { width:100%; min-height:48px; margin:0 0 12px; border-color:#60a5fa; }
|
||||
.week-reflow-review { box-sizing:border-box; width:100%; margin:0 0 14px; padding:14px; border:1px solid #60a5fa; border-radius:12px; background:#10233d; overflow-x:hidden; }
|
||||
|
|
|
|||
|
|
@ -460,46 +460,33 @@
|
|||
escapeHtml,escapeAttribute:escAttr,todayWork,
|
||||
t:v=>v?latestTodayPlan=v:latestTodayPlan,r:refreshMyWorkView,w:warmTodayOffline,
|
||||
x:currentTodayProgressTarget});
|
||||
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
|
||||
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
|
||||
});
|
||||
const todaySync = createTodaySync({
|
||||
storage: localStorage,
|
||||
getLogin: () => planningOwnerLogin,
|
||||
fetchJson: fetchReviewJson,
|
||||
coordinator: outboxCoordinator,
|
||||
onRemoteIds: ids => {
|
||||
if (!planningOwnerLogin || !todayWork.replace(ids)) return;
|
||||
refreshMyWorkView();
|
||||
warmTodayOffline();
|
||||
},
|
||||
onRemotePlan: plan => {
|
||||
if (!planningOwnerLogin) return;
|
||||
latestTodayPlan = plan;
|
||||
const wc=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekFlow.i,escapeHtml,escapeAttribute:escAttr,
|
||||
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();}});
|
||||
const todaySync=createTodaySync({storage:localStorage,getLogin: () => planningOwnerLogin,fetchJson:fetchReviewJson,
|
||||
coordinator:outboxCoordinator,
|
||||
onRemoteIds:ids=>{if(!planningOwnerLogin||!todayWork.replace(ids))return;refreshMyWorkView();warmTodayOffline();},
|
||||
onRemotePlan:plan=>{
|
||||
if(!planningOwnerLogin)return;
|
||||
latestTodayPlan=plan;
|
||||
weekFlow.promote(plan).finally(() => {
|
||||
if (window.location.hash !== '#/my-work/start-day') return;
|
||||
window.history.replaceState({}, '', '#/my-work/today');
|
||||
openMobileStartDay();
|
||||
});
|
||||
todayWork.replacePlanning({
|
||||
capacity_minutes: plan.capacity_minutes ?? null,
|
||||
estimates: plan.estimates || {},
|
||||
});
|
||||
const s = todayRollover.reviewState(plan);
|
||||
if (!['stale', 'legacy'].includes(s)) {
|
||||
rolloverReviewPlan = null;
|
||||
qs('#plan-today').textContent = 'Plan Today';
|
||||
todayWork.replacePlanning({capacity_minutes:plan.capacity_minutes??null,estimates:plan.estimates||{}});
|
||||
const s=todayRollover.reviewState(plan);
|
||||
if(!['stale','legacy'].includes(s)){
|
||||
rolloverReviewPlan=null;
|
||||
qs('#plan-today').textContent='Plan Today';
|
||||
return;
|
||||
}
|
||||
rolloverReviewPlan = plan;
|
||||
qs('#plan-today').textContent = 'Review new day';
|
||||
qs('#my-work-action-status').textContent = 'Review yesterday’s unfinished work before starting today.';
|
||||
const marker = `stackchain.today-rollover-reviewed.v1.${encodeURIComponent(planningOwnerLogin)}.${todayRollover.localDate()}`;
|
||||
if (!localStorage.getItem(marker)) {
|
||||
localStorage.setItem(marker, '1');
|
||||
setTimeout(() => {
|
||||
if (!workSession.checkpointed()) openPlanToday(qs('#plan-today'));
|
||||
}, 0);
|
||||
rolloverReviewPlan=plan;
|
||||
qs('#plan-today').textContent='Review new day';
|
||||
qs('#my-work-action-status').textContent='Review yesterday’s unfinished work before starting today.';
|
||||
const marker=`stackchain.today-rollover-reviewed.v1.${encodeURIComponent(planningOwnerLogin)}.${todayRollover.localDate()}`;
|
||||
if(!localStorage.getItem(marker)){
|
||||
localStorage.setItem(marker,'1');
|
||||
setTimeout(()=>{if(!workSession.checkpointed())openPlanToday(qs('#plan-today'));},0);
|
||||
}
|
||||
},
|
||||
onStatus: (state, detail = {}) => {
|
||||
|
|
@ -2706,7 +2693,7 @@
|
|||
}
|
||||
planToday.cancel();
|
||||
planningTomorrow = false;
|
||||
weekCalendar.close();
|
||||
wc.close();
|
||||
weekFlow.clear();
|
||||
qs('#tomorrow-conflict-review').hidden = true;
|
||||
qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
|
||||
|
|
@ -7926,7 +7913,7 @@
|
|||
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;
|
||||
}
|
||||
weekCalendar.open(weekPlan.state());
|
||||
wc.open(weekPlan.state());
|
||||
});
|
||||
qs('#plan-today-sheet').addEventListener('click', event => {
|
||||
if (event.target === qs('#plan-today-sheet')) closePlanToday();
|
||||
|
|
|
|||
|
|
@ -560,6 +560,7 @@
|
|||
<div class="week-unplan-receipt" id="week-unplan-receipt" role="status" aria-live="polite" hidden></div>
|
||||
<button id="undo-week-unplan" type="button" hidden>Undo</button>
|
||||
<div class="small week-review-status" id="week-review-status" role="status" aria-live="polite"></div>
|
||||
<button id="export-saved-week-calendar" type="button" hidden>Add saved week to calendar</button>
|
||||
<button id="edit-week-plan" type="button" hidden>Edit week</button>
|
||||
<button id="confirm-week-plan" type="button">Confirm Week Ahead</button>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
function mountWeekCalendarHandoff({qs,getItem,getAvailability=availabilityProvider,escapeHtml,escapeAttribute,onDone,windowObject=root,navigatorObject=root.navigator,
|
||||
documentObject=root.document,urlApi=root.URL,FileCtor=root.File}={}){
|
||||
const handoff=qs('#week-calendar-handoff'),review=qs('#week-review'),daysRoot=qs('#week-calendar-days');
|
||||
let plan=null;
|
||||
let plan=null,focusReturn=null;
|
||||
const selected=()=>new Set(Array.from(daysRoot.querySelectorAll('[data-week-calendar-item]')).filter(input=>input.checked).map(input=>input.value));
|
||||
const starts=()=>Object.fromEntries(Array.from(daysRoot.querySelectorAll('[data-week-calendar-start]')).map(input=>[input.dataset.weekCalendarStart,input.value]));
|
||||
const schedule=()=>buildWeekSchedule(plan,starts(),getItem,selected(),getAvailability?.());
|
||||
|
|
@ -94,10 +94,10 @@
|
|||
}
|
||||
function close({back=false}={}){
|
||||
handoff.hidden=true;
|
||||
if(back){review.hidden=false;qs('#confirm-week-plan').focus?.();}
|
||||
if(back){review.hidden=false;const target=focusReturn||qs('#confirm-week-plan');focusReturn=null;target.focus?.();}
|
||||
}
|
||||
function open(value){
|
||||
plan=value;review.hidden=true;handoff.hidden=false;
|
||||
function open(value,{returnFocus=null}={}){
|
||||
plan=value;if(returnFocus)focusReturn=returnFocus;review.hidden=true;handoff.hidden=false;
|
||||
const labels=new Map((value.days||[]).map(day=>[day.plan_date,new Intl.DateTimeFormat('en',{weekday:'short',month:'short',day:'numeric',timeZone:'UTC'}).format(new Date(day.plan_date+'T12:00:00Z'))]));
|
||||
daysRoot.innerHTML=(value.days||[]).filter(day=>day.ids?.length).map(day=>'<article class="week-calendar-day"><header><h3>'+escapeHtml(labels.get(day.plan_date)||day.plan_date)+
|
||||
'</h3><label>Start <input type="time" value="09:00" data-week-calendar-start="'+escapeAttribute(day.plan_date)+'"></label></header>'+
|
||||
|
|
@ -108,6 +108,7 @@
|
|||
update();qs('#back-to-week-review-from-calendar').focus?.();return true;
|
||||
}
|
||||
qs('#back-to-week-review-from-calendar').addEventListener('click',()=>close({back:true}));
|
||||
qs('#export-saved-week-calendar')?.addEventListener('click',event=>{focusReturn=event.currentTarget;qs('#confirm-week-plan').click();});
|
||||
qs('#share-week-calendar').addEventListener('click',async()=>{
|
||||
const button=qs('#share-week-calendar'),current=blocks();if(!current.length)return;
|
||||
button.disabled=true;qs('#week-calendar-status').textContent='Preparing calendar blocks…';
|
||||
|
|
|
|||
|
|
@ -936,13 +936,22 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
|||
const confirm=qs('#confirm-week-plan');
|
||||
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.':
|
||||
(pending?'Saving the latest week to your account…':blocker?(
|
||||
const blockerMessage=blocker?(
|
||||
blocker.type==='over-capacity'?blocker.plan_date+' is '+blocker.minutes+' min over capacity. Edit that day before confirming.':
|
||||
blocker.type==='missing-estimate'?'Add an estimate for work on '+blocker.plan_date+' before confirming.':
|
||||
'Add capacity for '+blocker.plan_date+' before confirming.'
|
||||
):'';
|
||||
qs('#week-review-status').textContent=readOnly?'Offline snapshot · viewing only. Retry for live editing.':(overviewing?(pending?'Week Ahead overview · sync pending.':
|
||||
(blockerMessage||'Week Ahead overview · no changes made.')):(value.duplicates.length?'Duplicate work must be moved to one date.':
|
||||
(pending?'Saving the latest week to your account…':blocker?(
|
||||
blockerMessage
|
||||
):'Week Ahead is balanced and ready.')));
|
||||
confirm.hidden=overviewing||readOnly;
|
||||
const exportSaved=qs('#export-saved-week-calendar');
|
||||
if(exportSaved){
|
||||
exportSaved.hidden=!overviewing||!value.days.some(day=>day.ids.length);
|
||||
exportSaved.disabled=pending||!value.can_confirm;
|
||||
}
|
||||
const reflow=qs('#open-week-reflow');if(reflow)reflow.hidden=!overviewing||readOnly||pending||!value.days.some(day=>day.overloaded);
|
||||
const editWeek=qs('#edit-week-plan');if(editWeek){editWeek.hidden=!overviewing||readOnly;editWeek.textContent=nextUp?'Edit week':'Plan Week Ahead';}
|
||||
root.querySelectorAll('[data-week-move]').forEach(button=>button.addEventListener('click',()=>{
|
||||
|
|
@ -1215,6 +1224,7 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
|||
}
|
||||
}
|
||||
const workflow={open,save,advance,confirm,finish,promote,retire,resumePull,flushPull,editWeek,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
|
||||
i:id=>getItem(id)||controller.item?.(id),
|
||||
reviewing:()=>reviewing,overviewing:()=>overviewing,selectedDate:()=>selectedDate,
|
||||
day:()=>reconciliationDay()||(selectedDate?controller.day(selectedDate):(overviewing?{ids:[]}:null)),
|
||||
copy:()=>reconciliation?{title:"Start today's plan",heading:'Unfinished Today + due Week Ahead',available:'Available today',build:'Build combined Today'}:
|
||||
|
|
|
|||
|
|
@ -105,10 +105,11 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
|||
expect(cards).to_have_count(7)
|
||||
expect(cards.first).to_contain_text("Next up")
|
||||
expect(cards.first).to_contain_text("Ship mobile capture")
|
||||
expect(page.locator("#week-review-status")).to_have_text(
|
||||
"Week Ahead overview · no changes made."
|
||||
)
|
||||
expect(page.locator("#week-review-status")).to_contain_text("30 min over capacity")
|
||||
expect(page.locator("#confirm-week-plan")).to_be_hidden()
|
||||
blocked_export = page.locator("#export-saved-week-calendar")
|
||||
expect(blocked_export).to_be_visible()
|
||||
expect(blocked_export).to_be_disabled()
|
||||
availability = page.locator("#open-week-availability")
|
||||
expect(availability).to_be_visible()
|
||||
availability_bounds = availability.bounding_box()
|
||||
|
|
@ -192,6 +193,22 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
|||
expect(overview).to_be_visible()
|
||||
expect(planned_item).to_be_focused()
|
||||
assert len(saved) == writes_before_reflow + 1, "opening and inspecting Week Ahead must not add a write"
|
||||
export_saved = page.locator("#export-saved-week-calendar")
|
||||
expect(export_saved).to_be_visible()
|
||||
expect(export_saved).to_be_enabled()
|
||||
export_bounds = export_saved.bounding_box()
|
||||
assert export_bounds and export_bounds["height"] >= 44
|
||||
writes_before_export = len(saved)
|
||||
export_saved.click()
|
||||
expect(page.locator("#week-calendar-handoff")).to_be_visible()
|
||||
expect(overview).to_be_hidden()
|
||||
expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text(
|
||||
"09:00–09:45 · 45 min"
|
||||
)
|
||||
assert len(saved) == writes_before_export, "exporting a saved week must not add a write"
|
||||
page.locator("#back-to-week-review-from-calendar").click()
|
||||
expect(overview).to_be_visible()
|
||||
expect(export_saved).to_be_focused()
|
||||
for control in (cards.first.locator("[data-week-edit-day]"), edit_week):
|
||||
bounds = control.bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
|
|
|
|||
|
|
@ -151,11 +151,17 @@ def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_co
|
|||
assert 'id="week-calendar-days"' in index
|
||||
assert 'id="share-week-calendar"' in index
|
||||
assert 'id="back-to-week-review-from-calendar"' in index
|
||||
assert 'id="export-saved-week-calendar"' in index
|
||||
assert "Add saved week to calendar" in index
|
||||
assert "Titles, references, dates, and times are included" in index
|
||||
assert "Imported busy time is used only until this page reloads" in index
|
||||
assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
|
||||
assert "weekCalendar.open(weekPlan.state())" in dashboard
|
||||
assert "weekCalendar.close()" in dashboard
|
||||
assert "wc.open(weekPlan.state())" in dashboard
|
||||
assert "getItem:weekFlow.i" in dashboard
|
||||
assert "i:id=>getItem(id)||controller.item?.(id)" in (WEEK_CALENDAR.parent / "week-plan.js").read_text()
|
||||
assert "#export-saved-week-calendar" in WEEK_CALENDAR.read_text()
|
||||
assert "focusReturn=event.currentTarget" in WEEK_CALENDAR.read_text()
|
||||
assert "wc.close()" in dashboard
|
||||
assert "StackchainWeekCalendar.setAvailabilityProvider(workflow.availability)" in (WEEK_CALENDAR.parent / "week-calendar-import.js").read_text()
|
||||
assert "Planning around imported busy time" in WEEK_CALENDAR.read_text()
|
||||
assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user