feat: fill Today from ranked Find Work results (Closes #677)
This commit is contained in:
parent
5675ca5d41
commit
c93a682dfb
|
|
@ -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-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 { display:flex; align-items:center; gap:10px; min-height:44px; }
|
||||||
.find-work-select input { width:22px; height:22px; }
|
.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 { 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[hidden] { display:none; }
|
||||||
.find-work-batch-actions span { grid-column:1 / -1; }
|
.find-work-batch-actions span { grid-column:1 / -1; }
|
||||||
|
|
|
||||||
|
|
@ -4277,6 +4277,14 @@
|
||||||
qs('#find-work').addEventListener('click', openFindWorkSheet);
|
qs('#find-work').addEventListener('click', openFindWorkSheet);
|
||||||
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
|
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
|
||||||
qs('#select-find-work').addEventListener('click', () => findWorkController.startSelection());
|
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-form').addEventListener('submit', event => event.preventDefault());
|
||||||
qs('#find-work-search').addEventListener('input', event => {
|
qs('#find-work-search').addEventListener('input', event => {
|
||||||
const value = event.currentTarget.value;
|
const value = event.currentTarget.value;
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,8 @@
|
||||||
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
|
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
|
||||||
<span id="find-work-match-status" class="small" aria-live="polite"></span>
|
<span id="find-work-match-status" class="small" aria-live="polite"></span>
|
||||||
</form>
|
</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 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>
|
<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>
|
<button class="find-work-more" id="load-more-available" type="button" hidden>Load more available issues</button>
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,19 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio
|
||||||
else selected.set(key, item);
|
else selected.set(key, item);
|
||||||
return emitSelection();
|
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) {
|
isSelected(item) {
|
||||||
return selected.has(itemKey(item));
|
return selected.has(itemKey(item));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,66 @@ controller.load().then(()=>{{
|
||||||
assert run_node(script) == {"selected": [672], "selecting": True}
|
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():
|
def test_mobile_find_work_exposes_accessible_batch_controls_and_offline_asset():
|
||||||
html = HTML.read_text()
|
html = HTML.read_text()
|
||||||
dashboard = DASHBOARD.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
|
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():
|
def test_find_work_search_ignores_stale_response_and_preserves_cross_query_selection():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createFindWork=require({json.dumps(str(PICK_WORK))});
|
const createFindWork=require({json.dumps(str(PICK_WORK))});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user