Merge pull request 'Queue mobile Search results into Today' (#798) from timmy/797-queue-mobile-search-results into main
This commit is contained in:
commit
0a8f33dc83
|
|
@ -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
|
the private content. Issue capture and authored mobile actions (issue
|
||||||
comments, pull-request comments, notification replies, and reviews) persist per-draft
|
comments, pull-request comments, notification replies, and reviews) persist per-draft
|
||||||
idempotency keys, so retrying after a timeout, reload, process restart, or handoff to
|
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
|
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
|
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,
|
time. An over-capacity plan requires a second explicit save, legacy plans migrate with unestimated work,
|
||||||
|
|
|
||||||
|
|
@ -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 { 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-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; }
|
.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; }
|
.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 { min-width:0; max-width:100%; overflow-wrap:anywhere; white-space:normal; }
|
||||||
.markdown-content > :first-child { margin-top:0; }
|
.markdown-content > :first-child { margin-top:0; }
|
||||||
|
|
|
||||||
|
|
@ -4330,6 +4330,7 @@
|
||||||
const sheet = qs('#search-preview');
|
const sheet = qs('#search-preview');
|
||||||
const status = qs('#search-preview-status');
|
const status = qs('#search-preview-status');
|
||||||
const claimButton = qs('#claim-search-result');
|
const claimButton = qs('#claim-search-result');
|
||||||
|
const queueButton = qs('#queue-search-result');
|
||||||
const startButton = qs('#start-search-result');
|
const startButton = qs('#start-search-result');
|
||||||
const shareButton = qs('#share-search-result');
|
const shareButton = qs('#share-search-result');
|
||||||
|
|
||||||
|
|
@ -4342,6 +4343,7 @@
|
||||||
sheet.classList.add('open');
|
sheet.classList.add('open');
|
||||||
claimButton.hidden = true;
|
claimButton.hidden = true;
|
||||||
claimButton.disabled = false;
|
claimButton.disabled = false;
|
||||||
|
queueButton.hidden = true;
|
||||||
startButton.hidden = true;
|
startButton.hidden = true;
|
||||||
startButton.disabled = false;
|
startButton.disabled = false;
|
||||||
shareButton.disabled = true;
|
shareButton.disabled = true;
|
||||||
|
|
@ -4376,6 +4378,9 @@
|
||||||
claimButton.hidden = !(detail.claimable || detail.assigned_to_me);
|
claimButton.hidden = !(detail.claimable || detail.assigned_to_me);
|
||||||
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
|
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
|
||||||
claimButton.disabled = state.status === 'claiming';
|
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' &&
|
startButton.hidden = !(detail.reviewable || (detail.kind === 'issue' &&
|
||||||
(detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me)))));
|
(detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me)))));
|
||||||
startButton.textContent = detail.reviewable ? 'Review now' : detail.reopenable ? 'Reopen & resume' :
|
startButton.textContent = detail.reviewable ? 'Review now' : detail.reopenable ? 'Reopen & resume' :
|
||||||
|
|
@ -4423,6 +4428,7 @@
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
return createAndStart.complete(item);
|
return createAndStart.complete(item);
|
||||||
},
|
},
|
||||||
|
queue: confirmed => queueToday(acceptClaimedIssue(confirmed)),
|
||||||
recover: confirmed => {
|
recover: confirmed => {
|
||||||
const item = acceptClaimedIssue(confirmed);
|
const item = acceptClaimedIssue(confirmed);
|
||||||
taskOverlayHistory.leave();
|
taskOverlayHistory.leave();
|
||||||
|
|
@ -4431,6 +4437,7 @@
|
||||||
},
|
},
|
||||||
announce: message => {
|
announce: message => {
|
||||||
qs('#search-preview-status').textContent = message;
|
qs('#search-preview-status').textContent = message;
|
||||||
|
qs('#cmd-search-action-status').textContent = message;
|
||||||
qs('#my-work-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('#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 () => {
|
qs('#start-search-result').addEventListener('click', async () => {
|
||||||
const detail = searchPreviewDetail;
|
const detail = searchPreviewDetail;
|
||||||
if (detail?.reviewable) {
|
if (detail?.reviewable) {
|
||||||
|
|
|
||||||
|
|
@ -448,6 +448,7 @@
|
||||||
<datalist id="cmd-search-repositories"></datalist>
|
<datalist id="cmd-search-repositories"></datalist>
|
||||||
<span id="cmd-repository-status" class="cmd-repository-status" aria-live="polite"></span>
|
<span id="cmd-repository-status" class="cmd-repository-status" aria-live="polite"></span>
|
||||||
</fieldset>
|
</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>
|
<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>
|
<button id="cmd-load-more" hidden>More results</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -467,6 +468,7 @@
|
||||||
<div class="search-preview-actions">
|
<div class="search-preview-actions">
|
||||||
<div class="search-preview-primary-actions">
|
<div class="search-preview-primary-actions">
|
||||||
<button id="claim-search-result" type="button" hidden>Assign to me</button>
|
<button id="claim-search-result" type="button" hidden>Assign to me</button>
|
||||||
|
<button id="queue-search-result" type="button" hidden>Assign & add to Today</button>
|
||||||
<button id="start-search-result" type="button" hidden>Assign & start</button>
|
<button id="start-search-result" type="button" hidden>Assign & start</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ FEATURE_SOURCES = {
|
||||||
"today-timer": (
|
"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/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/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/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/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
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():
|
def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard():
|
||||||
html = dashboard_bundle_text()
|
html = dashboard_bundle_text()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 attachSecurityCenter" in security_center.runtime_bytes
|
||||||
assert b"function createGlobalSearchController" not in first.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 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" not in first.runtime_bytes
|
||||||
assert b"gitea_time_logged" in security_center.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.
|
# Core mobile workflows stay below 98 KiB gzip, including transaction-safe Update decisions.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user