feat: plan Following changes and continue review (Closes #1326)
This commit is contained in:
parent
0f36d3077b
commit
07836f7176
|
|
@ -1056,8 +1056,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
#keep-following-status { display:block; min-height:20px; overflow-wrap:anywhere; }
|
||||
.search-preview-actions button, .search-preview-actions a { min-height:44px; box-sizing:border-box; display:flex; align-items:center; justify-content:center; }
|
||||
.search-preview-primary-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
||||
.search-preview-primary-actions.following-disposition-mode { grid-template-columns:repeat(3,minmax(0,1fr)); }
|
||||
@media (max-width:420px) {
|
||||
.search-preview-primary-actions { grid-template-columns:1fr; }
|
||||
.search-preview-primary-actions.following-disposition-mode { grid-template-columns:1fr; }
|
||||
}
|
||||
@media (max-width:600px) {
|
||||
.search-preview-panel { width:100%; border-left:0; padding:14px; padding-top:max(12px,env(safe-area-inset-top)); }
|
||||
|
|
|
|||
|
|
@ -5716,6 +5716,7 @@
|
|||
const detail = state.detail;
|
||||
if (!detail) return;
|
||||
searchPreviewDetail = detail;
|
||||
|
||||
shareButton.disabled = state.status === 'sharing';
|
||||
qs('#search-preview-key').textContent = detail.repository + ' #' + detail.number;
|
||||
qs('#search-preview-title').textContent = detail.title || 'Untitled work item';
|
||||
|
|
@ -5747,6 +5748,7 @@
|
|||
(detail.assigned_to_me ? 'Start in Today' : 'Assign & start');
|
||||
startButton.disabled = state.status === 'claiming' || state.status === 'reopening';
|
||||
renderSearchPreviewWatch(detail, state, watchButton);
|
||||
followingQueue.preview(state);
|
||||
const shareStatus = {
|
||||
sharing:'Opening share options…', shared:'Search result shared.', copied:'Search result link copied.',
|
||||
'share-canceled':'Share canceled.', 'share-error':'Could not share this result. Try again.',
|
||||
|
|
@ -5817,7 +5819,7 @@
|
|||
createSearchWeekPlanUI({planner:searchWeekPlan,document,window,escapeHtml,escapeAttribute:escAttr,
|
||||
getDetail:()=>searchPreviewDetail,
|
||||
announce:message=>{qs('#search-preview-status').textContent=message;},
|
||||
onPlanned:item=>{searchPreviewDetail={...item,assigned_to_me:true,claimable:false};},
|
||||
onPlanned:item=>(searchPreviewDetail=item,item.following&&next()),
|
||||
});
|
||||
searchBatchPlanning = mountSearchBatchPlanning(
|
||||
document, createBatchFindWork, todayWork, ()=>planningOwnerLogin, fetchReviewJson,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,33 @@
|
|||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = exports.createFollowing;
|
||||
module.exports.attachFollowing = exports.attachFollowing;
|
||||
module.exports.planningDisposition = exports.planningDisposition;
|
||||
} else {
|
||||
root.createFollowing = exports.createFollowing;
|
||||
root.attachFollowing = exports.attachFollowing;
|
||||
root.followingPlanningDisposition = exports.planningDisposition;
|
||||
}
|
||||
})(typeof self !== 'undefined' ? self : this, function () {
|
||||
function planningDisposition(detail, controls) {
|
||||
const active = detail?.following === true && detail.kind === 'issue' && detail.state === 'open' &&
|
||||
Boolean(detail.claimable || detail.assigned_to_me);
|
||||
const result = active ? {active:true,today:'Add to Today & next',later:'Later & next',week:'Week Ahead & next'} : {active:false};
|
||||
if (!controls && globalThis.document) {
|
||||
const query = selector => globalThis.document.querySelector(selector);
|
||||
controls = {root:query('.search-preview-primary-actions'),claim:query('#claim-search-result'),
|
||||
today:query('#queue-search-result'),later:query('#defer-search-result'),week:query('#plan-search-result'),
|
||||
start:query('#start-search-result'),watch:query('#watch-search-result')};
|
||||
}
|
||||
controls?.root?.classList.toggle('following-disposition-mode', active);
|
||||
if (active && controls) {
|
||||
controls.claim.hidden = controls.start.hidden = controls.watch.hidden = true;
|
||||
controls.today.textContent = result.today;
|
||||
controls.later.textContent = result.later;
|
||||
controls.week.textContent = result.week;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function createFollowing(options) {
|
||||
let generation = 0;
|
||||
let snapshot = {revision:0, items:[]};
|
||||
|
|
@ -248,7 +270,8 @@
|
|||
keepForLater:feature.keepForLater,
|
||||
retire:feature.retire,
|
||||
preview(state) {
|
||||
keepButton.hidden = state?.item?.following !== true;
|
||||
const disposition = planningDisposition(state?.detail || state?.item);
|
||||
keepButton.hidden = state?.item?.following !== true || disposition.active;
|
||||
keepButton.disabled = false;
|
||||
if (keepButton.hidden || state.status === 'loading') query('#keep-following-status').textContent = '';
|
||||
},
|
||||
|
|
@ -265,5 +288,5 @@
|
|||
};
|
||||
}
|
||||
|
||||
return {createFollowing, attachFollowing};
|
||||
return {createFollowing, attachFollowing, planningDisposition};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function createSearchWeekPlan({week,claim,accept=item=>item,identity,maxItems=5}
|
|||
await week.flush();
|
||||
return {status:'planned',date:options.date,estimate,item};
|
||||
} catch(_error) {
|
||||
return {status:'planned-pending',date:options.date,estimate};
|
||||
return {status:'planned-pending',date:options.date,estimate,item};
|
||||
}
|
||||
};
|
||||
running=perform().finally(()=>{running=null;});
|
||||
|
|
@ -99,8 +99,8 @@ function createSearchWeekPlanUI({planner,document,window,getDetail,onPlanned=()=
|
|||
else if(outcome.status==='day-full')status.textContent='That day already has '+outcome.limit+' items. Choose another day.';
|
||||
else if(outcome.status==='already-planned')status.textContent='Already planned for '+outcome.date+'. Choose Move to change the day.';
|
||||
else if(outcome.status==='assigned-not-planned')status.textContent='Assigned, not planned. Find it in My Work and retry.';
|
||||
else if(outcome.status==='planned-pending'){announce('Planned for '+date+' · sync pending.');onPlanned(detail,true);close(true);}
|
||||
else if(outcome.status==='planned'){announce('Planned for '+date+'.');onPlanned(detail,false);close(true);}
|
||||
else if(outcome.status==='planned-pending'){announce('Planned for '+date+' · sync pending.');await onPlanned(outcome.item||detail,true,outcome);close(true);}
|
||||
else if(outcome.status==='planned'){announce('Planned for '+date+'.');await onPlanned(outcome.item||detail,false,outcome);close(true);}
|
||||
else status.textContent='Could not plan this issue. Retry.';
|
||||
button.disabled=false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,6 +487,40 @@ def test_following_review_controls_are_wired_into_the_phone_preview_flow():
|
|||
assert "afterUnwatch:item => followingQueue.retire(item)" in dashboard
|
||||
assert "followingQueue.returnToFollowing()" in dashboard
|
||||
assert "'Back to Following'" in dashboard
|
||||
|
||||
|
||||
def test_following_open_issue_exposes_only_action_and_continue_planning_dispositions():
|
||||
script = f"""
|
||||
const createFollowing = require({json.dumps(str(MODULE))});
|
||||
const disposition = createFollowing.planningDisposition;
|
||||
process.stdout.write(JSON.stringify({{
|
||||
active:disposition({{following:true,kind:'issue',state:'open',claimable:true}}),
|
||||
generic:disposition({{following:false,kind:'issue',state:'open',claimable:true}}),
|
||||
pull:disposition({{following:true,kind:'pull',state:'open',assigned_to_me:true}}),
|
||||
}}));
|
||||
"""
|
||||
result = json.loads(subprocess.run(
|
||||
["node", "-e", script], text=True, capture_output=True, check=True
|
||||
).stdout)
|
||||
|
||||
assert result == {
|
||||
"active": {
|
||||
"active": True,
|
||||
"today": "Add to Today & next",
|
||||
"later": "Later & next",
|
||||
"week": "Week Ahead & next",
|
||||
},
|
||||
"generic": {"active": False},
|
||||
"pull": {"active": False},
|
||||
}
|
||||
|
||||
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
||||
css = (ROOT / "frontend" / "dashboard.css").read_text()
|
||||
service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
|
||||
assert "followingQueue.preview(state);" in dashboard
|
||||
assert "following-disposition-mode" in MODULE.read_text()
|
||||
assert "controls.claim.hidden = controls.start.hidden = controls.watch.hidden = true" in MODULE.read_text()
|
||||
assert ".following-disposition-mode" in css
|
||||
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
||||
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
||||
assert "stackchain-dashboard-shell-v137" in service_worker
|
||||
|
|
|
|||
|
|
@ -98,7 +98,12 @@ const outcome=await planner.plan({kind:'issue',state:'open',assigned_to_me:true}
|
|||
console.log(JSON.stringify(outcome));
|
||||
""")
|
||||
|
||||
assert result == {"status": "planned-pending", "date": "2026-08-22", "estimate": 45}
|
||||
assert result == {
|
||||
"status": "planned-pending",
|
||||
"date": "2026-08-22",
|
||||
"estimate": 45,
|
||||
"item": {"kind": "issue", "state": "open", "assigned_to_me": True},
|
||||
}
|
||||
|
||||
|
||||
def test_search_week_plan_stages_canonical_details_before_flushing():
|
||||
|
|
@ -122,3 +127,37 @@ console.log(JSON.stringify({outcome,calls}));
|
|||
["details", "issue:stackchain/dashboard:42:", "Canonical title", "stackchain/dashboard", 42],
|
||||
["flush"],
|
||||
]
|
||||
|
||||
|
||||
def test_search_week_plan_waits_for_success_callback_before_closing():
|
||||
result = run_planner("""
|
||||
const events=[];
|
||||
const nodes={};
|
||||
function node(extra={}) { return Object.assign({
|
||||
hidden:true,disabled:false,textContent:'',value:'',dataset:{},
|
||||
addEventListener(){},focus(){events.push('focus');},
|
||||
setAttribute(){},querySelectorAll(){return[];}
|
||||
},extra); }
|
||||
nodes['#search-week-plan-sheet']=node();
|
||||
nodes['#search-week-plan-status']=node();
|
||||
nodes['#confirm-search-week-plan']=node();
|
||||
nodes['#search-week-plan-copy']=node();
|
||||
nodes['#search-week-plan-days']=node({dataset:{selected:'2026-08-22'}});
|
||||
nodes['#search-week-plan-estimate']=node({value:'45'});
|
||||
nodes['#plan-search-result']=node(); nodes['#cancel-search-week-plan']=node();
|
||||
const document={querySelector:selector=>nodes[selector],addEventListener(){}};
|
||||
const window={history:{state:{},pushState(value){this.state=value;},back(){events.push('back');nodes['#search-week-plan-sheet'].hidden=true;}},location:{href:'/'},addEventListener(){}};
|
||||
const planner={pending:()=>false,preview:async()=>({days:[{date:'2026-08-22',label:'Sat',planned_minutes:0,capacity_minutes:90,ids:[]}]}),
|
||||
plan:async()=>({status:'planned',date:'2026-08-22',estimate:45,item:{number:42}})};
|
||||
const ui=createSearchWeekPlan.UI({planner,document,window,getDetail:()=>({title:'Issue',number:42}),
|
||||
escapeHtml:value=>value,escapeAttribute:value=>value,announce:message=>events.push(message),
|
||||
onPlanned:async()=>{events.push('callback-start');await Promise.resolve();events.push('callback-end');}});
|
||||
await ui.open(node());
|
||||
await ui.confirm();
|
||||
events.push(nodes['#search-week-plan-sheet'].hidden?'closed':'open');
|
||||
console.log(JSON.stringify({events}));
|
||||
""")
|
||||
|
||||
assert result["events"][-4:] == [
|
||||
"callback-start", "callback-end", "back", "closed"
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user