feat: continue Today from mobile Work dock (#409)
This commit is contained in:
parent
3ba1fe76f0
commit
8b36455c21
|
|
@ -43,20 +43,30 @@
|
||||||
Array.from(document.querySelectorAll('[data-mobile-task]')).map(button => [button.dataset.mobileTask, button])
|
Array.from(document.querySelectorAll('[data-mobile-task]')).map(button => [button.dataset.mobileTask, button])
|
||||||
);
|
);
|
||||||
const mobileTaskOverlays = Array.from(document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal'));
|
const mobileTaskOverlays = Array.from(document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal'));
|
||||||
function openMobileWork() {
|
function openMobileWorkFallback() {
|
||||||
const counts = countMyWork(activeMyWork);
|
const counts = countMyWork(activeMyWork);
|
||||||
const filter = todayMyWork.length ? 'today' : (counts.attention ? 'attention' : 'all');
|
const filter = counts.attention ? 'attention' : 'all';
|
||||||
qs('[data-work-filter="' + filter + '"]').click();
|
qs('[data-work-filter="' + filter + '"]').click();
|
||||||
qs('#my-work').scrollIntoView({block:'start'});
|
qs('#my-work').scrollIntoView({block:'start'});
|
||||||
qs('#my-work').focus();
|
qs('#my-work').focus();
|
||||||
}
|
}
|
||||||
|
const mobileWorkEntry = createMobileWorkEntry({
|
||||||
|
isTodayActive: () => workSession.checkpointed(),
|
||||||
|
isTodayResumable: () => workSession.resumable(),
|
||||||
|
getTodayCount: () => todayMyWork.length,
|
||||||
|
continueToday: continueTodaySession,
|
||||||
|
resumeToday: resumeTodaySession,
|
||||||
|
startToday: startTodaySession,
|
||||||
|
openFallback: openMobileWorkFallback,
|
||||||
|
});
|
||||||
const mobileTaskDock = createMobileTaskDock({
|
const mobileTaskDock = createMobileTaskDock({
|
||||||
nav: qs('#mobile-task-dock'),
|
nav: qs('#mobile-task-dock'),
|
||||||
buttons: mobileTaskButtons,
|
buttons: mobileTaskButtons,
|
||||||
|
workLabel: qs('#mobile-work-label'),
|
||||||
attentionBadge: qs('#mobile-attention-count'),
|
attentionBadge: qs('#mobile-attention-count'),
|
||||||
overlays: mobileTaskOverlays,
|
overlays: mobileTaskOverlays,
|
||||||
actions: {
|
actions: {
|
||||||
work: openMobileWork,
|
work: () => mobileWorkEntry.open(),
|
||||||
find: () => qs('#find-work').click(),
|
find: () => qs('#find-work').click(),
|
||||||
new: () => qs('#new-issue').click(),
|
new: () => qs('#new-issue').click(),
|
||||||
search: () => qs('#open-palette').click(),
|
search: () => qs('#open-palette').click(),
|
||||||
|
|
@ -678,6 +688,7 @@
|
||||||
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;
|
||||||
|
mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention);
|
||||||
}
|
}
|
||||||
|
|
||||||
const workSession = createWorkSession({
|
const workSession = createWorkSession({
|
||||||
|
|
@ -709,6 +720,31 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function selectTodayWork() {
|
||||||
|
qs('[data-work-filter="today"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueTodaySession() {
|
||||||
|
selectTodayWork();
|
||||||
|
if (!workSession.reopen() && !workSession.reconcile()) {
|
||||||
|
qs('#my-work-action-status').textContent = 'Current Today item is no longer available.';
|
||||||
|
updateWorkSessionActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeTodaySession() {
|
||||||
|
selectTodayWork();
|
||||||
|
if (!workSession.resume()) {
|
||||||
|
qs('#my-work-action-status').textContent = 'Saved Today session is no longer available.';
|
||||||
|
updateWorkSessionActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTodaySession() {
|
||||||
|
selectTodayWork();
|
||||||
|
workSession.start();
|
||||||
|
}
|
||||||
|
|
||||||
const createAndStart = createCreateAndStart({
|
const createAndStart = createCreateAndStart({
|
||||||
todayWork,
|
todayWork,
|
||||||
todaySync,
|
todaySync,
|
||||||
|
|
@ -1105,7 +1141,7 @@
|
||||||
const element = qs('[data-work-count="' + filter + '"]');
|
const element = qs('[data-work-count="' + filter + '"]');
|
||||||
if (element) element.textContent = count;
|
if (element) element.textContent = count;
|
||||||
});
|
});
|
||||||
mobileTaskDock.updateAttention(counts.attention);
|
mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention);
|
||||||
const activeQueue = qs('[data-work-filter="' + selectedWorkFilter + '"]');
|
const activeQueue = qs('[data-work-filter="' + selectedWorkFilter + '"]');
|
||||||
qs('#active-work-queue').textContent = activeQueue.firstChild.textContent.trim() +
|
qs('#active-work-queue').textContent = activeQueue.firstChild.textContent.trim() +
|
||||||
' (' + (counts[selectedWorkFilter] || 0) + ')';
|
' (' + (counts[selectedWorkFilter] || 0) + ')';
|
||||||
|
|
|
||||||
|
|
@ -567,7 +567,7 @@
|
||||||
</dialog>
|
</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">Work <span class="mobile-task-count" id="mobile-attention-count" hidden>0</span></button>
|
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span> <span class="mobile-task-count" id="mobile-attention-count" hidden>0</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>
|
||||||
<button class="mobile-task-action" data-mobile-task="new" type="button">New</button>
|
<button class="mobile-task-action" data-mobile-task="new" type="button">New</button>
|
||||||
<button class="mobile-task-action" data-mobile-task="search" type="button">Search</button>
|
<button class="mobile-task-action" data-mobile-task="search" type="button">Search</button>
|
||||||
|
|
@ -612,6 +612,7 @@
|
||||||
<script src="static/task-overlay-history.js"></script>
|
<script src="static/task-overlay-history.js"></script>
|
||||||
<script src="static/context-poller.js"></script>
|
<script src="static/context-poller.js"></script>
|
||||||
<script src="static/mobile-task-dock.js"></script>
|
<script src="static/mobile-task-dock.js"></script>
|
||||||
|
<script src="static/mobile-work-entry.js"></script>
|
||||||
<script src="static/mobile-launch.js"></script>
|
<script src="static/mobile-launch.js"></script>
|
||||||
<script src="static/install-app.js"></script>
|
<script src="static/install-app.js"></script>
|
||||||
<script src="static/mobile-search-viewport.js"></script>
|
<script src="static/mobile-search-viewport.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
const overlays = options.overlays || [];
|
const overlays = options.overlays || [];
|
||||||
let launcher = null;
|
let launcher = null;
|
||||||
let wasHidden = false;
|
let wasHidden = false;
|
||||||
|
let workMode = 'work';
|
||||||
|
|
||||||
function select(name) {
|
function select(name) {
|
||||||
Object.entries(buttons).forEach(([key, button]) => {
|
Object.entries(buttons).forEach(([key, button]) => {
|
||||||
|
|
@ -34,9 +35,17 @@
|
||||||
refreshVisibility();
|
refreshVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAttention(count) {
|
function updateWork(mode, count) {
|
||||||
|
const labels = {
|
||||||
|
continue: ['Continue', 'Continue Today'],
|
||||||
|
resume: ['Resume', 'Resume Today'],
|
||||||
|
start: ['Start', 'Start Today'],
|
||||||
|
work: ['Work', 'Work'],
|
||||||
|
};
|
||||||
|
workMode = labels[mode] ? mode : 'work';
|
||||||
const total = Math.max(0, Number(count) || 0);
|
const total = Math.max(0, Number(count) || 0);
|
||||||
const badge = options.attentionBadge;
|
const badge = options.attentionBadge;
|
||||||
|
if (options.workLabel) options.workLabel.textContent = labels[workMode][0];
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.textContent = String(total);
|
badge.textContent = String(total);
|
||||||
badge.hidden = total === 0;
|
badge.hidden = total === 0;
|
||||||
|
|
@ -44,10 +53,14 @@
|
||||||
if (buttons.work) {
|
if (buttons.work) {
|
||||||
buttons.work.setAttribute(
|
buttons.work.setAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
total ? 'Work, ' + total + ' items need attention' : 'Work'
|
labels[workMode][1] + (total ? ', ' + total + ' items need attention' : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {start, refreshVisibility, updateAttention};
|
function updateAttention(count) {
|
||||||
|
updateWork(workMode, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {start, refreshVisibility, updateAttention, updateWork};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
frontend/mobile-work-entry.js
Normal file
22
frontend/mobile-work-entry.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else root.createMobileWorkEntry = factory;
|
||||||
|
})(typeof self !== 'undefined' ? self : this, function createMobileWorkEntry(options) {
|
||||||
|
function mode() {
|
||||||
|
if (options.isTodayActive()) return 'continue';
|
||||||
|
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
|
||||||
|
if (options.getTodayCount() > 0) return 'start';
|
||||||
|
return 'work';
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
const current = mode();
|
||||||
|
if (current === 'continue') options.continueToday();
|
||||||
|
else if (current === 'resume') options.resumeToday();
|
||||||
|
else if (current === 'start') options.startToday();
|
||||||
|
else options.openFallback();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {mode, open};
|
||||||
|
});
|
||||||
|
|
@ -563,8 +563,19 @@ function createWorkSession({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: () => running,
|
active: () => running,
|
||||||
|
checkpointed: () => running && durable,
|
||||||
end: () => finish(),
|
end: () => finish(),
|
||||||
resumable: () => Boolean(checkpoint?.read()),
|
resumable: () => Boolean(checkpoint?.read()),
|
||||||
|
reopen() {
|
||||||
|
if (!running) return false;
|
||||||
|
const items = queue();
|
||||||
|
const index = items.findIndex(item => workIdentity(item) === currentIdentity);
|
||||||
|
if (index < 0) return false;
|
||||||
|
currentIndex = index;
|
||||||
|
report(items, index);
|
||||||
|
onOpen(items[index]);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
resume() {
|
resume() {
|
||||||
const saved = checkpoint?.read();
|
const saved = checkpoint?.read();
|
||||||
if (!saved) return false;
|
if (!saved) return false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v65';
|
const CACHE = 'stackchain-dashboard-shell-v66';
|
||||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||||
|
|
@ -46,6 +46,7 @@ const SHELL = [
|
||||||
BASE + 'static/task-overlay-history.js',
|
BASE + 'static/task-overlay-history.js',
|
||||||
BASE + 'static/context-poller.js',
|
BASE + 'static/context-poller.js',
|
||||||
BASE + 'static/mobile-task-dock.js',
|
BASE + 'static/mobile-task-dock.js',
|
||||||
|
BASE + 'static/mobile-work-entry.js',
|
||||||
BASE + 'static/mobile-launch.js',
|
BASE + 'static/mobile-launch.js',
|
||||||
BASE + 'static/install-app.js',
|
BASE + 'static/install-app.js',
|
||||||
BASE + 'static/mobile-search-viewport.js',
|
BASE + 'static/mobile-search-viewport.js',
|
||||||
|
|
|
||||||
|
|
@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/later-sync.js'" in source
|
assert "BASE + 'static/later-sync.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
||||||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||||
assert ".markdown-content a { min-height:44px;" in css
|
assert ".markdown-content a { min-height:44px;" in css
|
||||||
assert "stackchain-dashboard-shell-v65" in worker
|
assert "stackchain-dashboard-shell-v66" in worker
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
||||||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||||
|
|
||||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||||
assert "stackchain-dashboard-shell-v65" in worker
|
assert "stackchain-dashboard-shell-v66" in worker
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,39 @@ from tests.dashboard_bundle import dashboard
|
||||||
|
|
||||||
|
|
||||||
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
|
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
|
||||||
|
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_work_entry_prioritizes_continue_resume_start_then_fallback():
|
||||||
|
script = f"""
|
||||||
|
const createEntry = require({json.dumps(str(ENTRY))});
|
||||||
|
const state = {{active:true, resumable:true, today:2}};
|
||||||
|
const calls = [];
|
||||||
|
const entry = createEntry({{
|
||||||
|
isTodayActive: () => state.active,
|
||||||
|
isTodayResumable: () => state.resumable,
|
||||||
|
getTodayCount: () => state.today,
|
||||||
|
continueToday: () => calls.push('continue'),
|
||||||
|
resumeToday: () => calls.push('resume'),
|
||||||
|
startToday: () => calls.push('start'),
|
||||||
|
openFallback: () => calls.push('fallback'),
|
||||||
|
}});
|
||||||
|
const modes = [];
|
||||||
|
modes.push(entry.open());
|
||||||
|
state.active = false; modes.push(entry.open());
|
||||||
|
state.resumable = false; modes.push(entry.open());
|
||||||
|
state.today = 0; modes.push(entry.open());
|
||||||
|
process.stdout.write(JSON.stringify({{modes, calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"modes": ["continue", "resume", "start", "work"],
|
||||||
|
"calls": ["continue", "resume", "start", "fallback"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
|
def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
|
||||||
|
|
@ -70,6 +103,43 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_task_dock_shows_contextual_work_label_with_attention_count():
|
||||||
|
script = f"""
|
||||||
|
const createDock = require({json.dumps(str(DOCK))});
|
||||||
|
const work = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }} }};
|
||||||
|
const workLabel = {{ textContent:'' }};
|
||||||
|
const badge = {{ textContent:'', hidden:true }};
|
||||||
|
const dock = createDock({{ nav:{{}}, buttons:{{work}}, workLabel, attentionBadge:badge }});
|
||||||
|
dock.updateWork('continue', 3);
|
||||||
|
const continuing = {{ text:workLabel.textContent, count:badge.textContent, hidden:badge.hidden, label:work.attributes['aria-label'] }};
|
||||||
|
dock.updateWork('resume', 0);
|
||||||
|
const resuming = {{ text:workLabel.textContent, hidden:badge.hidden, label:work.attributes['aria-label'] }};
|
||||||
|
dock.updateWork('start', 0);
|
||||||
|
const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
|
||||||
|
dock.updateWork('work', 0);
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
continuing, resuming, starting,
|
||||||
|
fallback:{{ text:workLabel.textContent, label:work.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) == {
|
||||||
|
"continuing": {
|
||||||
|
"text": "Continue",
|
||||||
|
"count": "3",
|
||||||
|
"hidden": False,
|
||||||
|
"label": "Continue Today, 3 items need attention",
|
||||||
|
},
|
||||||
|
"resuming": {"text": "Resume", "hidden": True, "label": "Resume Today"},
|
||||||
|
"starting": {"text": "Start", "label": "Start Today"},
|
||||||
|
"fallback": {"text": "Work", "label": "Work"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_task_dock_exposes_and_hides_deduplicated_attention_count():
|
def test_mobile_task_dock_exposes_and_hides_deduplicated_attention_count():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createDock = require({json.dumps(str(DOCK))});
|
const createDock = require({json.dumps(str(DOCK))});
|
||||||
|
|
@ -95,6 +165,24 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_dashboard_wires_mobile_work_dock_into_today_session_lifecycle():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'id="mobile-work-label">Work</span>' in html
|
||||||
|
assert '<script src="static/mobile-work-entry.js"></script>' in html
|
||||||
|
assert "const mobileWorkEntry = createMobileWorkEntry({" in html
|
||||||
|
assert "isTodayActive: () => workSession.checkpointed()" in html
|
||||||
|
assert "isTodayResumable: () => workSession.resumable()" in html
|
||||||
|
assert "getTodayCount: () => todayMyWork.length" in html
|
||||||
|
assert "continueToday: continueTodaySession" in html
|
||||||
|
assert "resumeToday: resumeTodaySession" in html
|
||||||
|
assert "startToday: startTodaySession" in html
|
||||||
|
assert "work: () => mobileWorkEntry.open()" in html
|
||||||
|
assert "mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention)" in html
|
||||||
|
assert "workSession.reopen()" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -112,12 +200,12 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
||||||
assert '.mobile-task-action { min-width:0; min-height:44px;' in html
|
assert '.mobile-task-action { min-width:0; min-height:44px;' in html
|
||||||
assert '<script src="static/mobile-task-dock.js"></script>' in html
|
assert '<script src="static/mobile-task-dock.js"></script>' in html
|
||||||
assert "createMobileTaskDock({" in html
|
assert "createMobileTaskDock({" in html
|
||||||
assert "work: openMobileWork" in html
|
assert "work: () => mobileWorkEntry.open()" in html
|
||||||
assert "counts.attention ? 'attention' : 'all'" in html
|
assert "const filter = counts.attention ? 'attention' : 'all';" in html
|
||||||
assert "qs('[data-work-filter=\"' + filter + '\"]').click()" in html
|
assert "qs('[data-work-filter=\"' + filter + '\"]').click()" in html
|
||||||
assert "find: () => qs('#find-work').click()" in html
|
assert "find: () => qs('#find-work').click()" in html
|
||||||
assert "new: () => qs('#new-issue').click()" in html
|
assert "new: () => qs('#new-issue').click()" in html
|
||||||
assert "search: () => qs('#open-palette').click()" in html
|
assert "search: () => qs('#open-palette').click()" in html
|
||||||
assert "drafts: () => qs('[data-work-filter=\"draft\"]').click()" in html
|
assert "drafts: () => qs('[data-work-filter=\"draft\"]').click()" in html
|
||||||
assert "draftCount.textContent = sourceDraftCount.textContent" in html
|
assert "draftCount.textContent = sourceDraftCount.textContent" in html
|
||||||
assert "mobileTaskDock.updateAttention(counts.attention)" in html
|
assert "mobileTaskDock.updateWork(mobileWorkEntry.mode(), countMyWork(activeMyWork).attention)" in html
|
||||||
|
|
|
||||||
|
|
@ -1428,6 +1428,49 @@ process.stdout.write(JSON.stringify({{finished,active:session.active()}}));
|
||||||
assert json.loads(result.stdout) == {"finished": 1, "active": False}
|
assert json.loads(result.stdout) == {"finished": 1, "active": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_today_session_reopens_current_item_without_restarting_checkpoint():
|
||||||
|
script = f"""
|
||||||
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
let saves = 0;
|
||||||
|
const checkpoint = {{
|
||||||
|
read: () => null,
|
||||||
|
save: () => {{ saves += 1; return true; }},
|
||||||
|
clear: () => true,
|
||||||
|
}};
|
||||||
|
const items = [1, 2].map(number => ({{
|
||||||
|
kind:'issue', repository:'stackchain/dashboard', number, title:'Item ' + number,
|
||||||
|
}}));
|
||||||
|
const opened = [];
|
||||||
|
const progress = [];
|
||||||
|
const session = buildMyWork.createWorkSession({{
|
||||||
|
getItems: () => items, getFilter: () => 'all', checkpoint,
|
||||||
|
onOpen: item => opened.push(item.number),
|
||||||
|
onProgress: state => progress.push(state.index), onFinish: () => {{}},
|
||||||
|
}});
|
||||||
|
session.start();
|
||||||
|
session.next();
|
||||||
|
const savesBeforeReopen = saves;
|
||||||
|
const reopened = session.reopen();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
reopened, opened, progress, savesBeforeReopen, savesAfterReopen:saves,
|
||||||
|
todayActive:session.checkpointed(),
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"reopened": True,
|
||||||
|
"opened": [1, 2, 2],
|
||||||
|
"progress": [1, 2, 2],
|
||||||
|
"savesBeforeReopen": 2,
|
||||||
|
"savesAfterReopen": 2,
|
||||||
|
"todayActive": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_today_session_checkpoint_resumes_current_item_after_reload():
|
def test_today_session_checkpoint_resumes_current_item_after_reload():
|
||||||
script = f"""
|
script = f"""
|
||||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,6 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||||
source = SERVICE_WORKER.read_text()
|
source = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ async function dispatchNotificationClick(route) {{
|
||||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/my-work.js'" in source
|
assert "BASE + 'static/my-work.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
@ -130,7 +130,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -138,14 +138,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/later-picker.js'" in source
|
assert "BASE + 'static/later-picker.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/install-app.js'" in source
|
assert "BASE + 'static/install-app.js'" in source
|
||||||
|
|
@ -154,21 +154,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -383,6 +383,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/task-overlay-history.js",
|
"/dashboard/static/task-overlay-history.js",
|
||||||
"/dashboard/static/context-poller.js",
|
"/dashboard/static/context-poller.js",
|
||||||
"/dashboard/static/mobile-task-dock.js",
|
"/dashboard/static/mobile-task-dock.js",
|
||||||
|
"/dashboard/static/mobile-work-entry.js",
|
||||||
"/dashboard/static/mobile-launch.js",
|
"/dashboard/static/mobile-launch.js",
|
||||||
"/dashboard/static/install-app.js",
|
"/dashboard/static/install-app.js",
|
||||||
"/dashboard/static/mobile-search-viewport.js",
|
"/dashboard/static/mobile-search-viewport.js",
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
|
||||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v65" in source
|
assert "stackchain-dashboard-shell-v66" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,8 @@ async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
|
||||||
assert 'data-work-filter="today"' in html
|
assert 'data-work-filter="today"' in html
|
||||||
assert 'data-work-count="today"' in html
|
assert 'data-work-count="today"' in html
|
||||||
assert "const todayWork = createTodayWork({" in html
|
assert "const todayWork = createTodayWork({" in html
|
||||||
assert "const filter = todayMyWork.length ? 'today' : (counts.attention ? 'attention' : 'all');" in html
|
assert "getTodayCount: () => todayMyWork.length" in html
|
||||||
|
assert "startToday: startTodaySession" in html
|
||||||
assert "selectedWorkFilter === 'today' ? todayMyWork : activeMyWork" in html
|
assert "selectedWorkFilter === 'today' ? todayMyWork : activeMyWork" in html
|
||||||
assert 'data-today-add' in html
|
assert 'data-today-add' in html
|
||||||
assert 'data-today-remove' in html
|
assert 'data-today-remove' in html
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user