feat: defer Today work and continue (#455)
This commit is contained in:
parent
b214a49686
commit
72c470b652
|
|
@ -714,11 +714,23 @@
|
||||||
'Session recovery could not be saved on this device. You can keep working.';
|
'Session recovery could not be saved on this device. You can keep working.';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
function updateDetailDeferLabels(active) {
|
||||||
|
document.querySelectorAll('[data-detail-defer-preset=today]').forEach(button => {
|
||||||
|
button.textContent = active ? 'Later today & next' : 'Later today';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-detail-defer-preset=tomorrow]').forEach(button => {
|
||||||
|
button.textContent = active ? 'Tomorrow & next' : 'Tomorrow';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-detail-defer-custom]').forEach(button => {
|
||||||
|
button.textContent = active ? 'Choose date & time & next' : 'Choose date & time';
|
||||||
|
});
|
||||||
|
}
|
||||||
function updateWorkSessionActions() {
|
function updateWorkSessionActions() {
|
||||||
const active = workSession.active();
|
const active = workSession.active();
|
||||||
qs('#start-work-session').hidden = active;
|
qs('#start-work-session').hidden = active;
|
||||||
qs('#resume-today-session').hidden = active || !todayMyWork.length || !workSession.resumable();
|
qs('#resume-today-session').hidden = active || !todayMyWork.length || !workSession.resumable();
|
||||||
qs('#end-today-session').hidden = !active;
|
qs('#end-today-session').hidden = !active;
|
||||||
|
updateDetailDeferLabels(active);
|
||||||
mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention);
|
mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1238,6 +1250,10 @@
|
||||||
const detailDefer = createDetailDefer({
|
const detailDefer = createDetailDefer({
|
||||||
laterWork,
|
laterWork,
|
||||||
session: workSession,
|
session: workSession,
|
||||||
|
continueSession: item => completeTodayItem(item, {
|
||||||
|
successMessage: 'Deferred to Later. Next Today item opened.',
|
||||||
|
failureMessage: 'Could not remove this item from Today.',
|
||||||
|
}),
|
||||||
close: () => workRoute.close(),
|
close: () => workRoute.close(),
|
||||||
refresh: refreshMyWorkView,
|
refresh: refreshMyWorkView,
|
||||||
focus: () => qs('[data-work-filter="' + selectedWorkFilter + '"]')?.focus(),
|
focus: () => qs('[data-work-filter="' + selectedWorkFilter + '"]')?.focus(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
function createDetailDefer({
|
function createDetailDefer({
|
||||||
laterWork,
|
laterWork,
|
||||||
session,
|
session,
|
||||||
|
continueSession = null,
|
||||||
close,
|
close,
|
||||||
refresh,
|
refresh,
|
||||||
focus,
|
focus,
|
||||||
|
|
@ -16,6 +17,13 @@ function createDetailDefer({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const inSession = session.active();
|
const inSession = session.active();
|
||||||
|
if (continueSession && session.checkpointed?.(item)) {
|
||||||
|
if (continueSession(item) === true) return true;
|
||||||
|
laterWork.restore(item);
|
||||||
|
refresh();
|
||||||
|
announce('Could not remove this item from Today, so it was restored from Later.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!inSession && closeSheet) close();
|
if (!inSession && closeSheet) close();
|
||||||
refresh();
|
refresh();
|
||||||
announce('Deferred until ' + formatTime(until) + '. It stays unread and unchanged in Gitea.');
|
announce('Deferred until ' + formatTime(until) + '. It stays unread and unchanged in Gitea.');
|
||||||
|
|
|
||||||
|
|
@ -1223,6 +1223,75 @@ process.stdout.write(JSON.stringify({{saved,calls}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_detail_defer_completes_checkpointed_today_item_after_saving_later():
|
||||||
|
script = f"""
|
||||||
|
const createDetailDefer = require({json.dumps(str(DETAIL_DEFER))});
|
||||||
|
const calls = [];
|
||||||
|
const item = {{kind:'issue',repository:'stackchain/api',number:17,title:'Read first'}};
|
||||||
|
const controller = createDetailDefer({{
|
||||||
|
laterWork: {{
|
||||||
|
presetUntil:() => new Date('2026-08-09T09:00:00Z'),
|
||||||
|
defer:(saved, until) => {{ calls.push(['defer', saved.title, until.toISOString()]); return 'deferred'; }},
|
||||||
|
restore:() => calls.push(['restore']),
|
||||||
|
}},
|
||||||
|
session: {{active:() => true, checkpointed:saved => saved === item}},
|
||||||
|
continueSession:saved => {{ calls.push(['continue', saved.title]); return true; }},
|
||||||
|
close:() => calls.push(['close']), refresh:() => calls.push(['refresh']),
|
||||||
|
focus:() => calls.push(['focus']), announce:message => calls.push(['announce', message]),
|
||||||
|
}});
|
||||||
|
const saved = controller.defer(item, 'tomorrow');
|
||||||
|
process.stdout.write(JSON.stringify({{saved,calls}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"saved": True,
|
||||||
|
"calls": [
|
||||||
|
["defer", "Read first", "2026-08-09T09:00:00.000Z"],
|
||||||
|
["continue", "Read first"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_detail_defer_rolls_back_later_when_today_cannot_be_removed():
|
||||||
|
script = f"""
|
||||||
|
const createDetailDefer = require({json.dumps(str(DETAIL_DEFER))});
|
||||||
|
const calls = [];
|
||||||
|
const item = {{kind:'issue',repository:'stackchain/api',number:17}};
|
||||||
|
const controller = createDetailDefer({{
|
||||||
|
laterWork: {{
|
||||||
|
presetUntil:() => new Date('2026-08-09T09:00:00Z'),
|
||||||
|
defer:() => {{ calls.push('defer'); return 'deferred'; }},
|
||||||
|
restore:saved => {{ calls.push(['restore', saved.number]); return true; }},
|
||||||
|
}},
|
||||||
|
session: {{active:() => true, checkpointed:() => true}},
|
||||||
|
continueSession:() => {{ calls.push('continue'); return false; }},
|
||||||
|
close:() => calls.push('close'), refresh:() => calls.push('refresh'),
|
||||||
|
focus:() => calls.push('focus'), announce:message => calls.push(['announce', message]),
|
||||||
|
}});
|
||||||
|
const saved = controller.defer(item, 'tomorrow');
|
||||||
|
process.stdout.write(JSON.stringify({{saved,calls}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"saved": False,
|
||||||
|
"calls": [
|
||||||
|
"defer",
|
||||||
|
"continue",
|
||||||
|
["restore", 17],
|
||||||
|
"refresh",
|
||||||
|
["announce", "Could not remove this item from Today, so it was restored from Later."],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutation():
|
async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutation():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -1238,6 +1307,18 @@ async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutat
|
||||||
assert "fetch(" not in DETAIL_DEFER.read_text()
|
assert "fetch(" not in DETAIL_DEFER.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_checkpointed_detail_defer_completes_today_and_advertises_next_item():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert "continueSession: item => completeTodayItem(item, {" in html
|
||||||
|
assert "successMessage: 'Deferred to Later. Next Today item opened.'" in html
|
||||||
|
assert "advance: () => runTodayTransition('complete')" in html
|
||||||
|
assert "button.textContent = active ? 'Later today & next' : 'Later today'" in html
|
||||||
|
assert "button.textContent = active ? 'Tomorrow & next' : 'Tomorrow'" in html
|
||||||
|
assert "button.textContent = active ? 'Choose date & time & next' : 'Choose date & time'" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_my_work_wires_touch_safe_non_mutating_later_actions():
|
async def test_mobile_my_work_wires_touch_safe_non_mutating_later_actions():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user