Batch-plan mobile Search results into Today (#810)
This commit is contained in:
commit
da45cc36a3
|
|
@ -7,9 +7,12 @@ function createBatchFindWork({
|
|||
onProgress = () => {},
|
||||
storage = typeof localStorage === 'undefined' ? null : localStorage,
|
||||
owner = () => '',
|
||||
journalName = 'find-work-batch',
|
||||
autoMount = true,
|
||||
}) {
|
||||
let request = null;
|
||||
const journalKey = () => 'stackchain.find-work-batch.v1.' + encodeURIComponent(String(owner() || ''));
|
||||
const journalKey = () => 'stackchain.' + journalName + '.v1.' +
|
||||
encodeURIComponent(String(owner() || ''));
|
||||
|
||||
function readJournal() {
|
||||
if (!storage || !String(owner() || '')) return null;
|
||||
|
|
@ -125,6 +128,11 @@ function createBatchFindWork({
|
|||
return request;
|
||||
}
|
||||
|
||||
function pending() {
|
||||
const journal = readJournal();
|
||||
return journal ? journal.items.filter(item => item.state !== 'queued').length : 0;
|
||||
}
|
||||
|
||||
function mountRecovery(button, opener) {
|
||||
const show = () => {
|
||||
const journal = readJournal();
|
||||
|
|
@ -144,10 +152,10 @@ function createBatchFindWork({
|
|||
opener.addEventListener('click', show);
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') mountRecovery(
|
||||
if (autoMount && typeof document !== 'undefined') mountRecovery(
|
||||
document.getElementById('resume-find-work-batch'), document.getElementById('find-work')
|
||||
);
|
||||
return { run, resume, mountRecovery };
|
||||
return { run, resume, pending, mountRecovery };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createBatchFindWork;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
#cmd-palette { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: min(900px, 94vw); background: rgba(11,21,38,.96); border: 1px solid #2a496e; border-radius: 12px; box-shadow: 0 20px 70px rgba(0,0,0,.55); padding: 10px; z-index: 30; display: none; backdrop-filter: blur(12px); }
|
||||
#cmd-palette.open { display: block; }
|
||||
.cmd-palette-header { display:none; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.cmd-palette-header-actions { display:flex; gap:8px; }
|
||||
.cmd-search-scope { display:grid; grid-template-columns:auto minmax(120px,1fr) auto minmax(120px,1fr); gap:6px 10px; align-items:center; margin:8px 0 0; padding:8px; border:1px solid #1f3a5f; border-radius:8px; }
|
||||
.cmd-search-scope legend { padding:0 4px; color:#93a4b8; font-size:12px; }
|
||||
.cmd-search-scope label { font-size:12px; color:#cbd5e1; }
|
||||
|
|
@ -125,6 +126,14 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.cmd-item:hover, .cmd-item.selected { background: #10233a; outline:1px solid #31577f; }
|
||||
.cmd-meta { color:#93a4b8; font-size:12px; text-align:right; }
|
||||
.cmd-status { padding:10px; color:#93a4b8; font-size:13px; }
|
||||
.cmd-select-result { min-height:44px; width:100%; display:grid; grid-template-columns:28px 1fr auto; gap:10px; align-items:center; text-align:left; }
|
||||
.cmd-select-result input { width:22px; height:22px; }
|
||||
.cmd-select-result[disabled] { cursor:not-allowed; opacity:.62; }
|
||||
.search-batch-actions { position:sticky; bottom:0; z-index:3; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0 calc(10px + env(safe-area-inset-bottom)); background:#0b1526; border-top:1px solid #2a496e; }
|
||||
.search-batch-actions[hidden] { display:none; }
|
||||
.search-batch-actions span { grid-column:1 / -1; }
|
||||
.search-batch-actions button, .search-batch-recovery { min-height:44px; }
|
||||
.search-batch-recovery { width:100%; font-weight:700; }
|
||||
|
||||
.cmd-group { padding:8px 10px 3px; color:#60a5fa; font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
|
||||
#whiteboard-modal, #markdown-modal { position: fixed; inset: 0; background: rgba(5,12,21,.55); display: none; align-items: center; justify-content: center; z-index: 40; backdrop-filter: blur(6px); }
|
||||
|
|
|
|||
|
|
@ -4293,6 +4293,7 @@
|
|||
let commandSearchState = { status:'idle', query:'', items:[] };
|
||||
let commandItems = [];
|
||||
let commandSelection = -1;
|
||||
let searchBatchPlanning = null;
|
||||
async function searchGlobalWork(query, signal, page = 1, scope = {kind:'all', state:'all'}, continuation) {
|
||||
const response = await fetch(filterCommands.searchUrl(query, page, scope, continuation), {
|
||||
headers: { Accept:'application/json' },
|
||||
|
|
@ -4421,6 +4422,11 @@
|
|||
session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})],
|
||||
onState: renderSearchPreview,
|
||||
});
|
||||
searchBatchPlanning = mountSearchBatchPlanning(
|
||||
document, createBatchFindWork, todayWork, ()=>planningOwnerLogin, fetchReviewJson,
|
||||
searchPreviewPath, queueToday, acceptClaimedIssue, index=>commandItems[index]?.result,
|
||||
()=>renderCommands(qs('#cmd-input').value), escapeHtml, escAttr
|
||||
);
|
||||
searchDefer = createSearchDefer({
|
||||
claim: detail => searchPreview.claim(detail),
|
||||
accept: confirmed => acceptClaimedIssue(confirmed),
|
||||
|
|
@ -4510,7 +4516,8 @@
|
|||
function renderCommands(filter) {
|
||||
const el = qs('#cmd-results');
|
||||
const loadMore = qs('#cmd-load-more');
|
||||
const local = filterCommands(commands, filter).map(command => ({ command }));
|
||||
const selecting = searchBatchPlanning?.plan.snapshot().active === true;
|
||||
const local = selecting ? [] : filterCommands(commands, filter).map(command => ({ command }));
|
||||
const remote = commandSearchState.query === String(filter || '').trim()
|
||||
? commandSearchState.items.map(result => ({ result })) : [];
|
||||
commandItems = local.concat(remote);
|
||||
|
|
@ -4521,6 +4528,7 @@
|
|||
html += remote.map((item, remoteIdx) => {
|
||||
const idx = local.length + remoteIdx;
|
||||
const result = item.result;
|
||||
if (selecting) return searchBatchPlanning.resultHtml(result, idx);
|
||||
return '<div class="cmd-item' + (idx === commandSelection ? ' selected' : '') + '" role="option" aria-selected="' + (idx === commandSelection) + '" data-idx="' + idx + '"><span>' + escapeHtml(result.title) + '</span><span class="cmd-meta">' + escapeHtml(result.repository) + ' #' + escapeHtml(result.number) + ' · ' + escapeHtml(result.kind === 'pull' ? 'Pull request' : 'Issue') + ' · ' + escapeHtml(result.state) + '</span></div>';
|
||||
}).join('');
|
||||
if (commandSearchState.status === 'loading') html += '<div class="cmd-status">Searching accessible work…</div>';
|
||||
|
|
@ -4533,6 +4541,7 @@
|
|||
el.querySelectorAll('.cmd-item').forEach((item) => {
|
||||
item.addEventListener('click', () => runCommandItem(commandItems[Number(item.dataset.idx)]));
|
||||
});
|
||||
|
||||
}
|
||||
function openCommandPalette(navigate = true) {
|
||||
if (navigate) {
|
||||
|
|
@ -4607,6 +4616,7 @@
|
|||
qs('#open-palette').addEventListener('click', openCommandPalette);
|
||||
qs('#close-command-palette').addEventListener('click', () => taskOverlayHistory.close());
|
||||
qs('#cmd-load-more').addEventListener('click', () => commandSearch.loadMore());
|
||||
|
||||
function changeSearchScope() {
|
||||
const scope = currentSearchScope();
|
||||
commandSelection = -1;
|
||||
|
|
|
|||
|
|
@ -426,7 +426,10 @@
|
|||
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
||||
<div class="cmd-palette-header">
|
||||
<strong>Search work</strong>
|
||||
<button id="close-command-palette" type="button">Close</button>
|
||||
<div class="cmd-palette-header-actions">
|
||||
<button id="select-search-results" type="button">Select</button>
|
||||
<button id="close-command-palette" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<input id="cmd-input" type="text" role="combobox" aria-autocomplete="list" aria-controls="cmd-results" aria-expanded="false" placeholder="Search commands, issues, and pull requests..." />
|
||||
<fieldset class="cmd-search-scope">
|
||||
|
|
@ -449,8 +452,14 @@
|
|||
<span id="cmd-repository-status" class="cmd-repository-status" aria-live="polite"></span>
|
||||
</fieldset>
|
||||
<div id="cmd-search-action-status" class="small" aria-live="polite"></div>
|
||||
<button id="resume-search-batch" class="search-batch-recovery" type="button" hidden>Resume batch</button>
|
||||
<div id="cmd-results" class="stack" role="listbox" aria-label="Commands and work search results"></div>
|
||||
<button id="cmd-load-more" hidden>More results</button>
|
||||
<div class="search-batch-actions" id="search-batch-actions" hidden>
|
||||
<span id="search-selection-status" class="small" aria-live="polite">No issues selected.</span>
|
||||
<button id="cancel-search-selection" type="button">Cancel</button>
|
||||
<button id="queue-selected-search-results" type="button" disabled>Assign & add to Today</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-preview" id="search-preview" role="dialog" aria-modal="true" aria-labelledby="search-preview-title">
|
||||
|
|
@ -1117,6 +1126,7 @@
|
|||
<script src="static/later-picker.js"></script>
|
||||
<script src="static/pick-work.js"></script>
|
||||
<script src="static/batch-find-work.js"></script>
|
||||
<script src="static/search-batch-plan.js"></script>
|
||||
<script src="static/conversation.js"></script>
|
||||
<script src="static/comment-actions.js"></script>
|
||||
<script src="static/issue-attachment.js"></script>
|
||||
|
|
|
|||
138
frontend/search-batch-plan.js
Normal file
138
frontend/search-batch-plan.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
(function (root, factory) {
|
||||
const createSearchBatchPlan = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = createSearchBatchPlan;
|
||||
if (root) {
|
||||
root.createSearchBatchPlan = createSearchBatchPlan;
|
||||
root.mountSearchBatchPlanning = createSearchBatchPlan.mount;
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||
function createSearchBatchPlan({
|
||||
limit = 50,
|
||||
resolve = item => Promise.resolve(item),
|
||||
claim = item => Promise.resolve(item),
|
||||
onChange = () => {},
|
||||
} = {}) {
|
||||
const maximum = Number.isInteger(limit) && limit > 0 ? limit : 50;
|
||||
const selected = new Map();
|
||||
let active = false;
|
||||
|
||||
const identity = item => [item?.kind || '', item?.repository || '', item?.number || ''].join(':');
|
||||
const eligible = item => item?.kind === 'issue' && item?.state === 'open' &&
|
||||
Boolean(item.repository) && Number.isInteger(item.number);
|
||||
function snapshot() {
|
||||
return { active, count:selected.size, items:Array.from(selected.values()) };
|
||||
}
|
||||
function changed() {
|
||||
const state = snapshot();
|
||||
onChange(state);
|
||||
return state;
|
||||
}
|
||||
function start() {
|
||||
active = true;
|
||||
selected.clear();
|
||||
return changed();
|
||||
}
|
||||
function cancel() {
|
||||
active = false;
|
||||
selected.clear();
|
||||
return changed();
|
||||
}
|
||||
function toggle(item) {
|
||||
if (!active) return 'inactive';
|
||||
if (!eligible(item)) return 'ineligible';
|
||||
const key = identity(item);
|
||||
if (selected.has(key)) {
|
||||
selected.delete(key);
|
||||
changed();
|
||||
return 'removed';
|
||||
}
|
||||
if (selected.size >= maximum) return 'limit';
|
||||
selected.set(key, { ...item });
|
||||
changed();
|
||||
return 'selected';
|
||||
}
|
||||
async function prepare(item) {
|
||||
const detail = await resolve(item);
|
||||
if (detail?.assigned_to_me) return detail;
|
||||
if (detail?.claimable) return claim(detail);
|
||||
throw new Error('This issue is no longer available to assign.');
|
||||
}
|
||||
return { identity, eligible, start, cancel, toggle, snapshot, prepare, limit:maximum };
|
||||
}
|
||||
|
||||
createSearchBatchPlan.mount = function mountSearchBatchPlanning(
|
||||
document, batchFactory, todayWork, getOwner, fetchJson, previewPath, queueToday,
|
||||
acceptIssue, lookup, render, escapeHtml, escapeAttribute
|
||||
) {
|
||||
const get = selector => document.querySelector(selector);
|
||||
let processor;
|
||||
const plan = createSearchBatchPlan({
|
||||
resolve:item => fetchJson(previewPath(item), {headers:{Accept:'application/json'}}),
|
||||
claim:detail => fetchJson(
|
||||
'api/v1/repos/' + detail.repository.split('/').map(encodeURIComponent).join('/') +
|
||||
'/issues/' + encodeURIComponent(detail.number) + '/claim',
|
||||
{method:'PATCH', headers:{Accept:'application/json'}}
|
||||
),
|
||||
onChange:state => {
|
||||
get('#search-batch-actions').hidden = !state.active;
|
||||
get('#select-search-results').hidden = state.active;
|
||||
get('#queue-selected-search-results').disabled = state.count === 0;
|
||||
get('#search-selection-status').textContent = state.count ?
|
||||
state.count + ' issue' + (state.count === 1 ? '' : 's') + ' selected.' : 'No issues selected.';
|
||||
render();
|
||||
},
|
||||
});
|
||||
processor = batchFactory({
|
||||
capacity:() => Math.max(0, todayWork.limit - todayWork.read().length),
|
||||
owner:getOwner,
|
||||
journalName:'search-today-batch',
|
||||
autoMount:false,
|
||||
claim:item => plan.prepare(item),
|
||||
queue:confirmed => queueToday(acceptIssue(confirmed)),
|
||||
onProgress:progress => {
|
||||
get('#queue-selected-search-results').disabled = progress.status === 'running';
|
||||
if (progress.status === 'running') {
|
||||
get('#search-selection-status').textContent = 'Planning ' + progress.processed + ' of ' + progress.selected + '…';
|
||||
} else if (progress.status === 'full') {
|
||||
get('#search-selection-status').textContent = 'Today has ' + progress.available + ' remaining slot' +
|
||||
(progress.available === 1 ? '.' : 's.');
|
||||
} else if (progress.status === 'complete') {
|
||||
get('#cmd-search-action-status').textContent = progress.failed.length ?
|
||||
progress.queued.length + ' queued · ' + progress.failed.length + ' need retry.' :
|
||||
progress.queued.length + ' added to Today.';
|
||||
get('#resume-search-batch').hidden = processor.pending() === 0;
|
||||
if (!progress.failed.length) plan.cancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
get('#select-search-results').addEventListener('click', () => plan.start());
|
||||
get('#cancel-search-selection').addEventListener('click', () => plan.cancel());
|
||||
get('#queue-selected-search-results').addEventListener('click', () => processor.run(plan.snapshot().items));
|
||||
get('#resume-search-batch').addEventListener('click', () => processor.resume());
|
||||
get('#open-palette').addEventListener('click', () => restore());
|
||||
get('#cmd-results').addEventListener('change', event => {
|
||||
const index = event.target?.dataset?.searchSelect;
|
||||
if (index === undefined) return;
|
||||
const result = lookup(Number(index));
|
||||
if (result) plan.toggle(result);
|
||||
});
|
||||
function restore() {
|
||||
const pending = processor.pending();
|
||||
get('#resume-search-batch').hidden = pending === 0;
|
||||
get('#resume-search-batch').textContent = 'Resume ' + pending + ' interrupted';
|
||||
}
|
||||
function resultHtml(result, index) {
|
||||
const allowed = result.kind === 'issue' && result.state === 'open';
|
||||
const selected = plan.snapshot().items.some(item => plan.identity(item) === plan.identity(result));
|
||||
return '<label class="cmd-select-result"' + (allowed ? '' : ' aria-disabled="true"') + '>' +
|
||||
'<input type="checkbox" data-search-select="' + index + '" ' + (selected ? 'checked ' : '') +
|
||||
(allowed ? '' : 'disabled ') + 'aria-label="Select ' + escapeAttribute(result.title) + '" />' +
|
||||
'<span>' + escapeHtml(result.title) + '</span><span class="cmd-meta">' +
|
||||
escapeHtml(result.repository) + ' #' + escapeHtml(result.number) + ' · ' +
|
||||
(allowed ? 'Open issue' : 'Not eligible') + '</span></label>';
|
||||
}
|
||||
return { plan, processor, restore, resultHtml };
|
||||
};
|
||||
|
||||
return createSearchBatchPlan;
|
||||
});
|
||||
|
|
@ -60,6 +60,7 @@ const SHELL = [
|
|||
BASE + 'static/later-picker.js',
|
||||
BASE + 'static/pick-work.js',
|
||||
BASE + 'static/batch-find-work.js',
|
||||
BASE + 'static/search-batch-plan.js',
|
||||
BASE + 'static/conversation.js',
|
||||
BASE + 'static/comment-actions.js',
|
||||
BASE + 'static/issue-attachment.js',
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ FEATURE_SOURCES = {
|
|||
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
||||
"security-center": ("static/security-center.js",),
|
||||
"today-timer": (
|
||||
"static/commands.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||
"static/commands.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
||||
"static/assign-and-start.js", "static/queue-today.js",
|
||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||
"static/search-batch-plan.js",
|
||||
),
|
||||
}
|
||||
CACHE_DECLARATION = re.compile(
|
||||
|
|
|
|||
|
|
@ -185,6 +185,41 @@ flow.resume().then(result=>process.stdout.write(JSON.stringify({{result,calls}})
|
|||
}
|
||||
|
||||
|
||||
def test_search_batch_uses_an_isolated_owner_journal_and_can_resume_without_dom_mounting():
|
||||
script = f"""
|
||||
const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))});
|
||||
const values={{}};
|
||||
const calls=[];
|
||||
const storage={{
|
||||
getItem:key=>values[key] || null,
|
||||
setItem:(key,value)=>{{values[key]=value;}},
|
||||
removeItem:key=>{{delete values[key];}},
|
||||
}};
|
||||
const flow=createBatchFindWork({{
|
||||
capacity:()=>1, owner:()=> 'timmy', storage,
|
||||
journalName:'search-today-batch', autoMount:false,
|
||||
claim:item=>Promise.resolve({{...item,assigned_to_me:true}}),
|
||||
queue:()=>{{calls.push('queue');return 'sync-unavailable';}},
|
||||
}});
|
||||
flow.run([{{repository:'stackchain/dashboard',number:809}}]).then(result=>
|
||||
process.stdout.write(JSON.stringify({{result,keys:Object.keys(values),calls,pending:flow.pending()}})));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"result": {
|
||||
"status": "complete", "selected": 1, "available": 1, "queued": [],
|
||||
"failed": [{
|
||||
"key": "stackchain/dashboard#809",
|
||||
"reason": "assigned but Today sync is unavailable",
|
||||
"assigned": True,
|
||||
}],
|
||||
},
|
||||
"keys": ["stackchain.search-today-batch.v1.timmy"],
|
||||
"calls": ["queue"],
|
||||
"pending": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_time_budget_blocks_claims_until_every_estimate_fits():
|
||||
script = f"""
|
||||
const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))});
|
||||
|
|
|
|||
96
tests/test_search_batch_plan.py
Normal file
96
tests/test_search_batch_plan.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SEARCH_BATCH_PLAN = Path(__file__).parents[1] / "frontend" / "search-batch-plan.js"
|
||||
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||
|
||||
|
||||
def run_node(script):
|
||||
return json.loads(subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
).stdout)
|
||||
|
||||
|
||||
def test_search_batch_selection_keeps_open_issues_across_pages_and_rejects_ineligible_results():
|
||||
script = f"""
|
||||
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
||||
const states=[];
|
||||
const flow=createSearchBatchPlan({{onChange:state=>states.push(state)}});
|
||||
const first={{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,title:'First'}};
|
||||
const second={{kind:'issue',state:'open',repository:'stackchain/api',number:17,title:'Second'}};
|
||||
const pull={{kind:'pull',state:'open',repository:'stackchain/dashboard',number:810}};
|
||||
const closed={{kind:'issue',state:'closed',repository:'stackchain/dashboard',number:808}};
|
||||
flow.start();
|
||||
const outcomes=[flow.toggle(first),flow.toggle(pull),flow.toggle(closed),flow.toggle(second)];
|
||||
process.stdout.write(JSON.stringify({{outcomes,snapshot:flow.snapshot(),states}}));
|
||||
"""
|
||||
|
||||
result = run_node(script)
|
||||
assert result["outcomes"] == ["selected", "ineligible", "ineligible", "selected"]
|
||||
assert result["snapshot"] == {
|
||||
"active": True,
|
||||
"count": 2,
|
||||
"items": [
|
||||
{"kind": "issue", "state": "open", "repository": "stackchain/dashboard", "number": 809, "title": "First"},
|
||||
{"kind": "issue", "state": "open", "repository": "stackchain/api", "number": 17, "title": "Second"},
|
||||
],
|
||||
}
|
||||
assert result["states"][-1]["count"] == 2
|
||||
|
||||
|
||||
def test_search_batch_prepares_owned_issues_without_reclaiming_and_claims_unassigned_issues():
|
||||
script = f"""
|
||||
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
||||
const calls=[];
|
||||
const details={{
|
||||
809:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,assigned_to_me:true}},
|
||||
810:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:810,claimable:true}},
|
||||
811:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:811,claimable:false}},
|
||||
}};
|
||||
const flow=createSearchBatchPlan({{
|
||||
resolve:item=>{{calls.push('resolve:'+item.number);return Promise.resolve(details[item.number]);}},
|
||||
claim:detail=>{{calls.push('claim:'+detail.number);return Promise.resolve({{...detail,assigned_to_me:true}});}},
|
||||
}});
|
||||
Promise.all([
|
||||
flow.prepare({{number:809}}),
|
||||
flow.prepare({{number:810}}),
|
||||
flow.prepare({{number:811}}).catch(error=>error.message),
|
||||
]).then(results=>process.stdout.write(JSON.stringify({{results,calls}})));
|
||||
"""
|
||||
|
||||
result = run_node(script)
|
||||
assert result["calls"] == ["resolve:809", "resolve:810", "resolve:811", "claim:810"]
|
||||
assert result["results"][0]["assigned_to_me"] is True
|
||||
assert result["results"][1]["assigned_to_me"] is True
|
||||
assert result["results"][2] == "This issue is no longer available to assign."
|
||||
|
||||
|
||||
def test_mobile_search_exposes_touch_safe_durable_batch_planning_controls():
|
||||
html = HTML.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
css = CSS.read_text()
|
||||
worker = (HTML.parent / "service-worker.js").read_text()
|
||||
controller = SEARCH_BATCH_PLAN.read_text()
|
||||
|
||||
assert 'id="select-search-results"' in html
|
||||
assert 'id="search-batch-actions"' in html
|
||||
assert 'id="queue-selected-search-results"' in html
|
||||
assert 'id="resume-search-batch"' in html
|
||||
assert 'src="static/search-batch-plan.js"' in html
|
||||
assert "mountSearchBatchPlanning(" in dashboard
|
||||
assert "journalName:'search-today-batch'" in controller
|
||||
assert "autoMount:false" in controller
|
||||
assert "plan.toggle(result)" in controller
|
||||
assert "processor.run(plan.snapshot().items)" in controller
|
||||
assert "processor.resume()" in controller
|
||||
assert 'aria-label="Select ' in controller
|
||||
assert "result.kind === 'issue' && result.state === 'open'" in controller
|
||||
assert ".search-batch-actions" in css
|
||||
assert "position:sticky" in css
|
||||
assert "env(safe-area-inset-bottom)" in css
|
||||
assert ".cmd-select-result" in css and "min-height:44px" in css
|
||||
assert "BASE + 'static/search-batch-plan.js'" in worker
|
||||
|
|
@ -830,6 +830,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
|||
"/dashboard/static/later-picker.js",
|
||||
"/dashboard/static/pick-work.js",
|
||||
"/dashboard/static/batch-find-work.js",
|
||||
"/dashboard/static/search-batch-plan.js",
|
||||
"/dashboard/static/conversation.js",
|
||||
"/dashboard/static/comment-actions.js",
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user