feat: reopen recent work from mobile queues (Closes #1471)
This commit is contained in:
parent
46e06c1a37
commit
eb101bf5ce
|
|
@ -1551,6 +1551,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.mobile-queue-list button { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:56px; width:100%; padding:10px 14px; text-align:left; }
|
.mobile-queue-list button { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:56px; width:100%; padding:10px 14px; text-align:left; }
|
||||||
.mobile-queue-list button > span:first-child { display:grid; gap:2px; }
|
.mobile-queue-list button > span:first-child { display:grid; gap:2px; }
|
||||||
.mobile-queue-list small { color:var(--muted); }
|
.mobile-queue-list small { color:var(--muted); }
|
||||||
|
.mobile-queue-list [data-recent-work-route] { min-height:56px; overflow-wrap:anywhere; }
|
||||||
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
||||||
.mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; }
|
.mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; }
|
||||||
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
let offlineWorkMode = false;
|
let offlineWorkMode = false;
|
||||||
let renderMobileQueuePresentation = () => {};
|
let renderMobileQueuePresentation = () => {};
|
||||||
let mobileQueuePriority = null;
|
let mobileQueuePriority = null;
|
||||||
|
let mobileRecentWork = { record:() => false, render:() => 0 };
|
||||||
const followingQueue = attachFollowing(item => {
|
const followingQueue = attachFollowing(item => {
|
||||||
searchPreviewReturnKind = 'following';
|
searchPreviewReturnKind = 'following';
|
||||||
return searchPreview.open(item);
|
return searchPreview.open(item);
|
||||||
|
|
@ -313,7 +314,7 @@
|
||||||
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(),
|
||||||
queues: () => refreshTomorrowQueueSummary(),
|
queues: () => { refreshTomorrowQueueSummary(); mobileRecentWork.render(); },
|
||||||
},
|
},
|
||||||
observe(callback, overlays) {
|
observe(callback, overlays) {
|
||||||
const observer = new MutationObserver(callback);
|
const observer = new MutationObserver(callback);
|
||||||
|
|
@ -431,6 +432,19 @@
|
||||||
let planningOwnerLogin = '';
|
let planningOwnerLogin = '';
|
||||||
let planningOwnerAccountKey = '';
|
let planningOwnerAccountKey = '';
|
||||||
let activeFlushLogin = '';
|
let activeFlushLogin = '';
|
||||||
|
mobileRecentWork = createMobileRecentWork({
|
||||||
|
storage:localStorage,
|
||||||
|
getLogin:() => confirmedOwnerLogin,
|
||||||
|
document,
|
||||||
|
section:qs('#mobile-recent-work'),
|
||||||
|
list:qs('#mobile-recent-work-list'),
|
||||||
|
openRoute:fragment => {
|
||||||
|
const sheet = qs('#mobile-queue-sheet');
|
||||||
|
if (sheet.open) sheet.close();
|
||||||
|
if (window.location.hash !== fragment) window.history.pushState({ workRoute:fragment }, '', fragment);
|
||||||
|
workRoute.sync();
|
||||||
|
},
|
||||||
|
});
|
||||||
mobileQueuePriority = createMobileQueuePriority({
|
mobileQueuePriority = createMobileQueuePriority({
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
getLogin: () => confirmedOwnerLogin,
|
getLogin: () => confirmedOwnerLogin,
|
||||||
|
|
@ -2061,6 +2075,7 @@
|
||||||
qs('#my-work-action-status').textContent = '';
|
qs('#my-work-action-status').textContent = '';
|
||||||
closeOpenWorkSheets();
|
closeOpenWorkSheets();
|
||||||
await openRoutedWorkSection(item);
|
await openRoutedWorkSection(item);
|
||||||
|
mobileRecentWork.record(item);
|
||||||
const route = createWorkRoute.parse(window.location.hash);
|
const route = createWorkRoute.parse(window.location.hash);
|
||||||
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2183,6 +2183,10 @@
|
||||||
<p class="small muted" id="mobile-queue-next-heading">Start / Continue</p>
|
<p class="small muted" id="mobile-queue-next-heading">Start / Continue</p>
|
||||||
<button id="mobile-queue-next-action" type="button">Find Work</button>
|
<button id="mobile-queue-next-action" type="button">Find Work</button>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="mobile-queue-group" id="mobile-recent-work" aria-labelledby="mobile-recent-work-heading" hidden>
|
||||||
|
<h3 id="mobile-recent-work-heading">Recent work</h3>
|
||||||
|
<div class="mobile-queue-list" id="mobile-recent-work-list"></div>
|
||||||
|
</section>
|
||||||
<details class="mobile-queue-priority" id="mobile-queue-priority">
|
<details class="mobile-queue-priority" id="mobile-queue-priority">
|
||||||
<summary>Customize routine order</summary>
|
<summary>Customize routine order</summary>
|
||||||
<p class="small muted">Delivery and Human Gates always stay first. Move the routine queues to match how you work.</p>
|
<p class="small muted">Delivery and Human Gates always stay first. Move the routine queues to match how you work.</p>
|
||||||
|
|
@ -2416,6 +2420,7 @@
|
||||||
<script src="static/mobile-pull-refresh.js"></script>
|
<script src="static/mobile-pull-refresh.js"></script>
|
||||||
<script src="static/mobile-first-task.js"></script>
|
<script src="static/mobile-first-task.js"></script>
|
||||||
<script src="static/mobile-work-entry.js"></script>
|
<script src="static/mobile-work-entry.js"></script>
|
||||||
|
<script src="static/mobile-recent-work.js"></script>
|
||||||
<script src="static/mobile-queue-priority.js"></script>
|
<script src="static/mobile-queue-priority.js"></script>
|
||||||
<script src="static/mobile-queue-launcher.js"></script>
|
<script src="static/mobile-queue-launcher.js"></script>
|
||||||
<script src="static/mobile-delivery-recovery.js"></script>
|
<script src="static/mobile-delivery-recovery.js"></script>
|
||||||
|
|
|
||||||
102
frontend/mobile-recent-work.js
Normal file
102
frontend/mobile-recent-work.js
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else root.createMobileRecentWork = factory;
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function createMobileRecentWork(options) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const storage = options.storage;
|
||||||
|
const getLogin = options.getLogin;
|
||||||
|
const limit = Math.max(1, Number(options.limit) || 5);
|
||||||
|
const prefix = 'stackchain.mobile-recent-work.v1.';
|
||||||
|
const repositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||||
|
const kinds = new Set(['issue', 'filed', 'pull', 'review', 'update']);
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
return String(getLogin?.() || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function key() {
|
||||||
|
const owner = login();
|
||||||
|
return owner ? prefix + owner : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(item) {
|
||||||
|
const kind = String(item?.kind || '');
|
||||||
|
const number = Number(item?.number ?? item?.notification_id);
|
||||||
|
if (!kinds.has(kind) || !Number.isSafeInteger(number) || number < 1) return null;
|
||||||
|
let route = '';
|
||||||
|
let repository = '';
|
||||||
|
if (kind === 'update') {
|
||||||
|
route = '#/my-work/update/' + number;
|
||||||
|
} else {
|
||||||
|
repository = String(item?.repository || '');
|
||||||
|
if (!repositoryPattern.test(repository)) return null;
|
||||||
|
route = '#/my-work/' + kind + '/' + repository + '/' + number;
|
||||||
|
}
|
||||||
|
const title = String(item?.title || item?.subject?.title || '').trim().slice(0, 180);
|
||||||
|
if (!title) return null;
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
...(repository ? { repository } : {}),
|
||||||
|
number,
|
||||||
|
title,
|
||||||
|
route,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function items() {
|
||||||
|
const storageKey = key();
|
||||||
|
if (!storageKey) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(storage.getItem(storageKey) || '[]');
|
||||||
|
if (!Array.isArray(parsed)) return [];
|
||||||
|
return parsed.map(normalize).filter(Boolean).slice(0, limit);
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function record(item) {
|
||||||
|
const storageKey = key();
|
||||||
|
const normalized = normalize(item);
|
||||||
|
if (!storageKey || !normalized) return false;
|
||||||
|
const next = [normalized, ...items().filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
||||||
|
try {
|
||||||
|
storage.setItem(storageKey, JSON.stringify(next));
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const recent = items();
|
||||||
|
const list = options.list;
|
||||||
|
const section = options.section;
|
||||||
|
if (!list || !section || !options.document) return recent.length;
|
||||||
|
const rows = recent.map(item => {
|
||||||
|
const detail = item.kind === 'update'
|
||||||
|
? 'Update · #' + item.number
|
||||||
|
: item.kind.charAt(0).toUpperCase() + item.kind.slice(1) + ' · ' + item.repository + ' #' + item.number;
|
||||||
|
const button = options.document.createElement('button');
|
||||||
|
const copy = options.document.createElement('span');
|
||||||
|
const primary = options.document.createElement('strong');
|
||||||
|
const secondary = options.document.createElement('small');
|
||||||
|
primary.textContent = item.title;
|
||||||
|
secondary.textContent = detail;
|
||||||
|
copy.appendChild(primary);
|
||||||
|
copy.appendChild(secondary);
|
||||||
|
button.appendChild(copy);
|
||||||
|
button.setAttribute('type', 'button');
|
||||||
|
button.setAttribute('data-recent-work-route', item.route);
|
||||||
|
button.setAttribute('aria-label', 'Open ' + item.title + ', ' + detail.toLowerCase().replace(' · ', ' '));
|
||||||
|
button.addEventListener('click', () => options.openRoute?.(item.route));
|
||||||
|
return button;
|
||||||
|
});
|
||||||
|
list.replaceChildren(...rows);
|
||||||
|
section.hidden = rows.length === 0;
|
||||||
|
return rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items, record, render };
|
||||||
|
});
|
||||||
|
|
@ -270,6 +270,7 @@ const SHELL = [
|
||||||
BASE + 'static/mobile-task-dock.js',
|
BASE + 'static/mobile-task-dock.js',
|
||||||
BASE + 'static/mobile-first-task.js',
|
BASE + 'static/mobile-first-task.js',
|
||||||
BASE + 'static/mobile-work-entry.js',
|
BASE + 'static/mobile-work-entry.js',
|
||||||
|
BASE + 'static/mobile-recent-work.js',
|
||||||
BASE + 'static/mobile-queue-priority.js',
|
BASE + 'static/mobile-queue-priority.js',
|
||||||
BASE + 'static/mobile-queue-launcher.js',
|
BASE + 'static/mobile-queue-launcher.js',
|
||||||
BASE + 'static/mobile-delivery-recovery.js',
|
BASE + 'static/mobile-delivery-recovery.js',
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ FEATURE_SOURCES = {
|
||||||
),
|
),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/mobile-app-badge.js", "static/conversation.js", "static/widgets.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js", "static/mobile-search-modal.js", "static/mobile-composer-viewport.js",
|
"static/mobile-app-badge.js", "static/conversation.js", "static/widgets.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js", "static/mobile-search-modal.js", "static/mobile-composer-viewport.js",
|
||||||
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-first-task.js", "static/mobile-work-entry.js", "static/mobile-queue-priority.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js",
|
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-first-task.js", "static/mobile-work-entry.js", "static/mobile-recent-work.js", "static/mobile-queue-priority.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js",
|
||||||
"static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/photo-draft-inbox.js", "static/unfiled-draft-sync.js",
|
"static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/photo-draft-inbox.js", "static/unfiled-draft-sync.js",
|
||||||
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
||||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||||
|
|
|
||||||
141
tests/test_mobile_recent_work.py
Normal file
141
tests/test_mobile_recent_work.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
RECENT_WORK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-recent-work.js"
|
||||||
|
INDEX = Path(__file__).resolve().parents[1] / "frontend" / "index.html"
|
||||||
|
DASHBOARD = Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js"
|
||||||
|
CSS = Path(__file__).resolve().parents[1] / "frontend" / "dashboard.css"
|
||||||
|
|
||||||
|
|
||||||
|
def run_node(script: str) -> dict:
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_work_is_account_scoped_deduplicated_and_bounded():
|
||||||
|
script = f"""
|
||||||
|
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
||||||
|
const values = new Map();
|
||||||
|
const storage = {{
|
||||||
|
getItem:key => values.has(key) ? values.get(key) : null,
|
||||||
|
setItem:(key, value) => values.set(key, value),
|
||||||
|
removeItem:key => values.delete(key),
|
||||||
|
}};
|
||||||
|
let login = ' Alice ';
|
||||||
|
const recent = createRecentWork({{storage, getLogin:() => login, limit:5}});
|
||||||
|
for (let number=1; number<=6; number += 1) {{
|
||||||
|
recent.record({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
|
||||||
|
}}
|
||||||
|
recent.record({{kind:'issue', repository:'stackchain/dashboard', number:3, title:'Issue 3 updated'}});
|
||||||
|
const alice = recent.items();
|
||||||
|
login = 'bob';
|
||||||
|
recent.record({{kind:'pull', repository:'stackchain/api', number:9, title:'Ship API'}});
|
||||||
|
const bob = recent.items();
|
||||||
|
login = '';
|
||||||
|
const anonymousRecord = recent.record({{kind:'issue', repository:'stackchain/dashboard', number:99, title:'Private'}});
|
||||||
|
const anonymous = recent.items();
|
||||||
|
process.stdout.write(JSON.stringify({{alice,bob,anonymousRecord,anonymous,keys:Array.from(values.keys()).sort()}}));
|
||||||
|
"""
|
||||||
|
payload = run_node(script)
|
||||||
|
|
||||||
|
assert [item["number"] for item in payload["alice"]] == [3, 6, 5, 4, 2]
|
||||||
|
assert payload["alice"][0] == {
|
||||||
|
"kind": "issue",
|
||||||
|
"repository": "stackchain/dashboard",
|
||||||
|
"number": 3,
|
||||||
|
"title": "Issue 3 updated",
|
||||||
|
"route": "#/my-work/issue/stackchain/dashboard/3",
|
||||||
|
}
|
||||||
|
assert payload["bob"] == [
|
||||||
|
{
|
||||||
|
"kind": "pull",
|
||||||
|
"repository": "stackchain/api",
|
||||||
|
"number": 9,
|
||||||
|
"title": "Ship API",
|
||||||
|
"route": "#/my-work/pull/stackchain/api/9",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert payload["anonymousRecord"] is False
|
||||||
|
assert payload["anonymous"] == []
|
||||||
|
assert payload["keys"] == [
|
||||||
|
"stackchain.mobile-recent-work.v1.alice",
|
||||||
|
"stackchain.mobile-recent-work.v1.bob",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_work_renders_safe_rows_and_opens_the_selected_route():
|
||||||
|
script = f"""
|
||||||
|
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
||||||
|
const values = new Map([
|
||||||
|
['stackchain.mobile-recent-work.v1.alice', JSON.stringify([
|
||||||
|
{{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Fix mobile queue',route:'#/wrong'}},
|
||||||
|
{{kind:'update',number:42,title:'Review release status'}},
|
||||||
|
{{kind:'pull',repository:'bad/repo/extra',number:1,title:'Unsafe'}},
|
||||||
|
{{kind:'issue',repository:'stackchain/dashboard',number:0,title:'Invalid'}},
|
||||||
|
])],
|
||||||
|
]);
|
||||||
|
function node(tag) {{
|
||||||
|
return {{tag,children:[],attributes:{{}},listeners:{{}},hidden:false,textContent:'',
|
||||||
|
appendChild(child){{this.children.push(child);return child;}},
|
||||||
|
replaceChildren(...children){{this.children=children;}},
|
||||||
|
setAttribute(name,value){{this.attributes[name]=String(value);}},
|
||||||
|
addEventListener(name,callback){{this.listeners[name]=callback;}},
|
||||||
|
click(){{this.listeners.click?.();}},
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
const list=node('div'); const section=node('section'); const opened=[];
|
||||||
|
const recent=createRecentWork({{
|
||||||
|
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}},
|
||||||
|
getLogin:()=>'alice', document:{{createElement:node}}, list, section,
|
||||||
|
openRoute:route=>opened.push(route),
|
||||||
|
}});
|
||||||
|
const rendered=recent.render();
|
||||||
|
list.children[1].click();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
rendered,hidden:section.hidden,rows:list.children.map(button=>({{
|
||||||
|
label:button.attributes['aria-label'],route:button.attributes['data-recent-work-route'],
|
||||||
|
primary:button.children[0].children[0].textContent,
|
||||||
|
secondary:button.children[0].children[1].textContent,
|
||||||
|
}})),opened,
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
payload = run_node(script)
|
||||||
|
|
||||||
|
assert payload == {
|
||||||
|
"rendered": 2,
|
||||||
|
"hidden": False,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"label": "Open Fix mobile queue, issue stackchain/dashboard #7",
|
||||||
|
"route": "#/my-work/issue/stackchain/dashboard/7",
|
||||||
|
"primary": "Fix mobile queue",
|
||||||
|
"secondary": "Issue · stackchain/dashboard #7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Open Review release status, update #42",
|
||||||
|
"route": "#/my-work/update/42",
|
||||||
|
"primary": "Review release status",
|
||||||
|
"secondary": "Update · #42",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"opened": ["#/my-work/update/42"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
||||||
|
html = INDEX.read_text()
|
||||||
|
dashboard = DASHBOARD.read_text()
|
||||||
|
css = CSS.read_text()
|
||||||
|
|
||||||
|
assert 'id="mobile-recent-work"' in html
|
||||||
|
assert 'id="mobile-recent-work-list"' in html
|
||||||
|
assert '<script src="static/mobile-recent-work.js"></script>' in html
|
||||||
|
assert "createMobileRecentWork({" in dashboard
|
||||||
|
assert "mobileRecentWork.record(item)" in dashboard
|
||||||
|
assert "mobileRecentWork.render()" in dashboard
|
||||||
|
assert "workRoute.sync()" in dashboard
|
||||||
|
assert "[data-recent-work-route]" in css
|
||||||
|
assert "min-height:56px" in css
|
||||||
|
|
@ -1527,6 +1527,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/mobile-task-dock.js",
|
"/dashboard/static/mobile-task-dock.js",
|
||||||
"/dashboard/static/mobile-first-task.js",
|
"/dashboard/static/mobile-first-task.js",
|
||||||
"/dashboard/static/mobile-work-entry.js",
|
"/dashboard/static/mobile-work-entry.js",
|
||||||
|
"/dashboard/static/mobile-recent-work.js",
|
||||||
"/dashboard/static/mobile-queue-priority.js",
|
"/dashboard/static/mobile-queue-priority.js",
|
||||||
"/dashboard/static/mobile-queue-launcher.js",
|
"/dashboard/static/mobile-queue-launcher.js",
|
||||||
"/dashboard/static/mobile-delivery-recovery.js",
|
"/dashboard/static/mobile-delivery-recovery.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user