Make mobile Agenda discover every assigned deadline #704
|
|
@ -147,6 +147,7 @@
|
||||||
let lastContextSnapshot = null;
|
let lastContextSnapshot = null;
|
||||||
let notificationPagination = { page: 1, total: 0, has_more: false };
|
let notificationPagination = { page: 1, total: 0, has_more: false };
|
||||||
let workPagination = {};
|
let workPagination = {};
|
||||||
|
let agendaChecking = false;
|
||||||
let hasContextSnapshot = false;
|
let hasContextSnapshot = false;
|
||||||
let selectedReview = null;
|
let selectedReview = null;
|
||||||
let reviewTrigger = null;
|
let reviewTrigger = null;
|
||||||
|
|
@ -2202,6 +2203,9 @@
|
||||||
'No assigned work, review requests, or unread updates.';
|
'No assigned work, review requests, or unread updates.';
|
||||||
updateWorkPaginationControls();
|
updateWorkPaginationControls();
|
||||||
renderMyWork();
|
renderMyWork();
|
||||||
|
if (selectedWorkFilter === 'agenda' && workPagination.issue?.has_more && !agendaChecking) {
|
||||||
|
completeAgendaIssues();
|
||||||
|
}
|
||||||
if (reconcileSession && workSession.active()) workSession.reconcile();
|
if (reconcileSession && workSession.active()) workSession.reconcile();
|
||||||
updateWorkSessionActions();
|
updateWorkSessionActions();
|
||||||
}
|
}
|
||||||
|
|
@ -2217,6 +2221,20 @@
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeAgendaIssues() {
|
||||||
|
if (selectedWorkFilter !== 'agenda' || !workPagination.issue?.has_more || !lastContextSnapshot) return;
|
||||||
|
agendaChecking = true;
|
||||||
|
qs('#my-work-action-status').textContent = 'Checking all assigned deadlines…';
|
||||||
|
renderMyWork();
|
||||||
|
const complete = await workPager.loadAll('issue', () => lastContextSnapshot?.issues || []);
|
||||||
|
agendaChecking = false;
|
||||||
|
if (selectedWorkFilter !== 'agenda') return;
|
||||||
|
qs('#my-work-action-status').textContent = complete ?
|
||||||
|
'All assigned deadlines checked.' :
|
||||||
|
'Agenda check paused. Retry to check older assigned deadlines.';
|
||||||
|
renderMyWork();
|
||||||
|
}
|
||||||
|
|
||||||
function renderDrafts() {
|
function renderDrafts() {
|
||||||
const list = qs('#my-work-list');
|
const list = qs('#my-work-list');
|
||||||
const displayedDrafts = findQueueItems(lastDrafts, queueFindQuery);
|
const displayedDrafts = findQueueItems(lastDrafts, queueFindQuery);
|
||||||
|
|
@ -2489,9 +2507,12 @@
|
||||||
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main pull-trigger" href="' + escAttr(routeHref) + '" data-pull-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + planningActions + '</article>';
|
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main pull-trigger" href="' + escAttr(routeHref) + '" data-pull-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + planningActions + '</article>';
|
||||||
}
|
}
|
||||||
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main update-trigger" href="' + escAttr(routeHref) + '" data-update-index="' + index + '">' + contents + '</a>' + markRead + planningActions + '</article>';
|
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main update-trigger" href="' + escAttr(routeHref) + '" data-update-index="' + index + '">' + contents + '</a>' + markRead + planningActions + '</article>';
|
||||||
}).join('') : (showEmptyStart ? '' : '<div class="muted">' + (incomplete ?
|
}).join('') : (showEmptyStart ? '' : '<div class="muted">' +
|
||||||
|
(selectedWorkFilter === 'agenda' && workPagination.issue?.has_more ?
|
||||||
|
(agendaChecking ? 'Checking all assigned deadlines…' :
|
||||||
|
'Older assigned deadlines remain unchecked. Retry the Agenda check.') : (incomplete ?
|
||||||
'More work is available. Load the next page.' :
|
'More work is available. Load the next page.' :
|
||||||
'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '</div>');
|
'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.')) + '</div>');
|
||||||
cardPlanning.wire();
|
cardPlanning.wire();
|
||||||
document.querySelectorAll('[data-select-notification-id]').forEach(input => {
|
document.querySelectorAll('[data-select-notification-id]').forEach(input => {
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
|
|
@ -5778,6 +5799,10 @@
|
||||||
notificationPager.loadMore(lastNotifications)
|
notificationPager.loadMore(lastNotifications)
|
||||||
);
|
);
|
||||||
qs('#load-more-work').addEventListener('click', async () => {
|
qs('#load-more-work').addEventListener('click', async () => {
|
||||||
|
if (selectedWorkFilter === 'agenda') {
|
||||||
|
await completeAgendaIssues();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stream = activeWorkStreams().find(item => workPagination[item]?.has_more);
|
const stream = activeWorkStreams().find(item => workPagination[item]?.has_more);
|
||||||
if (!stream || !lastContextSnapshot) return;
|
if (!stream || !lastContextSnapshot) return;
|
||||||
const button = qs('#load-more-work');
|
const button = qs('#load-more-work');
|
||||||
|
|
@ -5931,6 +5956,7 @@
|
||||||
qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')';
|
qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')';
|
||||||
renderMyWork();
|
renderMyWork();
|
||||||
updateWorkPaginationControls();
|
updateWorkPaginationControls();
|
||||||
|
if (selectedWorkFilter === 'agenda') completeAgendaIssues();
|
||||||
if (!preserveRoute) workRoute.queue(filter);
|
if (!preserveRoute) workRoute.queue(filter);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -277,12 +277,13 @@ function createNotificationPager({ load, onNotifications, onPagination, onStatus
|
||||||
function createWorkPager({ load, onItems, onPagination, onStatus }) {
|
function createWorkPager({ load, onItems, onPagination, onStatus }) {
|
||||||
let pagination = {};
|
let pagination = {};
|
||||||
const pending = new Set();
|
const pending = new Set();
|
||||||
|
const completing = new Map();
|
||||||
const labels = {
|
const labels = {
|
||||||
issue: 'issues',
|
issue: 'issues',
|
||||||
pull: 'pull requests',
|
pull: 'pull requests',
|
||||||
review: 'review requests',
|
review: 'review requests',
|
||||||
};
|
};
|
||||||
return {
|
const pager = {
|
||||||
reset(next) {
|
reset(next) {
|
||||||
Object.entries(next || {}).forEach(([stream, value]) => {
|
Object.entries(next || {}).forEach(([stream, value]) => {
|
||||||
const current = pagination[stream];
|
const current = pagination[stream];
|
||||||
|
|
@ -331,7 +332,20 @@ function createWorkPager({ load, onItems, onPagination, onStatus }) {
|
||||||
pending.delete(stream);
|
pending.delete(stream);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
loadAll(stream, getExisting) {
|
||||||
|
if (completing.has(stream)) return completing.get(stream);
|
||||||
|
const completion = (async () => {
|
||||||
|
while (pagination[stream]?.has_more) {
|
||||||
|
const loaded = await pager.loadMore(stream, getExisting());
|
||||||
|
if (!loaded) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})().finally(() => completing.delete(stream));
|
||||||
|
completing.set(stream, completion);
|
||||||
|
return completion;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNotificationReader({
|
function createNotificationReader({
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,60 @@ process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-1
|
||||||
assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"]
|
assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_agenda_pager_loads_every_issue_page_single_flight_and_retries_failed_page():
|
||||||
|
script = f"""
|
||||||
|
const work = require({json.dumps(str(MY_WORK))});
|
||||||
|
const calls = [];
|
||||||
|
let failPage = 3;
|
||||||
|
let items = [{{id:1, title:'first'}}];
|
||||||
|
let pagination = {{issue:{{page:1,total:4,has_more:true}}}};
|
||||||
|
const pager = work.createWorkPager({{
|
||||||
|
load: async (stream, page) => {{
|
||||||
|
calls.push(page);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
if (page === failPage) throw new Error('offline');
|
||||||
|
return {{page,total:4,has_more:page < 4,items:[{{id:page,title:'page '+page}}]}};
|
||||||
|
}},
|
||||||
|
onItems: (_stream, next) => {{ items = next; }},
|
||||||
|
onPagination: next => {{ pagination = next; }},
|
||||||
|
onStatus: () => {{}},
|
||||||
|
}});
|
||||||
|
pager.reset(pagination);
|
||||||
|
async function run() {{
|
||||||
|
const first = pager.loadAll('issue', () => items);
|
||||||
|
const duplicate = pager.loadAll('issue', () => items);
|
||||||
|
const failed = await first;
|
||||||
|
const samePromise = first === duplicate;
|
||||||
|
failPage = 0;
|
||||||
|
const retried = await pager.loadAll('issue', () => items);
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
failed, samePromise, retried, calls, ids:items.map(item => item.id), pagination,
|
||||||
|
}}));
|
||||||
|
}}
|
||||||
|
run();
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"failed": False,
|
||||||
|
"samePromise": True,
|
||||||
|
"retried": True,
|
||||||
|
"calls": [2, 3, 3, 4],
|
||||||
|
"ids": [1, 2, 3, 4],
|
||||||
|
"pagination": {"issue": {"page": 4, "total": 4, "has_more": False}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_state():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert "workPager.loadAll('issue'" in html
|
||||||
|
assert "Checking all assigned deadlines…" in html
|
||||||
|
assert "Agenda check paused. Retry to check older assigned deadlines." in html
|
||||||
|
assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search():
|
async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user