feat: open Week Ahead planned work (Closes #1222)
This commit is contained in:
parent
a3861da125
commit
9f5baaef6e
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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.';
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user