From c93a682dfb3bbfda41f037c439a0589c58348c8f Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 12 Aug 2026 20:25:37 +0000 Subject: [PATCH] feat: fill Today from ranked Find Work results (Closes #677) --- frontend/dashboard.css | 1 + frontend/dashboard.js | 8 ++++ frontend/index.html | 2 + frontend/pick-work.js | 13 +++++++ tests/test_batch_find_work.py | 72 +++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 8c468d9..e015c0b 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -463,6 +463,7 @@ textarea { resize: vertical; min-height: 120px; } .find-work-card.selected { border-color:#60a5fa; box-shadow:0 0 0 2px rgba(96,165,250,.25); } .find-work-select { display:flex; align-items:center; gap:10px; min-height:44px; } .find-work-select input { width:22px; height:22px; } +.fill-find-work-today { min-height:44px; width:100%; font-weight:700; } .find-work-batch-actions { position:sticky; bottom:0; z-index:2; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:12px 0 calc(12px + env(safe-area-inset-bottom)); background:#0b1526; border-top:1px solid #2a496e; } .find-work-batch-actions[hidden] { display:none; } .find-work-batch-actions span { grid-column:1 / -1; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index a85b124..ee223d1 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -4277,6 +4277,14 @@ qs('#find-work').addEventListener('click', openFindWorkSheet); qs('#close-find-work').addEventListener('click', closeFindWorkSheet); qs('#select-find-work').addEventListener('click', () => findWorkController.startSelection()); + qs('#fill-find-work-today').addEventListener('click', () => { + const remainingSlots = todayWork.limit - todayWork.read().length; + const outcome = findWorkController.fillSelection(remainingSlots); + qs('#find-work-status').textContent = remainingSlots < 1 ? 'Today is full.' : + outcome.selected ? outcome.selected + ' ranked work selected.' : 'No matching work.'; + if (outcome.selected && todayWork.planning().capacity_minutes !== null) + openFindWorkEstimateReview(findWorkController.selectedItems()); + }); qs('#find-work-search-form').addEventListener('submit', event => event.preventDefault()); qs('#find-work-search').addEventListener('input', event => { const value = event.currentTarget.value; diff --git a/frontend/index.html b/frontend/index.html index d5fdcc8..302d595 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -541,6 +541,8 @@ +
Open Find Work to load available issues.
diff --git a/frontend/pick-work.js b/frontend/pick-work.js index 43426a1..73f38d6 100644 --- a/frontend/pick-work.js +++ b/frontend/pick-work.js @@ -101,6 +101,19 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio else selected.set(key, item); return emitSelection(); }, + fillSelection(limit) { + limit = Math.max(0, Math.floor(Number(limit) || 0)); + if (!limit) return { selected: selected.size, added: 0, limit }; + selecting = true; + const before = selected.size; + available.some(item => { + if (selected.size >= limit) return true; + selected.set(itemKey(item), item); + return false; + }); + emitSelection(); + return { selected: selected.size, added: selected.size - before, limit }; + }, isSelected(item) { return selected.has(itemKey(item)); }, diff --git a/tests/test_batch_find_work.py b/tests/test_batch_find_work.py index b44ca3a..e135f41 100644 --- a/tests/test_batch_find_work.py +++ b/tests/test_batch_find_work.py @@ -212,6 +212,66 @@ controller.load().then(()=>{{ assert run_node(script) == {"selected": [672], "selecting": True} +def test_fill_today_selects_ranked_matches_up_to_remaining_capacity(): + script = f""" +const createFindWork=require({json.dumps(str(PICK_WORK))}); +const states=[]; +const controller=createFindWork({{ + fetchJson:()=>Promise.resolve({{items:[ + {{id:1,repository:'stackchain/dashboard',number:701}}, + {{id:2,repository:'stackchain/dashboard',number:702}}, + {{id:3,repository:'stackchain/dashboard',number:703}}, + {{id:4,repository:'stackchain/dashboard',number:704}}, + ],page:1,total:4,has_more:false}}), + onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, + onSelection:state=>states.push(state), +}}); +controller.load().then(()=>{{ + controller.startSelection(); + controller.toggleSelection(controller.items()[1]); + const outcome=controller.fillSelection(3); + process.stdout.write(JSON.stringify({{ + outcome, + selected:controller.selectedItems().map(item=>item.number), + lastState:states.at(-1), + }})); +}}); +""" + + assert run_node(script) == { + "outcome": {"selected": 3, "added": 2, "limit": 3}, + "selected": [702, 701, 703], + "lastState": { + "active": True, + "count": 3, + "ids": [ + "stackchain/dashboard#702", + "stackchain/dashboard#701", + "stackchain/dashboard#703", + ], + }, + } + + +def test_fill_today_with_no_capacity_does_not_change_selection(): + script = f""" +const createFindWork=require({json.dumps(str(PICK_WORK))}); +const controller=createFindWork({{ + fetchJson:()=>Promise.resolve({{items:[{{id:1,repository:'stackchain/dashboard',number:701}}]}}), + onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}}, +}}); +controller.load().then(()=>{{ + const outcome=controller.fillSelection(0); + process.stdout.write(JSON.stringify({{outcome,selection:controller.selection()}})); +}}); +""" + + assert run_node(script) == { + "outcome": {"selected": 0, "added": 0, "limit": 0}, + "selection": {"active": False, "count": 0, "ids": []}, + } + + def test_mobile_find_work_exposes_accessible_batch_controls_and_offline_asset(): html = HTML.read_text() dashboard = DASHBOARD.read_text() @@ -232,6 +292,18 @@ def test_mobile_find_work_exposes_accessible_batch_controls_and_offline_asset(): assert '' in html +def test_mobile_find_work_exposes_fill_today_action_with_capacity_handoff(): + html = HTML.read_text() + dashboard = DASHBOARD.read_text() + css = CSS.read_text() + + assert 'id="fill-find-work-today"' in html + assert 'aria-describedby="find-work-selection-status"' in html + assert "findWorkController.fillSelection(remainingSlots)" in dashboard + assert "openFindWorkEstimateReview(findWorkController.selectedItems())" in dashboard + assert ".fill-find-work-today" in css and "min-height:44px" in css + + def test_find_work_search_ignores_stale_response_and_preserves_cross_query_selection(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); -- 2.43.0