Queue mobile Search results into Today #798

Merged
timmy merged 1 commits from timmy/797-queue-mobile-search-results into main 2026-08-14 04:08:21 +00:00
7 changed files with 80 additions and 1 deletions

View File

@ -62,7 +62,11 @@ and durable delivery flow. A different or unconfirmed account can only copy or d
the private content. Issue capture and authored mobile actions (issue
comments, pull-request comments, notification replies, and reviews) persist per-draft
idempotency keys, so retrying after a timeout, reload, process restart, or handoff to
another worker replays a confirmed result instead of posting duplicate content. The ordered,
another worker replays a confirmed result instead of posting duplicate content. Mobile Search previews
let operators assign an eligible issue and add it to Today without starting or replacing active work.
The queue action keeps the Search query, filters, results, and scroll position available for continued
planning, reports an existing Today item without duplicating it, and uses the same capacity, sync, and
offline-warm path as other Today admission flows. The ordered,
five-item Today plan syncs across the operator's devices. **Plan Today** also stores available minutes
and a per-item estimate with the account-scoped plan, continuously showing planned/free or over-capacity
time. An over-capacity plan requires a second explicit save, legacy plans migrate with unestimated work,

View File

@ -549,6 +549,9 @@ textarea { resize: vertical; min-height: 120px; }
.search-preview-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
.search-preview-actions button, .search-preview-actions a { min-height:44px; box-sizing:border-box; display:flex; align-items:center; justify-content:center; }
.search-preview-primary-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
@media (max-width:420px) {
.search-preview-primary-actions { grid-template-columns:1fr; }
}
.search-preview-actions a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
.markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere; white-space:normal; }
.markdown-content > :first-child { margin-top:0; }

View File

@ -4330,6 +4330,7 @@
const sheet = qs('#search-preview');
const status = qs('#search-preview-status');
const claimButton = qs('#claim-search-result');
const queueButton = qs('#queue-search-result');
const startButton = qs('#start-search-result');
const shareButton = qs('#share-search-result');
@ -4342,6 +4343,7 @@
sheet.classList.add('open');
claimButton.hidden = true;
claimButton.disabled = false;
queueButton.hidden = true;
startButton.hidden = true;
startButton.disabled = false;
shareButton.disabled = true;
@ -4376,6 +4378,9 @@
claimButton.hidden = !(detail.claimable || detail.assigned_to_me);
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
claimButton.disabled = state.status === 'claiming';
queueButton.hidden = !(detail.kind === 'issue' && detail.state === 'open' &&
(detail.claimable || detail.assigned_to_me));
queueButton.textContent = detail.assigned_to_me ? 'Add to Today' : 'Assign & add to Today';
startButton.hidden = !(detail.reviewable || (detail.kind === 'issue' &&
(detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me)))));
startButton.textContent = detail.reviewable ? 'Review now' : detail.reopenable ? 'Reopen & resume' :
@ -4423,6 +4428,7 @@
refreshMyWorkView();
return createAndStart.complete(item);
},
queue: confirmed => queueToday(acceptClaimedIssue(confirmed)),
recover: confirmed => {
const item = acceptClaimedIssue(confirmed);
taskOverlayHistory.leave();
@ -4431,6 +4437,7 @@
},
announce: message => {
qs('#search-preview-status').textContent = message;
qs('#cmd-search-action-status').textContent = message;
qs('#my-work-action-status').textContent = message;
},
});
@ -4729,6 +4736,25 @@
qs('#search-preview-status').textContent = error.message + ' Retry assignment.';
}
});
qs('#queue-search-result').addEventListener('click', async () => {
const detail = searchPreviewDetail;
if (!detail || detail.kind !== 'issue' || detail.state !== 'open' ||
(!detail.claimable && !detail.assigned_to_me)) return;
try {
if (todayWork.contains(detail)) {
qs('#cmd-search-action-status').textContent = 'Already in Today.';
closeSearchPreview();
return;
}
const outcome = await searchAssignAndStart.run(detail, {
alreadyOwned: detail.assigned_to_me,
destination: 'queue',
});
if (outcome === 'queued') closeSearchPreview();
} catch (error) {
qs('#search-preview-status').textContent = error.message + ' Retry.';
}
});
qs('#start-search-result').addEventListener('click', async () => {
const detail = searchPreviewDetail;
if (detail?.reviewable) {

View File

@ -448,6 +448,7 @@
<datalist id="cmd-search-repositories"></datalist>
<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>
<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>
@ -467,6 +468,7 @@
<div class="search-preview-actions">
<div class="search-preview-primary-actions">
<button id="claim-search-result" type="button" hidden>Assign to me</button>
<button id="queue-search-result" type="button" hidden>Assign &amp; add to Today</button>
<button id="start-search-result" type="button" hidden>Assign &amp; start</button>
</div>

View File

@ -30,6 +30,7 @@ FEATURE_SOURCES = {
"today-timer": (
"static/commands.js", "static/task-overlay-history.js", "static/search-preview.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",
),

View File

@ -513,6 +513,45 @@ def test_search_preview_offers_assign_and_start_for_eligible_issues():
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
def test_search_preview_queues_eligible_issue_and_returns_to_preserved_search():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
assert 'id="queue-search-result"' in html
assert "queueButton.hidden = !(detail.kind === 'issue'" in html
assert "detail.assigned_to_me ? 'Add to Today' : 'Assign & add to Today'" in html
orchestrator = html.split("function createSearchStart(claim)", 1)[1].split(
"const searchAssignAndStart", 1
)[0]
assert "queue: confirmed => queueToday(acceptClaimedIssue(confirmed))" in orchestrator
handler = html.split("qs('#queue-search-result').addEventListener('click'", 1)[1].split(
"qs('#start-search-result')", 1
)[0]
assert "destination: 'queue'" in handler
assert "alreadyOwned: detail.assigned_to_me" in handler
assert "closeSearchPreview()" in handler
assert "mobileSearchViewport.restoreScroll()" in html
assert "@media (max-width:420px)" in css
assert ".search-preview-primary-actions { grid-template-columns:1fr; }" in css
def test_search_queue_confirmation_is_accessible_and_existing_today_item_is_not_readded():
html = dashboard_bundle_text()
assert 'id="cmd-search-action-status"' in html
assert 'aria-live="polite"' in html
orchestrator = html.split("function createSearchStart(claim)", 1)[1].split(
"const searchAssignAndStart", 1
)[0]
assert "qs('#cmd-search-action-status').textContent = message" in orchestrator
handler = html.split("qs('#queue-search-result').addEventListener('click'", 1)[1].split(
"qs('#start-search-result')", 1
)[0]
assert "todayWork.contains(detail)" in handler
assert "Already in Today." in handler
assert handler.index("todayWork.contains(detail)") < handler.index("searchAssignAndStart.run")
def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard():
html = dashboard_bundle_text()

View File

@ -68,6 +68,10 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert b"function attachSecurityCenter" in security_center.runtime_bytes
assert b"function createGlobalSearchController" not in first.runtime_bytes
assert b"function createGlobalSearchController" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createAssignAndStart" not in first.runtime_bytes
assert b"function createAssignAndStart" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createQueueToday" not in first.runtime_bytes
assert b"function createQueueToday" in first.feature_bundles["today-timer"].runtime_bytes
assert b"gitea_time_logged" not in first.runtime_bytes
assert b"gitea_time_logged" in security_center.runtime_bytes
# Core mobile workflows stay below 98 KiB gzip, including transaction-safe Update decisions.