feat: batch-defer mobile search results (Closes #813)
This commit is contained in:
1 parent
3694f2501b
commit
1ef3e291c2
9 files changed
+196
-17
No files matched your search
@@ -8,6 +8,7 @@ function createBatchFindWork({
|
||||
storage = typeof localStorage === 'undefined' ? null : localStorage,
|
||||
owner = () => '',
|
||||
journalName = 'find-work-batch',
|
||||
queueFailureReason = 'assigned but Today sync is unavailable',
|
||||
autoMount = true,
|
||||
}) {
|
||||
let request = null;
|
||||
@@ -58,7 +59,7 @@ function createBatchFindWork({
|
||||
entry.state = 'assigned';
|
||||
writeJournal(journal);
|
||||
}
|
||||
const queueResult = await queue(confirmed);
|
||||
const queueResult = await queue(confirmed, journal.context);
|
||||
if (queueResult === 'queued' || queueResult === 'exists') {
|
||||
queued.push(key(item));
|
||||
entry.state = 'queued';
|
||||
@@ -67,7 +68,7 @@ function createBatchFindWork({
|
||||
}
|
||||
writeJournal(journal);
|
||||
} else {
|
||||
failed.push({ key: key(item), reason: 'assigned but Today sync is unavailable', assigned: true });
|
||||
failed.push({ key: key(item), reason: queueFailureReason, assigned: true });
|
||||
}
|
||||
} catch (error) {
|
||||
failed.push({ key: key(item), reason: error?.message || 'assignment failed' });
|
||||
@@ -81,7 +82,7 @@ function createBatchFindWork({
|
||||
return outcome;
|
||||
}
|
||||
|
||||
function run(items, estimates = {}) {
|
||||
function run(items, estimates = {}, context = null) {
|
||||
if (request) return request;
|
||||
const selected = Array.isArray(items) ? items.slice() : [];
|
||||
const available = Math.max(0, Number(capacity()) || 0);
|
||||
@@ -112,7 +113,7 @@ function createBatchFindWork({
|
||||
}
|
||||
}
|
||||
const journal = {
|
||||
owner: String(owner() || ''), available,
|
||||
owner: String(owner() || ''), available, context,
|
||||
items: selected.map(item => ({ item, estimate: estimates[key(item)] || null, state: 'pending' })),
|
||||
};
|
||||
writeJournal(journal);
|
||||
|
||||
@@ -129,7 +129,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.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 { position:sticky; bottom:0; z-index:3; display:grid; grid-template-columns:1fr 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; }
|
||||
|
||||
@@ -4424,8 +4424,8 @@
|
||||
});
|
||||
searchBatchPlanning = mountSearchBatchPlanning(
|
||||
document, createBatchFindWork, todayWork, ()=>planningOwnerLogin, fetchReviewJson,
|
||||
searchPreviewPath, queueToday, todaySync, acceptClaimedIssue, index=>commandItems[index]?.result,
|
||||
()=>renderCommands(qs('#cmd-input').value), escapeHtml, escAttr
|
||||
searchPreviewPath, queueToday, todaySync, acceptClaimedIssue, i=>commandItems[i]?.result,
|
||||
()=>renderCommands(qs('#cmd-input').value), escapeHtml, escAttr, laterWork, laterPicker
|
||||
);
|
||||
searchDefer = createSearchDefer({
|
||||
claim: detail => searchPreview.claim(detail),
|
||||
|
||||
@@ -458,6 +458,7 @@
|
||||
<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="defer-selected-search-results" type="button" disabled>Defer</button>
|
||||
<button id="queue-selected-search-results" type="button" disabled>Assign & add to Today</button>
|
||||
</div>
|
||||
<section class="search-batch-estimate-review" id="search-batch-estimate-review"
|
||||
|
||||
@@ -90,7 +90,8 @@ function createLaterPicker({
|
||||
onState({ open:true, message:result.message, value:String(input || '') });
|
||||
return false;
|
||||
}
|
||||
const confirmed = onConfirm(item, result.value, context);
|
||||
const confirmed = typeof item?.confirm === 'function' ? item.confirm(result.value) :
|
||||
onConfirm(item, result.value, context);
|
||||
if (confirmed !== true && typeof confirmed !== 'function') return false;
|
||||
committed = true;
|
||||
afterClose = typeof confirmed === 'function' ? confirmed : null;
|
||||
|
||||
@@ -62,10 +62,12 @@
|
||||
|
||||
createSearchBatchPlan.mount = function mountSearchBatchPlanning(
|
||||
document, batchFactory, todayWork, getOwner, fetchJson, previewPath, queueToday,
|
||||
todaySync, acceptIssue, lookup, render, escapeHtml, escapeAttribute
|
||||
todaySync, acceptIssue, lookup, render, escapeHtml, escapeAttribute,
|
||||
laterWork = null, laterPicker = null
|
||||
) {
|
||||
const get = selector => document.querySelector(selector);
|
||||
let processor;
|
||||
let laterProcessor;
|
||||
const plan = createSearchBatchPlan({
|
||||
resolve:item => fetchJson(previewPath(item), {headers:{Accept:'application/json'}}),
|
||||
claim:detail => fetchJson(
|
||||
@@ -77,6 +79,7 @@
|
||||
get('#search-batch-actions').hidden = !state.active;
|
||||
get('#select-search-results').hidden = state.active;
|
||||
get('#queue-selected-search-results').disabled = state.count === 0;
|
||||
get('#defer-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();
|
||||
@@ -125,6 +128,30 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
laterProcessor = batchFactory({
|
||||
capacity:() => plan.limit,
|
||||
owner:getOwner,
|
||||
journalName:'search-later-batch',
|
||||
queueFailureReason:'assigned but Later could not be saved',
|
||||
autoMount:false,
|
||||
claim:item => plan.prepare(item),
|
||||
queue:(confirmed, context) => {
|
||||
const outcome = laterWork?.defer(acceptIssue(confirmed), context?.until);
|
||||
return outcome === 'deferred' ? 'queued' : outcome;
|
||||
},
|
||||
onProgress:progress => {
|
||||
get('#defer-selected-search-results').disabled = progress.status === 'running';
|
||||
if (progress.status === 'running') {
|
||||
get('#search-selection-status').textContent = 'Deferring ' + progress.processed + ' of ' + progress.selected + '…';
|
||||
} else if (progress.status === 'complete') {
|
||||
get('#cmd-search-action-status').textContent = progress.failed.length ?
|
||||
progress.queued.length + ' deferred · ' + progress.failed.length + ' need retry.' :
|
||||
progress.queued.length + ' added to Later.';
|
||||
restore();
|
||||
if (!progress.failed.length) plan.cancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
function estimateValues() {
|
||||
return Object.fromEntries(Array.from(document.querySelectorAll('[data-search-batch-estimate]')).map(input =>
|
||||
[input.dataset.searchBatchEstimate, Number(input.value)]
|
||||
@@ -161,11 +188,21 @@
|
||||
if (Number.isInteger(capacity) && capacity > 0) openEstimateReview(items);
|
||||
else processor.run(items);
|
||||
});
|
||||
get('#defer-selected-search-results').addEventListener('click', event => {
|
||||
const items = plan.snapshot().items;
|
||||
if (!items.length) return;
|
||||
laterPicker?.open({ items, confirm:until => () => laterProcessor.run(
|
||||
items, {}, { until:new Date(until).toISOString() }
|
||||
) }, event.currentTarget, 'search-batch');
|
||||
});
|
||||
get('#cancel-search-batch-estimates').addEventListener('click', closeEstimateReview);
|
||||
get('#confirm-search-batch-estimates').addEventListener('click', () =>
|
||||
processor.run(plan.snapshot().items, estimateValues())
|
||||
);
|
||||
get('#resume-search-batch').addEventListener('click', () => processor.resume());
|
||||
get('#resume-search-batch').addEventListener('click', () => {
|
||||
if (processor.pending()) return processor.resume();
|
||||
return laterProcessor.resume();
|
||||
});
|
||||
get('#open-palette').addEventListener('click', () => restore());
|
||||
get('#cmd-results').addEventListener('change', event => {
|
||||
const index = event.target?.dataset?.searchSelect;
|
||||
@@ -174,7 +211,7 @@
|
||||
if (result) plan.toggle(result);
|
||||
});
|
||||
function restore() {
|
||||
const pending = processor.pending();
|
||||
const pending = processor.pending() + laterProcessor.pending();
|
||||
get('#resume-search-batch').hidden = pending === 0;
|
||||
get('#resume-search-batch').textContent = 'Resume ' + pending + ' interrupted';
|
||||
}
|
||||
@@ -188,7 +225,7 @@
|
||||
escapeHtml(result.repository) + ' #' + escapeHtml(result.number) + ' · ' +
|
||||
(allowed ? 'Open issue' : 'Not eligible') + '</span></label>';
|
||||
}
|
||||
return { plan, processor, restore, resultHtml };
|
||||
return { plan, processor, laterProcessor, restore, resultHtml };
|
||||
};
|
||||
|
||||
return createSearchBatchPlan;
|
||||
|
||||
Reference in new issue
Block a user