feat: guide mobile delivery recovery (Closes #1014)
This commit is contained in:
parent
c0a02a05d8
commit
1e4f101219
|
|
@ -1031,6 +1031,14 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
||||||
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||||
.mobile-queue-panel h2 { margin:0; }
|
.mobile-queue-panel h2 { margin:0; }
|
||||||
|
.mobile-delivery-recovery { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||||
|
.mobile-delivery-recovery::backdrop { background:rgba(3,9,18,.78); }
|
||||||
|
.mobile-delivery-recovery-panel { box-sizing:border-box; display:grid; gap:12px; width:100%; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); overflow-wrap:anywhere; }
|
||||||
|
.mobile-delivery-recovery-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||||
|
.mobile-delivery-recovery-panel h2, .mobile-delivery-recovery-panel p { margin:0; }
|
||||||
|
.mobile-delivery-recovery-panel button { min-height:48px; }
|
||||||
|
.mobile-delivery-recovery-panel > #mobile-delivery-recovery-action { width:100%; font-weight:800; }
|
||||||
|
.delivery-recovery-reason { padding:12px; border:1px solid #31577f; border-radius:12px; background:#0b1c30; }
|
||||||
.mobile-start-day { display:grid; gap:12px; max-width:100%; overflow-wrap:anywhere; margin-top:12px; padding:14px; border:1px solid #31577f; border-radius:14px; background:linear-gradient(135deg,#173b64,#102641); }
|
.mobile-start-day { display:grid; gap:12px; max-width:100%; overflow-wrap:anywhere; margin-top:12px; padding:14px; border:1px solid #31577f; border-radius:14px; background:linear-gradient(135deg,#173b64,#102641); }
|
||||||
.mobile-start-day h3, .mobile-start-day p { margin:0; }
|
.mobile-start-day h3, .mobile-start-day p { margin:0; }
|
||||||
.mobile-start-day p + p { margin-top:4px; }
|
.mobile-start-day p + p { margin-top:4px; }
|
||||||
|
|
|
||||||
|
|
@ -91,14 +91,15 @@
|
||||||
qs('#my-work').focus();
|
qs('#my-work').focus();
|
||||||
}
|
}
|
||||||
let mobileQueueCounts = {};
|
let mobileQueueCounts = {};
|
||||||
function openDeliveryRecovery() {
|
const mobileDeliveryRecovery = createMobileDeliveryRecovery({
|
||||||
selectMobileQueue('draft');
|
getItems: () => draftInbox.partition(lastDrafts).deliveries,
|
||||||
const deliveryCenterTitle = qs('#delivery-center-title');
|
getIndex: item => lastDrafts.indexOf(item),
|
||||||
if (!deliveryCenterTitle) return 'empty';
|
beforeOpen: () => selectMobileQueue('draft'),
|
||||||
deliveryCenterTitle.scrollIntoView({block:'start'});
|
onComplete: () => {
|
||||||
deliveryCenterTitle.focus({preventScroll:true});
|
if (!mobileStartDay.completePhase('delivery')) showMobileQueueCompletion('Delivery', true);
|
||||||
return 'opened';
|
},
|
||||||
}
|
});
|
||||||
|
mobileDeliveryRecovery.start();
|
||||||
function openFiledFollowUp() {
|
function openFiledFollowUp() {
|
||||||
selectMobileQueue('filed');
|
selectMobileQueue('filed');
|
||||||
const target = filedFollowUpTarget(completedFiledReview.visible(lastMyWork));
|
const target = filedFollowUpTarget(completedFiledReview.visible(lastMyWork));
|
||||||
|
|
@ -112,7 +113,7 @@
|
||||||
return target.kind === 'update' ? 'opened-update' : 'opened-issue';
|
return target.kind === 'update' ? 'opened-update' : 'opened-issue';
|
||||||
}
|
}
|
||||||
const mobileQueueLauncher = createMobileQueueLauncher({
|
const mobileQueueLauncher = createMobileQueueLauncher({
|
||||||
openDelivery: openDeliveryRecovery,
|
openDelivery: () => mobileDeliveryRecovery.open(),
|
||||||
openToday: () => mobileWorkEntry.open(),
|
openToday: () => mobileWorkEntry.open(),
|
||||||
openAgenda: openAgendaSession,
|
openAgenda: openAgendaSession,
|
||||||
openUpdates: openUpdateTriage,
|
openUpdates: openUpdateTriage,
|
||||||
|
|
@ -3338,6 +3339,7 @@
|
||||||
qs('#my-work-action-status').textContent = 'Draft discarded.';
|
qs('#my-work-action-status').textContent = 'Draft discarded.';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
mobileDeliveryRecovery.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWorkPaginationControls() {
|
function updateWorkPaginationControls() {
|
||||||
|
|
|
||||||
|
|
@ -1543,6 +1543,20 @@
|
||||||
</section>
|
</section>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="mobile-delivery-recovery" class="mobile-delivery-recovery" aria-labelledby="mobile-delivery-recovery-title">
|
||||||
|
<section class="mobile-delivery-recovery-panel">
|
||||||
|
<header>
|
||||||
|
<div><p class="small muted" id="mobile-delivery-recovery-progress"></p><h2 id="mobile-delivery-recovery-title">Recover delivery</h2></div>
|
||||||
|
<button id="close-mobile-delivery-recovery" type="button">Close</button>
|
||||||
|
</header>
|
||||||
|
<p id="mobile-delivery-recovery-destination" class="small"></p>
|
||||||
|
<p id="mobile-delivery-recovery-reason" class="delivery-recovery-reason"></p>
|
||||||
|
<p id="mobile-delivery-recovery-status" class="small" role="status" aria-live="polite"></p>
|
||||||
|
<button id="mobile-delivery-recovery-action" type="button">Recover delivery</button>
|
||||||
|
<details><summary>Other recovery options</summary><p class="small muted">Close this guide to edit, copy, or discard the saved delivery from Drafts.</p></details>
|
||||||
|
</section>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">
|
<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">
|
||||||
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button>
|
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button>
|
||||||
<button class="mobile-task-action" data-mobile-task="find" type="button">Find</button>
|
<button class="mobile-task-action" data-mobile-task="find" type="button">Find</button>
|
||||||
|
|
@ -1639,6 +1653,7 @@
|
||||||
<script src="static/mobile-pull-refresh.js"></script>
|
<script src="static/mobile-pull-refresh.js"></script>
|
||||||
<script src="static/mobile-work-entry.js"></script>
|
<script src="static/mobile-work-entry.js"></script>
|
||||||
<script src="static/mobile-queue-launcher.js"></script>
|
<script src="static/mobile-queue-launcher.js"></script>
|
||||||
|
<script src="static/mobile-delivery-recovery.js"></script>
|
||||||
<script src="static/mobile-start-day.js"></script>
|
<script src="static/mobile-start-day.js"></script>
|
||||||
<script src="static/update-triage-session.js"></script>
|
<script src="static/update-triage-session.js"></script>
|
||||||
<script src="static/update-review-handoff.js"></script>
|
<script src="static/update-review-handoff.js"></script>
|
||||||
|
|
|
||||||
148
frontend/mobile-delivery-recovery.js
Normal file
148
frontend/mobile-delivery-recovery.js
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else root.createMobileDeliveryRecovery = factory;
|
||||||
|
})(typeof self !== 'undefined' ? self : this, function createMobileDeliveryRecovery(options) {
|
||||||
|
const byId = id => typeof document === 'undefined' ? null : document.getElementById(id);
|
||||||
|
const elements = options.elements || (typeof document === 'undefined' ? null : {
|
||||||
|
dialog: byId('mobile-delivery-recovery'), close: byId('close-mobile-delivery-recovery'),
|
||||||
|
title: byId('mobile-delivery-recovery-title'), destination: byId('mobile-delivery-recovery-destination'),
|
||||||
|
reason: byId('mobile-delivery-recovery-reason'), progress: byId('mobile-delivery-recovery-progress'),
|
||||||
|
action: byId('mobile-delivery-recovery-action'), status: byId('mobile-delivery-recovery-status'),
|
||||||
|
});
|
||||||
|
const priority = {authorization: 0, attention: 1, uncertain: 2, waiting: 3};
|
||||||
|
|
||||||
|
function identity(item) {
|
||||||
|
return String(item?.outbox_id || item?.id || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(item) {
|
||||||
|
if (item?.status === 'authorization') return 'authorization';
|
||||||
|
if (item?.status === 'attention' || item?.checklist_conflict || item?.quarantined) return 'attention';
|
||||||
|
if (item?.delivery_state === 'uncertain') return 'uncertain';
|
||||||
|
return 'waiting';
|
||||||
|
}
|
||||||
|
|
||||||
|
function primary(item) {
|
||||||
|
const kind = state(item);
|
||||||
|
if (kind === 'authorization') {
|
||||||
|
return item?.outbox_kind === 'issue-close' ? 'Authorize & close' : 'Authorize & send';
|
||||||
|
}
|
||||||
|
if (item?.checklist_conflict) return 'Review changes';
|
||||||
|
if (kind === 'attention') return item?.quarantined ? 'Copy content' : 'Open current item';
|
||||||
|
if (kind === 'uncertain') return 'Verified not posted — retry';
|
||||||
|
return 'Retry now';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ordered() {
|
||||||
|
return [...(options.getItems ? options.getItems() : [])].sort((left, right) => {
|
||||||
|
const byState = priority[state(left)] - priority[state(right)];
|
||||||
|
if (byState) return byState;
|
||||||
|
return identity(left).localeCompare(identity(right));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateLabels = {
|
||||||
|
authorization: 'Authorization required',
|
||||||
|
attention: 'Needs attention',
|
||||||
|
uncertain: 'Delivery uncertain',
|
||||||
|
waiting: 'Ready to retry',
|
||||||
|
};
|
||||||
|
let currentId = '';
|
||||||
|
let active = false;
|
||||||
|
|
||||||
|
function currentItem(items) {
|
||||||
|
return items.find(item => identity(item) === currentId) || items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot() {
|
||||||
|
const items = ordered();
|
||||||
|
const item = currentItem(items);
|
||||||
|
if (item) currentId = identity(item);
|
||||||
|
else currentId = '';
|
||||||
|
const index = item ? items.indexOf(item) : -1;
|
||||||
|
return {
|
||||||
|
count: items.length,
|
||||||
|
current: item ? {
|
||||||
|
id: identity(item),
|
||||||
|
state: state(item),
|
||||||
|
primary: primary(item),
|
||||||
|
position: index + 1,
|
||||||
|
total: items.length,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const result = snapshot();
|
||||||
|
if (!elements) return result;
|
||||||
|
const items = ordered();
|
||||||
|
const item = currentItem(items);
|
||||||
|
if (!item) {
|
||||||
|
if (active) {
|
||||||
|
active = false;
|
||||||
|
if (elements.dialog?.open) elements.dialog.close();
|
||||||
|
if (options.onComplete) options.onComplete();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
elements.title.textContent = item.title || 'Queued delivery';
|
||||||
|
elements.destination.textContent = [item.repository, item.details].filter(Boolean).join(' · ') || 'Saved delivery';
|
||||||
|
elements.reason.textContent = item.last_attempt_error || item.ownership || stateLabels[state(item)];
|
||||||
|
elements.progress.textContent = 'Delivery ' + result.current.position + ' of ' + result.current.total + ' · ' + stateLabels[state(item)];
|
||||||
|
elements.action.textContent = primary(item);
|
||||||
|
if (elements.status) elements.status.textContent = '';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
const queue = byId('mobile-queue-sheet');
|
||||||
|
if (queue?.open) queue.close();
|
||||||
|
if (options.beforeOpen) options.beforeOpen();
|
||||||
|
currentId = '';
|
||||||
|
const result = snapshot();
|
||||||
|
if (!result.current) return 'empty';
|
||||||
|
active = true;
|
||||||
|
render();
|
||||||
|
if (elements?.dialog && !elements.dialog.open) elements.dialog.showModal();
|
||||||
|
return 'opened';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultActivate(item) {
|
||||||
|
if (typeof document === 'undefined') return false;
|
||||||
|
const index = options.getIndex ? options.getIndex(item) : -1;
|
||||||
|
const selector = item.status === 'authorization' ? '.draft-authorize' :
|
||||||
|
item.checklist_conflict ? '.draft-review-checklist' : item.quarantined ? '.draft-copy' :
|
||||||
|
item.status === 'attention' ? '.draft-resume' : '.draft-send';
|
||||||
|
const action = document.querySelector('#my-work-list ' + selector + '[data-draft-index="' + index + '"]');
|
||||||
|
if (!action) {
|
||||||
|
if (elements.status) elements.status.textContent = 'This delivery changed. Reopen recovery to load its current action.';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.click();
|
||||||
|
if (selector === '.draft-resume' || selector === '.draft-copy') elements.dialog.close();
|
||||||
|
await Promise.resolve();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activate() {
|
||||||
|
const items = ordered();
|
||||||
|
const item = currentItem(items);
|
||||||
|
const perform = options.activate || defaultActivate;
|
||||||
|
if (!item) return render();
|
||||||
|
if (elements?.action) elements.action.disabled = true;
|
||||||
|
if (elements?.status) elements.status.textContent = 'Working…';
|
||||||
|
try {
|
||||||
|
await perform(item, state(item));
|
||||||
|
} finally {
|
||||||
|
if (elements?.action) elements.action.disabled = false;
|
||||||
|
}
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (elements?.action?.addEventListener) elements.action.addEventListener('click', activate);
|
||||||
|
if (elements?.close?.addEventListener) elements.close.addEventListener('click', () => elements.dialog.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {activate, open, render, snapshot, start};
|
||||||
|
});
|
||||||
|
|
@ -106,6 +106,7 @@ const SHELL = [
|
||||||
BASE + 'static/mobile-task-dock.js',
|
BASE + 'static/mobile-task-dock.js',
|
||||||
BASE + 'static/mobile-work-entry.js',
|
BASE + 'static/mobile-work-entry.js',
|
||||||
BASE + 'static/mobile-queue-launcher.js',
|
BASE + 'static/mobile-queue-launcher.js',
|
||||||
|
BASE + 'static/mobile-delivery-recovery.js',
|
||||||
BASE + 'static/mobile-start-day.js',
|
BASE + 'static/mobile-start-day.js',
|
||||||
BASE + 'static/update-triage-session.js',
|
BASE + 'static/update-triage-session.js',
|
||||||
BASE + 'static/update-review-handoff.js',
|
BASE + 'static/update-review-handoff.js',
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ FEATURE_SOURCES = {
|
||||||
"security-center": ("static/security-center.js",),
|
"security-center": ("static/security-center.js",),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
|
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
|
||||||
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.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-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-start-day.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-wrap-up.js", "static/today-handoff.js",
|
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.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-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.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-wrap-up.js", "static/today-handoff.js",
|
||||||
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||||
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.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",
|
||||||
|
|
|
||||||
|
|
@ -379,8 +379,8 @@ async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_w
|
||||||
assert '.delivery-center { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;' in html
|
assert '.delivery-center { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;' in html
|
||||||
assert '.delivery-center button { min-height:44px;' in html
|
assert '.delivery-center button { min-height:44px;' in html
|
||||||
assert "counts.delivery = draftInbox.partition(lastDrafts).actionable;" in html
|
assert "counts.delivery = draftInbox.partition(lastDrafts).actionable;" in html
|
||||||
assert "openDelivery: openDeliveryRecovery" in html
|
assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
|
||||||
assert "deliveryCenterTitle.focus({preventScroll:true});" in html
|
assert "getItems: () => draftInbox.partition(lastDrafts).deliveries" in html
|
||||||
assert "authoritativePhases:['delivery']" in html
|
assert "authoritativePhases:['delivery']" in html
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
166
tests/test_mobile_delivery_recovery.py
Normal file
166
tests/test_mobile_delivery_recovery.py
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.dashboard_bundle import dashboard
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RECOVERY = ROOT / "frontend" / "mobile-delivery-recovery.js"
|
||||||
|
|
||||||
|
|
||||||
|
def run_node(script: str) -> dict:
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script],
|
||||||
|
cwd=ROOT,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_orders_deliveries_by_safest_required_intervention():
|
||||||
|
script = f"""
|
||||||
|
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||||
|
const recovery = createRecovery({{
|
||||||
|
getItems: () => [
|
||||||
|
{{outbox_id:'waiting', delivery_state:'waiting', title:'Waiting'}},
|
||||||
|
{{outbox_id:'uncertain', delivery_state:'uncertain', title:'Uncertain'}},
|
||||||
|
{{outbox_id:'attention', status:'attention', title:'Attention'}},
|
||||||
|
{{outbox_id:'authorization', status:'authorization', title:'Authorization'}},
|
||||||
|
],
|
||||||
|
}});
|
||||||
|
process.stdout.write(JSON.stringify(recovery.snapshot()));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"count": 4,
|
||||||
|
"current": {
|
||||||
|
"id": "authorization",
|
||||||
|
"state": "authorization",
|
||||||
|
"primary": "Authorize & send",
|
||||||
|
"position": 1,
|
||||||
|
"total": 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_advances_only_after_the_current_delivery_is_resolved():
|
||||||
|
script = f"""
|
||||||
|
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||||
|
(async () => {{
|
||||||
|
let items = [
|
||||||
|
{{outbox_id:'authorize', status:'authorization', title:'Approve review'}},
|
||||||
|
{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}},
|
||||||
|
];
|
||||||
|
let resolves = false;
|
||||||
|
const recovery = createRecovery({{
|
||||||
|
getItems: () => items,
|
||||||
|
activate: async item => {{
|
||||||
|
if (resolves) items = items.filter(candidate => candidate.outbox_id !== item.outbox_id);
|
||||||
|
return resolves;
|
||||||
|
}},
|
||||||
|
}});
|
||||||
|
recovery.open();
|
||||||
|
const retained = await recovery.activate();
|
||||||
|
resolves = true;
|
||||||
|
const advanced = await recovery.activate();
|
||||||
|
process.stdout.write(JSON.stringify({{retained, advanced}}));
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"retained": {
|
||||||
|
"count": 2,
|
||||||
|
"current": {
|
||||||
|
"id": "authorize",
|
||||||
|
"state": "authorization",
|
||||||
|
"primary": "Authorize & send",
|
||||||
|
"position": 1,
|
||||||
|
"total": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"count": 1,
|
||||||
|
"current": {
|
||||||
|
"id": "retry",
|
||||||
|
"state": "waiting",
|
||||||
|
"primary": "Retry now",
|
||||||
|
"position": 1,
|
||||||
|
"total": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_renders_delivery_context_and_hands_off_when_cleared():
|
||||||
|
script = f"""
|
||||||
|
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||||
|
(async () => {{
|
||||||
|
let items = [{{
|
||||||
|
outbox_id:'close-7', status:'authorization', outbox_kind:'issue-close',
|
||||||
|
title:'Close resolved incident', repository:'stackchain/ops',
|
||||||
|
details:'#7', last_attempt_error:'Fresh authorization expired',
|
||||||
|
}}];
|
||||||
|
let completed = 0;
|
||||||
|
const element = () => ({{textContent:'', hidden:false}});
|
||||||
|
const elements = {{
|
||||||
|
dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
|
||||||
|
title: element(), destination: element(), reason: element(), progress: element(),
|
||||||
|
action: element(), status: element(),
|
||||||
|
}};
|
||||||
|
const recovery = createRecovery({{
|
||||||
|
getItems: () => items,
|
||||||
|
activate: async () => {{items = []; return true}},
|
||||||
|
onComplete: () => {{completed += 1}},
|
||||||
|
elements,
|
||||||
|
}});
|
||||||
|
const opened = recovery.open();
|
||||||
|
const rendered = {{
|
||||||
|
opened, dialogOpen:elements.dialog.open, title:elements.title.textContent,
|
||||||
|
destination:elements.destination.textContent, reason:elements.reason.textContent,
|
||||||
|
progress:elements.progress.textContent, action:elements.action.textContent,
|
||||||
|
}};
|
||||||
|
const cleared = await recovery.activate();
|
||||||
|
process.stdout.write(JSON.stringify({{rendered, cleared, completed, dialogOpen:elements.dialog.open}}));
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"rendered": {
|
||||||
|
"opened": "opened",
|
||||||
|
"dialogOpen": True,
|
||||||
|
"title": "Close resolved incident",
|
||||||
|
"destination": "stackchain/ops · #7",
|
||||||
|
"reason": "Fresh authorization expired",
|
||||||
|
"progress": "Delivery 1 of 1 · Authorization required",
|
||||||
|
"action": "Authorize & close",
|
||||||
|
},
|
||||||
|
"cleared": {"count": 0, "current": None},
|
||||||
|
"completed": 1,
|
||||||
|
"dialogOpen": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_dashboard_packages_phone_safe_guided_delivery_recovery():
|
||||||
|
html = await dashboard()
|
||||||
|
service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
|
||||||
|
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
|
||||||
|
|
||||||
|
assert '<dialog id="mobile-delivery-recovery"' in html
|
||||||
|
assert 'aria-labelledby="mobile-delivery-recovery-title"' in html
|
||||||
|
assert 'id="mobile-delivery-recovery-action"' in html
|
||||||
|
assert '<script src="static/mobile-delivery-recovery.js"></script>' in html
|
||||||
|
assert "const mobileDeliveryRecovery = createMobileDeliveryRecovery({" in html
|
||||||
|
assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
|
||||||
|
assert "mobileStartDay.completePhase('delivery')" in html
|
||||||
|
assert "BASE + 'static/mobile-delivery-recovery.js'" in service_worker
|
||||||
|
assert '"static/mobile-delivery-recovery.js"' in bundle
|
||||||
|
assert ".mobile-delivery-recovery-panel" in html
|
||||||
|
assert "min-height:48px" in html
|
||||||
|
assert "env(safe-area-inset-bottom)" in html
|
||||||
|
|
@ -1043,6 +1043,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/mobile-task-dock.js",
|
"/dashboard/static/mobile-task-dock.js",
|
||||||
"/dashboard/static/mobile-work-entry.js",
|
"/dashboard/static/mobile-work-entry.js",
|
||||||
"/dashboard/static/mobile-queue-launcher.js",
|
"/dashboard/static/mobile-queue-launcher.js",
|
||||||
|
"/dashboard/static/mobile-delivery-recovery.js",
|
||||||
"/dashboard/static/mobile-start-day.js",
|
"/dashboard/static/mobile-start-day.js",
|
||||||
"/dashboard/static/update-triage-session.js",
|
"/dashboard/static/update-triage-session.js",
|
||||||
"/dashboard/static/update-review-handoff.js",
|
"/dashboard/static/update-review-handoff.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user