Make mobile Queues census include Following and My PRs (#1453)
All checks were successful
CI / lint (push) Successful in 3m48s
CI / build-release (push) Successful in 7s
CI / browser-journey (push) Successful in 7m16s
CI / release-candidate (push) Successful in 8s

Closes #1452
This commit is contained in:
timmy 2026-08-27 02:14:24 +00:00
commit 7a09452bef
3 changed files with 94 additions and 4 deletions

View File

@ -123,12 +123,14 @@
queueCounts.followingUnavailable = false; queueCounts.followingUnavailable = false;
preparationItems.following = items.filter(item => item.has_unseen_change === true); preparationItems.following = items.filter(item => item.has_unseen_change === true);
renderMobileQueuePresentation(); renderMobileQueuePresentation();
mobileTaskDock.updateQueues(queueCounts);
mobileStartDay.render(); mobileStartDay.render();
}, },
onStatus:status => { onStatus:status => {
if (status === 'loading') return; if (status === 'loading') return;
queueCounts.followingUnavailable = status === 'error'; queueCounts.followingUnavailable = status === 'error';
renderMobileQueuePresentation(); renderMobileQueuePresentation();
mobileTaskDock.updateQueues(queueCounts);
mobileStartDay.render(); mobileStartDay.render();
}, },
onReviewComplete:() => mobileStartDay.completePhase('following'), onReviewComplete:() => mobileStartDay.completePhase('following'),
@ -3547,9 +3549,12 @@
counts.delivery = draftInbox.partition(lastDrafts).actionable; counts.delivery = draftInbox.partition(lastDrafts).actionable;
counts.gate = queueCounts.gate; counts.gate = queueCounts.gate;
counts.gateUnavailable = queueCounts.gateUnavailable; counts.gateUnavailable = queueCounts.gateUnavailable;
counts.following = queueCounts.following;
counts.followingUnavailable = queueCounts.followingUnavailable;
preparationItems = { preparationItems = {
delivery:draftInbox.partition(lastDrafts).deliveries, delivery:draftInbox.partition(lastDrafts).deliveries,
gate:preparationItems.gate || [], gate:preparationItems.gate || [],
following:preparationItems.following || [],
agenda:agendaMyWork(activeMyWork), agenda:agendaMyWork(activeMyWork),
attention:activeMyWork.filter(item => item.needs_attention), attention:activeMyWork.filter(item => item.needs_attention),
update:activeMyWork.filter(item => item.has_update), update:activeMyWork.filter(item => item.has_update),

View File

@ -97,9 +97,9 @@
} }
function updateQueues(counts) { function updateQueues(counts) {
const names = 'today agenda delivery gate attention update filed later draft'.split(' '); const names = 'today agenda delivery gate attention update following filed authored later draft'.split(' ');
const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)])); const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)]));
const actionableNames = ['today', 'delivery', 'gate', 'attention', 'update', 'filed', 'later', 'draft']; const actionableNames = ['today', 'delivery', 'gate', 'attention', 'update', 'following', 'filed', 'authored', 'later', 'draft'];
const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0); const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0);
Object.entries(options.queueCounts || {}).forEach(([name, element]) => { Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
element.textContent = String(normalized[name] || 0); element.textContent = String(normalized[name] || 0);
@ -107,6 +107,9 @@
options.queueRows?.update?.setAttribute( options.queueRows?.update?.setAttribute(
'aria-label', 'Updates, ' + normalized.update + ' unread conversations' 'aria-label', 'Updates, ' + normalized.update + ' unread conversations'
); );
options.queueRows?.following?.setAttribute(
'aria-label', 'Following, ' + normalized.following + ' unseen changes'
);
if (options.queueBadge) { if (options.queueBadge) {
options.queueBadge.textContent = active + ' active'; options.queueBadge.textContent = active + ' active';
options.queueBadge.hidden = active === 0; options.queueBadge.hidden = active === 0;
@ -127,7 +130,9 @@
+ ', Human Gates ' + normalized.gate + ', Human Gates ' + normalized.gate
+ ', Attention ' + normalized.attention + ', Attention ' + normalized.attention
+ ', Updates ' + normalized.update + ', Updates ' + normalized.update
+ ', Following ' + normalized.following
+ ', Filed ' + normalized.filed + ', Filed ' + normalized.filed
+ ', My PRs ' + normalized.authored
+ ', Later ' + normalized.later + ', Later ' + normalized.later
+ ', Drafts ' + normalized.draft + ', Drafts ' + normalized.draft
+ '; ' + active + ' active ' + (active === 1 ? 'queue' : 'queues'); + '; ' + active + ' active ' + (active === 1 ? 'queue' : 'queues');

View File

@ -300,6 +300,33 @@ async def test_mobile_queue_sheet_prioritizes_next_active_and_planning_without_d
assert html.count(f'<button data-mobile-queue="{name}"') == 1 assert html.count(f'<button data-mobile-queue="{name}"') == 1
@pytest.mark.anyio
async def test_following_hydration_refreshes_the_persistent_mobile_queue_census():
html = await dashboard()
assert (
"preparationItems.following = items.filter(item => item.has_unseen_change === true);\n"
" renderMobileQueuePresentation();\n"
" mobileTaskDock.updateQueues(queueCounts);\n"
" mobileStartDay.render();"
) in html
assert (
"queueCounts.followingUnavailable = status === 'error';\n"
" renderMobileQueuePresentation();\n"
" mobileTaskDock.updateQueues(queueCounts);\n"
" mobileStartDay.render();"
) in html
@pytest.mark.anyio
async def test_my_work_refresh_preserves_the_hydrated_following_queue():
html = await dashboard()
assert "counts.following = queueCounts.following;" in html
assert "counts.followingUnavailable = queueCounts.followingUnavailable;" in html
assert "following:preparationItems.following || []," in html
@pytest.mark.anyio @pytest.mark.anyio
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing(): async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
html = await dashboard() html = await dashboard()
@ -548,7 +575,7 @@ process.stdout.write(JSON.stringify({{
"populated": { "populated": {
"badge": "8 active", "badge": "8 active",
"badgeHidden": False, "badgeHidden": False,
"badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Human Gates 2, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 8 active queues", "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Human Gates 2, Attention 1, Updates 5, Following 0, Filed 2, My PRs 0, Later 3, Drafts 4; 8 active queues",
"deadline": "5 due", "deadline": "5 due",
"deadlineHidden": False, "deadlineHidden": False,
"agendaDue": "true", "agendaDue": "true",
@ -557,7 +584,7 @@ process.stdout.write(JSON.stringify({{
"badge": "1 active", "badge": "1 active",
"badgeHidden": False, "badgeHidden": False,
"deadlineHidden": True, "deadlineHidden": True,
"badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue", "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 7, Following 0, Filed 0, My PRs 0, Later 0, Drafts 0; 1 active queue",
}, },
"clearedBadgeHidden": True, "clearedBadgeHidden": True,
"clearedDeadlineHidden": True, "clearedDeadlineHidden": True,
@ -570,6 +597,59 @@ process.stdout.write(JSON.stringify({{
} }
def test_mobile_task_dock_counts_following_and_authored_work_truthfully():
script = f"""
const createDock = require({json.dumps(str(DOCK))});
const node = () => ({{
textContent:'', hidden:false, attributes:{{}},
setAttribute(name, value) {{ this.attributes[name] = value; }},
removeAttribute(name) {{ delete this.attributes[name]; }},
}});
const queues = node();
const badge = node();
const deadline = node();
const following = node();
const authored = node();
const dock = createDock({{
nav:node(), buttons:{{queues}}, queueBadge:badge, deadlineBadge:deadline,
queueRows:{{following, authored}},
queueCounts:{{following:node(), authored:node()}},
}});
dock.updateQueues({{following:3, authored:2}});
const active = {{
badge:badge.textContent,
badgeHidden:badge.hidden,
summary:queues.attributes['aria-label'],
followingCount:dock ? following.attributes['aria-label'] : null,
}};
dock.updateQueues({{following:0, authored:0}});
process.stdout.write(JSON.stringify({{
active,
cleared:{{
badgeHidden:badge.hidden,
summary:queues.attributes['aria-label'],
followingLabel:following.attributes['aria-label'],
}},
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"active": {
"badge": "2 active",
"badgeHidden": False,
"summary": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 0, Following 3, Filed 0, My PRs 2, Later 0, Drafts 0; 2 active queues",
"followingCount": "Following, 3 unseen changes",
},
"cleared": {
"badgeHidden": True,
"summary": "Queues: no active queues; no upcoming deadlines",
"followingLabel": "Following, 0 unseen changes",
},
}
def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue(): def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue():
script = f""" script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});