Merge pull request 'Fill remaining Today slots from Find Work' (#678) from timmy/677-fill-today-from-find-work into main
All checks were successful
CI / lint (push) Successful in 1m27s
CI / build-release (push) Successful in 5s
CI / release-candidate (push) Successful in 7s

This commit is contained in:
timmy 2026-08-12 20:27:50 +00:00
commit a0dd044995
5 changed files with 96 additions and 0 deletions

View File

@ -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; }

View File

@ -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;

View File

@ -541,6 +541,8 @@
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
<span id="find-work-match-status" class="small" aria-live="polite"></span>
</form>
<button class="fill-find-work-today" id="fill-find-work-today" type="button"
aria-describedby="find-work-selection-status">Fill remaining Today slots</button>
<div id="find-work-status" class="small" aria-live="assertive">Open Find Work to load available issues.</div>
<div class="find-work-list" id="find-work-list"></div>
<button class="find-work-more" id="load-more-available" type="button" hidden>Load more available issues</button>

View File

@ -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));
},

View File

@ -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 '<script src="static/batch-find-work.js"></script>' 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))});