Merge pull request 'Launch work directly from mobile queues' (#646) from timmy/645-mobile-queue-launcher into main
This commit is contained in:
commit
8ce64d2aa7
|
|
@ -56,7 +56,7 @@
|
||||||
qs('#my-work').scrollIntoView({block:'start'});
|
qs('#my-work').scrollIntoView({block:'start'});
|
||||||
qs('#my-work').focus();
|
qs('#my-work').focus();
|
||||||
}
|
}
|
||||||
function openMobileQueue(name) {
|
function selectMobileQueue(name) {
|
||||||
if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();
|
if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();
|
||||||
renderAttentionInterruption();
|
renderAttentionInterruption();
|
||||||
timerView.render();
|
timerView.render();
|
||||||
|
|
@ -75,6 +75,12 @@
|
||||||
planToday: () => openPlanToday(mobileTaskButtons.work),
|
planToday: () => openPlanToday(mobileTaskButtons.work),
|
||||||
openFallback: openMobileWorkFallback,
|
openFallback: openMobileWorkFallback,
|
||||||
});
|
});
|
||||||
|
const mobileQueueLauncher = createMobileQueueLauncher({
|
||||||
|
openToday: () => mobileWorkEntry.open(),
|
||||||
|
selectFilter: selectMobileQueue,
|
||||||
|
firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit'),
|
||||||
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
|
});
|
||||||
const mobileTaskDock = createMobileTaskDock({
|
const mobileTaskDock = createMobileTaskDock({
|
||||||
nav: qs('#mobile-task-dock'),
|
nav: qs('#mobile-task-dock'),
|
||||||
sessionHud: qs('[data-mobile-today-hud]'),
|
sessionHud: qs('[data-mobile-today-hud]'),
|
||||||
|
|
@ -89,7 +95,7 @@
|
||||||
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
|
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
|
||||||
),
|
),
|
||||||
queueBadge: qs('#mobile-queue-count'),
|
queueBadge: qs('#mobile-queue-count'),
|
||||||
onSelectQueue: openMobileQueue,
|
onSelectQueue: name => mobileQueueLauncher.open(name),
|
||||||
overlays: mobileTaskOverlays,
|
overlays: mobileTaskOverlays,
|
||||||
actions: {
|
actions: {
|
||||||
work: () => mobileWorkEntry.open(),
|
work: () => mobileWorkEntry.open(),
|
||||||
|
|
|
||||||
|
|
@ -934,6 +934,7 @@
|
||||||
<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-work-entry.js"></script>
|
||||||
|
<script src="static/mobile-queue-launcher.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-device-setup.js"></script>
|
<script src="static/mobile-device-setup.js"></script>
|
||||||
|
|
|
||||||
25
frontend/mobile-queue-launcher.js
Normal file
25
frontend/mobile-queue-launcher.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else root.createMobileQueueLauncher = factory;
|
||||||
|
})(typeof self !== 'undefined' ? self : this, function createMobileQueueLauncher(options) {
|
||||||
|
const emptyMessages = {
|
||||||
|
attention: 'No items need attention.',
|
||||||
|
update: 'No unread updates are ready to open.',
|
||||||
|
later: 'No deferred work is ready to open.',
|
||||||
|
draft: 'No drafts are ready to open.',
|
||||||
|
};
|
||||||
|
|
||||||
|
function open(name) {
|
||||||
|
if (name === 'today') return options.openToday();
|
||||||
|
options.selectFilter(name);
|
||||||
|
const action = options.firstAction(name);
|
||||||
|
if (!action) {
|
||||||
|
options.announce(emptyMessages[name] || 'No work is ready to open.');
|
||||||
|
return 'empty';
|
||||||
|
}
|
||||||
|
action.click();
|
||||||
|
return 'opened';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { open };
|
||||||
|
});
|
||||||
|
|
@ -67,6 +67,7 @@ const SHELL = [
|
||||||
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-work-entry.js',
|
||||||
|
BASE + 'static/mobile-queue-launcher.js',
|
||||||
BASE + 'static/mobile-launch.js',
|
BASE + 'static/mobile-launch.js',
|
||||||
BASE + 'static/install-app.js',
|
BASE + 'static/install-app.js',
|
||||||
BASE + 'static/mobile-device-setup.js',
|
BASE + 'static/mobile-device-setup.js',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ 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"
|
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
|
||||||
|
QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js"
|
||||||
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
|
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -334,6 +335,34 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
const calls = [];
|
||||||
|
const launcher = createLauncher({{
|
||||||
|
selectFilter: name => calls.push('filter:' + name),
|
||||||
|
firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null,
|
||||||
|
announce: message => calls.push('announce:' + message),
|
||||||
|
}});
|
||||||
|
const opened = launcher.open('attention');
|
||||||
|
const empty = launcher.open('later');
|
||||||
|
process.stdout.write(JSON.stringify({{opened, empty, calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"opened": "opened",
|
||||||
|
"empty": "empty",
|
||||||
|
"calls": [
|
||||||
|
"filter:attention",
|
||||||
|
"open:first",
|
||||||
|
"filter:later",
|
||||||
|
"announce:No deferred work is ready to open.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -349,7 +378,10 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
|
||||||
assert 'data-mobile-queue="draft"' in html
|
assert 'data-mobile-queue="draft"' in html
|
||||||
assert 'id="mobile-queue-count"' in html
|
assert 'id="mobile-queue-count"' in html
|
||||||
assert 'mobileTaskDock.updateQueues(counts)' in html
|
assert 'mobileTaskDock.updateQueues(counts)' in html
|
||||||
assert "onSelectQueue: openMobileQueue" in html
|
assert '<script src="static/mobile-queue-launcher.js"></script>' in html
|
||||||
|
assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html
|
||||||
|
assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html
|
||||||
|
assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html
|
||||||
assert '.mobile-queue-sheet' in html
|
assert '.mobile-queue-sheet' in html
|
||||||
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
|
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
|
||||||
assert '.mobile-task-dock[data-attention="true"]' not in html
|
assert '.mobile-task-dock[data-attention="true"]' not in html
|
||||||
|
|
@ -394,7 +426,7 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
||||||
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: () => mobileWorkEntry.open()" in html
|
assert "work: () => mobileWorkEntry.open()" in html
|
||||||
assert "onSelectQueue: openMobileQueue" in html
|
assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html
|
||||||
assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
|
assert "qs('[data-work-filter=\"' + name + '\"]').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
|
||||||
|
|
|
||||||
|
|
@ -730,6 +730,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/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-work-entry.js",
|
||||||
|
"/dashboard/static/mobile-queue-launcher.js",
|
||||||
"/dashboard/static/mobile-launch.js",
|
"/dashboard/static/mobile-launch.js",
|
||||||
"/dashboard/static/install-app.js",
|
"/dashboard/static/install-app.js",
|
||||||
"/dashboard/static/mobile-device-setup.js",
|
"/dashboard/static/mobile-device-setup.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user