feat: launch Protect Today from deadline reminders (Closes #723)
This commit is contained in:
parent
cbe4017bdc
commit
8a12524595
|
|
@ -6058,10 +6058,27 @@
|
|||
if (!preserveRoute) workRoute.queue(filter);
|
||||
return true;
|
||||
}
|
||||
function openWorkQueueRoute(filter) {
|
||||
async function openWorkQueueRoute(filter, action = null) {
|
||||
if (!selectWorkQueue(filter, { preserveRoute:true })) return;
|
||||
qs('#my-work').scrollIntoView({block:'start'});
|
||||
qs('#my-work').focus();
|
||||
if (action !== 'protect-today') return;
|
||||
await completeAgendaIssues();
|
||||
if (workPagination.issue?.has_more) {
|
||||
qs('#my-work-action-status').textContent =
|
||||
'Protect Today needs all assigned deadlines. Retry when connected.';
|
||||
return;
|
||||
}
|
||||
const proposal = protectToday.propose({
|
||||
agenda:agendaMyWork(activeMyWork), today:todayMyWork,
|
||||
identity:item => todayWork.identity(item), limit:todayWork.limit,
|
||||
});
|
||||
if (!proposal.protected.length) {
|
||||
qs('#protect-today-status').textContent = 'No overdue or due-today work needs protection.';
|
||||
return;
|
||||
}
|
||||
pendingProtectToday = proposal;
|
||||
openPlanToday(qs('#protect-today'));
|
||||
}
|
||||
qs('#work-milestone-filter').addEventListener('change', event => {
|
||||
selectedWorkMilestone = event.target.value;
|
||||
|
|
|
|||
|
|
@ -314,12 +314,14 @@ self.addEventListener('push', event => {
|
|||
try { payload = event.data?.json?.() || {}; }
|
||||
catch (_error) { return; }
|
||||
const route = String(payload.route || '');
|
||||
const protectRoute = String(payload.protect_route || '');
|
||||
const tag = String(payload.tag || '');
|
||||
const notificationId = Number(payload.notification_id);
|
||||
const updateCount = Number(payload.update_count);
|
||||
const deadlineCount = Number(payload.deadline_count);
|
||||
if (
|
||||
route === '#/my-work/agenda'
|
||||
&& protectRoute === '#/my-work/agenda/protect-today'
|
||||
&& /^stackchain-deadline-digest-\d{4}-\d{2}-\d{2}$/.test(tag)
|
||||
&& Number.isSafeInteger(deadlineCount)
|
||||
&& deadlineCount > 0
|
||||
|
|
@ -330,7 +332,11 @@ self.addEventListener('push', event => {
|
|||
{
|
||||
body: 'Open Agenda to review or replan ' + (deadlineCount === 1 ? 'it.' : 'them.'),
|
||||
tag,
|
||||
data: {route},
|
||||
actions: [
|
||||
{ action: 'protect-today', title: 'Protect Today' },
|
||||
{ action: 'open-agenda', title: 'Open Agenda' },
|
||||
],
|
||||
data: {route, protectRoute},
|
||||
}
|
||||
));
|
||||
return;
|
||||
|
|
@ -380,6 +386,22 @@ async function openWorkRoute(route) {
|
|||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
const route = String(event.notification.data?.route || '');
|
||||
if (event.action === 'protect-today') {
|
||||
const protectRoute = String(event.notification.data?.protectRoute || route);
|
||||
if (
|
||||
protectRoute !== '#/my-work/agenda/protect-today'
|
||||
|| !['#/my-work/agenda', protectRoute].includes(route)
|
||||
) return;
|
||||
event.notification.close();
|
||||
event.waitUntil(openWorkRoute(protectRoute));
|
||||
return;
|
||||
}
|
||||
if (event.action === 'open-agenda') {
|
||||
if (route !== '#/my-work/agenda') return;
|
||||
event.notification.close();
|
||||
event.waitUntil(openWorkRoute(route));
|
||||
return;
|
||||
}
|
||||
if (event.action === 'tomorrow') {
|
||||
const notificationId = Number(event.notification.data?.notificationId);
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
if (queueFilters.includes(queue) && parts.length === 3) {
|
||||
return { kind: 'queue', filter: queue };
|
||||
}
|
||||
if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
|
||||
return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
|
||||
}
|
||||
if (parts[2] === 'update' && parts.length === 4) {
|
||||
const notificationId = positiveInteger(parts[3]);
|
||||
return notificationId ? { kind: 'update', notification_id: notificationId } : null;
|
||||
|
|
@ -114,7 +117,7 @@
|
|||
if (active && active !== fragment) onClose();
|
||||
if (active === fragment) return;
|
||||
active = fragment;
|
||||
onQueue(route.filter);
|
||||
onQueue(route.filter, route.action || null);
|
||||
return;
|
||||
}
|
||||
const item = items.find(candidate => sameRoute(candidate, route));
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ async def _dispatch_deadline_reminders_unlocked(
|
|||
"title": f"{due_count} deadline{'s' if due_count != 1 else ''} need{'s' if due_count == 1 else ''} attention",
|
||||
"body": f"Open Agenda to review or replan {'it' if due_count == 1 else 'them'}.",
|
||||
"route": "#/my-work/agenda",
|
||||
"protect_route": "#/my-work/agenda/protect-today",
|
||||
"tag": f"stackchain-deadline-digest-{local_day}",
|
||||
"deadline_count": due_count,
|
||||
}, separators=(",", ":"))
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ async def test_deadline_reminder_sends_one_private_local_day_digest_and_deduplic
|
|||
"title": "1 deadline needs attention",
|
||||
"body": "Open Agenda to review or replan it.",
|
||||
"route": "#/my-work/agenda",
|
||||
"protect_route": "#/my-work/agenda/protect-today",
|
||||
"tag": "stackchain-deadline-digest-2026-08-13",
|
||||
"deadline_count": 1,
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,20 @@ COMMENT_ACTIONS = Path(__file__).parents[1] / "frontend" / "comment-actions.js"
|
|||
PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js"
|
||||
WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js"
|
||||
UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js"
|
||||
|
||||
|
||||
def test_protect_today_route_is_an_explicit_agenda_action():
|
||||
script = f"""
|
||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||
process.stdout.write(JSON.stringify(routes.parse('#/my-work/agenda/protect-today')));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"kind": "queue", "filter": "agenda", "action": "protect-today"
|
||||
}
|
||||
CARD_PLANNING = Path(__file__).parents[1] / "frontend" / "card-planning.js"
|
||||
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
|
||||
WORK_SELECTION = Path(__file__).parents[1] / "frontend" / "work-selection.js"
|
||||
|
|
|
|||
|
|
@ -459,14 +459,15 @@ def test_update_digest_push_opens_unread_inbox_without_item_actions_or_private_c
|
|||
assert "must-not-render" not in json.dumps(result["notifications"])
|
||||
|
||||
|
||||
def test_deadline_digest_push_opens_agenda_without_rendering_private_copy():
|
||||
def test_deadline_digest_push_offers_protect_today_and_agenda_without_rendering_private_copy():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
await dispatchPush({
|
||||
title:'must-not-render', body:'private details must-not-render',
|
||||
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda', deadline_count:3,
|
||||
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda',
|
||||
protect_route:'#/my-work/agenda/protect-today', deadline_count:3,
|
||||
});
|
||||
await dispatchNotificationClick('#/my-work/agenda');
|
||||
await dispatchNotificationClick('#/my-work/agenda/protect-today', 'protect-today', null, 'stackchain-deadline-digest-2026-08-13');
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
|
@ -476,15 +477,37 @@ def test_deadline_digest_push_opens_agenda_without_rendering_private_copy():
|
|||
"options": {
|
||||
"body": "Open Agenda to review or replan them.",
|
||||
"tag": "stackchain-deadline-digest-2026-08-13",
|
||||
"data": {"route": "#/my-work/agenda"},
|
||||
"actions": [
|
||||
{"action": "protect-today", "title": "Protect Today"},
|
||||
{"action": "open-agenda", "title": "Open Agenda"},
|
||||
],
|
||||
"data": {
|
||||
"route": "#/my-work/agenda",
|
||||
"protectRoute": "#/my-work/agenda/protect-today",
|
||||
},
|
||||
},
|
||||
}]
|
||||
assert result["opened"] == [
|
||||
"https://forge.example/dashboard/#/my-work/agenda"
|
||||
"https://forge.example/dashboard/#/my-work/agenda/protect-today"
|
||||
]
|
||||
assert "must-not-render" not in json.dumps(result["notifications"])
|
||||
|
||||
|
||||
def test_deadline_digest_open_agenda_action_preserves_browsing_flow():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
await dispatchPush({
|
||||
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda',
|
||||
protect_route:'#/my-work/agenda/protect-today', deadline_count:1,
|
||||
});
|
||||
await dispatchNotificationClick('#/my-work/agenda', 'open-agenda', null, 'stackchain-deadline-digest-2026-08-13');
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["opened"] == ["https://forge.example/dashboard/#/my-work/agenda"]
|
||||
|
||||
|
||||
def test_push_mark_read_action_confirms_authenticated_mutation_without_opening_app():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user