feat: preview work before planning (#405)
This commit is contained in:
parent
461f90c8ff
commit
7a62f7ead7
|
|
@ -94,6 +94,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.plan-today-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:10px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; }
|
.plan-today-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:10px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; }
|
||||||
.plan-today-item-copy { min-width:0; overflow-wrap:anywhere; }
|
.plan-today-item-copy { min-width:0; overflow-wrap:anywhere; }
|
||||||
.plan-today-item-actions { display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end; }
|
.plan-today-item-actions { display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end; }
|
||||||
|
.plan-today-candidate-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:6px; }
|
||||||
|
.plan-preview-actions { position:fixed; z-index:76; right:16px; bottom:calc(16px + env(safe-area-inset-bottom)); display:grid; grid-template-columns:1fr 1fr; gap:8px; width:min(420px,calc(100vw - 32px)); padding:10px; border:1px solid #31577f; border-radius:12px; background:rgba(11,21,38,.98); box-shadow:0 12px 36px rgba(0,0,0,.45); }
|
||||||
|
.plan-preview-actions[hidden] { display:none; }
|
||||||
|
.plan-preview-actions button { min-height:44px; }
|
||||||
.plan-today-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:16px -6px -6px; padding:12px 6px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
|
.plan-today-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:16px -6px -6px; padding:12px 6px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
|
||||||
.plan-today-actions button { min-height:44px; width:100%; }
|
.plan-today-actions button { min-height:44px; width:100%; }
|
||||||
.my-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
.my-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
||||||
|
|
@ -365,6 +369,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.plan-today-panel { width:100%; border-left:0; padding:14px; }
|
.plan-today-panel { width:100%; border-left:0; padding:14px; }
|
||||||
.plan-today-item { grid-template-columns:1fr; }
|
.plan-today-item { grid-template-columns:1fr; }
|
||||||
.plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr); width:100%; }
|
.plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr); width:100%; }
|
||||||
|
.plan-today-candidate-actions { grid-template-columns:repeat(2,1fr); width:100%; }
|
||||||
.plan-today-item-actions button { min-width:0; padding-inline:4px; }
|
.plan-today-item-actions button { min-width:0; padding-inline:4px; }
|
||||||
.work-filters { width:100%; }
|
.work-filters { width:100%; }
|
||||||
.work-filter { flex:1 1 calc(50% - 8px); }
|
.work-filter { flex:1 1 calc(50% - 8px); }
|
||||||
|
|
|
||||||
|
|
@ -728,7 +728,7 @@
|
||||||
const title = escapeHtml(item.title || 'Untitled work');
|
const title = escapeHtml(item.title || 'Untitled work');
|
||||||
const controls = selected ?
|
const controls = selected ?
|
||||||
'<div class="plan-today-item-actions"><button type="button" data-plan-move="up" data-plan-id="' + escAttr(id) + '"' + (index === 0 ? ' disabled' : '') + '>Up</button><button type="button" data-plan-move="down" data-plan-id="' + escAttr(id) + '"' + (index === planToday.snapshot().count - 1 ? ' disabled' : '') + '>Down</button><button type="button" data-plan-remove="' + escAttr(id) + '">Remove</button></div>' :
|
'<div class="plan-today-item-actions"><button type="button" data-plan-move="up" data-plan-id="' + escAttr(id) + '"' + (index === 0 ? ' disabled' : '') + '>Up</button><button type="button" data-plan-move="down" data-plan-id="' + escAttr(id) + '"' + (index === planToday.snapshot().count - 1 ? ' disabled' : '') + '>Down</button><button type="button" data-plan-remove="' + escAttr(id) + '">Remove</button></div>' :
|
||||||
'<button type="button" data-plan-add="' + escAttr(id) + '">Add</button>';
|
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview</button><button type="button" data-plan-add="' + escAttr(id) + '">Add</button></div>';
|
||||||
return '<article class="plan-today-item"><div class="plan-today-item-copy"><span class="small">' + key + '</span><strong class="my-work-card-title">' + title + '</strong></div>' + controls + '</article>';
|
return '<article class="plan-today-item"><div class="plan-today-item-copy"><span class="small">' + key + '</span><strong class="my-work-card-title">' + title + '</strong></div>' + controls + '</article>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -747,6 +747,15 @@
|
||||||
qs('#plan-today-error').textContent = result === 'full' ? 'Today is full. Remove an item before adding another.' : '';
|
qs('#plan-today-error').textContent = result === 'full' ? 'Today is full. Remove an item before adding another.' : '';
|
||||||
renderPlanToday();
|
renderPlanToday();
|
||||||
}));
|
}));
|
||||||
|
document.querySelectorAll('[data-plan-preview]').forEach(button => button.addEventListener('click', () => {
|
||||||
|
const item = planToday.item(button.dataset.planPreview);
|
||||||
|
if (!canPreviewPlanItem(item)) {
|
||||||
|
qs('#plan-today-error').textContent = 'Preview unavailable offline. Reconnect or add the item without previewing it.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qs('#plan-today-error').textContent = '';
|
||||||
|
if (planTodayPreview.open(item, button)) taskOverlayHistory.open('plan-today-preview');
|
||||||
|
}));
|
||||||
document.querySelectorAll('[data-plan-remove]').forEach(button => button.addEventListener('click', () => {
|
document.querySelectorAll('[data-plan-remove]').forEach(button => button.addEventListener('click', () => {
|
||||||
planToday.toggle(planToday.item(button.dataset.planRemove));
|
planToday.toggle(planToday.item(button.dataset.planRemove));
|
||||||
qs('#plan-today-error').textContent = '';
|
qs('#plan-today-error').textContent = '';
|
||||||
|
|
@ -792,6 +801,58 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function canPreviewPlanItem(item) {
|
||||||
|
if (!item || !offlineWorkMode) return Boolean(item);
|
||||||
|
const login = planningOwnerLogin || confirmedOwnerLogin ||
|
||||||
|
String(offlineWorkStore.load()?.user?.login || '').trim();
|
||||||
|
return Boolean(offlineWorkStore.loadDetail(login, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPlanPreviewDetail(item, trigger) {
|
||||||
|
qs('#plan-today-sheet').hidden = true;
|
||||||
|
qs('#plan-preview-actions').hidden = false;
|
||||||
|
if (offlineWorkMode) {
|
||||||
|
openRoutedWork(item, trigger);
|
||||||
|
} else if (item.kind === 'update' && item.has_update) {
|
||||||
|
updateTrigger = trigger;
|
||||||
|
notificationReader.open(item, lastMyWork);
|
||||||
|
} else if (item.is_review || item.kind === 'review') {
|
||||||
|
reviewTrigger = trigger;
|
||||||
|
openReviewSheet(item, trigger);
|
||||||
|
} else if (item.kind === 'issue') {
|
||||||
|
issueTrigger = trigger;
|
||||||
|
openIssueSheet(item, trigger);
|
||||||
|
} else if (item.kind === 'pull') {
|
||||||
|
pullTrigger = trigger;
|
||||||
|
openPullSheet(item, trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const planTodayPreview = createPlanTodayPreview({
|
||||||
|
planner: planToday,
|
||||||
|
identity: item => todayWork.identity(item),
|
||||||
|
getScroll: () => qs('.plan-today-panel').scrollTop,
|
||||||
|
setScroll: value => requestAnimationFrame(() => { qs('.plan-today-panel').scrollTop = value; }),
|
||||||
|
onOpen: openPlanPreviewDetail,
|
||||||
|
onClose: (_item, trigger) => {
|
||||||
|
closeOpenWorkSheets();
|
||||||
|
qs('#plan-preview-actions').hidden = true;
|
||||||
|
qs('#plan-today-sheet').hidden = false;
|
||||||
|
renderPlanToday();
|
||||||
|
requestAnimationFrame(() => trigger?.focus());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let addPlanPreviewOnReturn = false;
|
||||||
|
|
||||||
|
qs('#back-to-plan').addEventListener('click', () => {
|
||||||
|
addPlanPreviewOnReturn = false;
|
||||||
|
taskOverlayHistory.close();
|
||||||
|
});
|
||||||
|
qs('#add-plan-preview').addEventListener('click', () => {
|
||||||
|
addPlanPreviewOnReturn = true;
|
||||||
|
taskOverlayHistory.close();
|
||||||
|
});
|
||||||
|
|
||||||
function openPlanToday(trigger, navigate = true) {
|
function openPlanToday(trigger, navigate = true) {
|
||||||
if (!planningOwnerLogin) {
|
if (!planningOwnerLogin) {
|
||||||
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
|
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
|
||||||
|
|
@ -2652,11 +2713,16 @@
|
||||||
mobileSearchViewport.close();
|
mobileSearchViewport.close();
|
||||||
qs('#open-palette').focus();
|
qs('#open-palette').focus();
|
||||||
}
|
}
|
||||||
if (previous === 'plan-today' && kind !== 'plan-today') closePlanToday(false);
|
if (previous === 'plan-today-preview' && kind !== 'plan-today-preview') {
|
||||||
|
if (addPlanPreviewOnReturn) planTodayPreview.close({ add:true });
|
||||||
|
else planTodayPreview.close();
|
||||||
|
addPlanPreviewOnReturn = false;
|
||||||
|
}
|
||||||
|
if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false);
|
||||||
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
||||||
if (kind === 'find' && previous !== 'find') openFindWorkSheet(false);
|
if (kind === 'find' && previous !== 'find') openFindWorkSheet(false);
|
||||||
if (kind === 'search' && previous !== 'search-preview') openCommandPalette(false);
|
if (kind === 'search' && previous !== 'search-preview') openCommandPalette(false);
|
||||||
if (kind === 'plan-today' && previous !== 'plan-today') openPlanToday(planTodayTrigger, false);
|
if (kind === 'plan-today' && previous !== 'plan-today' && previous !== 'plan-today-preview') openPlanToday(planTodayTrigger, false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
taskOverlayHistory.start();
|
taskOverlayHistory.start();
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,11 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="plan-preview-actions" id="plan-preview-actions" hidden>
|
||||||
|
<button id="back-to-plan" type="button">Back to plan</button>
|
||||||
|
<button id="add-plan-preview" type="button">Add to Today & back</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
||||||
<div class="cmd-palette-header">
|
<div class="cmd-palette-header">
|
||||||
<strong>Search work</strong>
|
<strong>Search work</strong>
|
||||||
|
|
@ -589,6 +594,7 @@
|
||||||
<script src="static/card-planning.js"></script>
|
<script src="static/card-planning.js"></script>
|
||||||
<script src="static/today-work.js"></script>
|
<script src="static/today-work.js"></script>
|
||||||
<script src="static/plan-today.js"></script>
|
<script src="static/plan-today.js"></script>
|
||||||
|
<script src="static/plan-today-preview.js"></script>
|
||||||
<script src="static/today-sync.js"></script>
|
<script src="static/today-sync.js"></script>
|
||||||
<script src="static/update-ownership.js"></script>
|
<script src="static/update-ownership.js"></script>
|
||||||
<script src="static/later-work.js"></script>
|
<script src="static/later-work.js"></script>
|
||||||
|
|
|
||||||
45
frontend/plan-today-preview.js
Normal file
45
frontend/plan-today-preview.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
const api = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||||
|
else root.createPlanTodayPreview = api;
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
return function createPlanTodayPreview({
|
||||||
|
planner,
|
||||||
|
identity,
|
||||||
|
getScroll = () => 0,
|
||||||
|
setScroll = () => {},
|
||||||
|
onOpen = () => {},
|
||||||
|
onClose = () => {},
|
||||||
|
}) {
|
||||||
|
let current = null;
|
||||||
|
|
||||||
|
function open(item, trigger = null) {
|
||||||
|
if (!item || !identity?.(item) || !planner?.snapshot().open) return false;
|
||||||
|
current = { item, trigger, scroll:Number(getScroll()) || 0 };
|
||||||
|
onOpen(item, trigger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close({ add = false } = {}) {
|
||||||
|
if (!current) return 'closed';
|
||||||
|
const { item, trigger, scroll } = current;
|
||||||
|
let result = 'returned';
|
||||||
|
if (add) {
|
||||||
|
const id = identity(item);
|
||||||
|
result = planner.snapshot().ids.includes(id) ? 'already-added' : planner.toggle(item);
|
||||||
|
}
|
||||||
|
current = null;
|
||||||
|
setScroll(scroll);
|
||||||
|
onClose(item, trigger);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot() {
|
||||||
|
return current ? { open:true, item:current.item, trigger:current.trigger, scroll:current.scroll } : { open:false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { open, close, snapshot };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -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-v64';
|
const CACHE = 'stackchain-dashboard-shell-v65';
|
||||||
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;
|
||||||
|
|
@ -28,6 +28,7 @@ const SHELL = [
|
||||||
BASE + 'static/card-planning.js',
|
BASE + 'static/card-planning.js',
|
||||||
BASE + 'static/today-work.js',
|
BASE + 'static/today-work.js',
|
||||||
BASE + 'static/plan-today.js',
|
BASE + 'static/plan-today.js',
|
||||||
|
BASE + 'static/plan-today-preview.js',
|
||||||
BASE + 'static/today-sync.js',
|
BASE + 'static/today-sync.js',
|
||||||
BASE + 'static/update-ownership.js',
|
BASE + 'static/update-ownership.js',
|
||||||
BASE + 'static/later-work.js',
|
BASE + 'static/later-work.js',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const allowed = new Set(['new', 'find', 'search', 'search-preview', 'plan-today']);
|
const allowed = new Set(['new', 'find', 'search', 'search-preview', 'plan-today', 'plan-today-preview']);
|
||||||
|
|
||||||
return function createTaskOverlayHistory({ history, eventTarget, onChange }) {
|
return function createTaskOverlayHistory({ history, eventTarget, onChange }) {
|
||||||
let active = allowed.has(history.state?.taskOverlay) ? history.state.taskOverlay : null;
|
let active = allowed.has(history.state?.taskOverlay) ? history.state.taskOverlay : null;
|
||||||
|
|
|
||||||
|
|
@ -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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in worker
|
assert "stackchain-dashboard-shell-v65" 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-v64" in worker
|
assert "stackchain-dashboard-shell-v65" in worker
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from tests.dashboard_bundle import dashboard
|
||||||
|
|
||||||
|
|
||||||
PLAN_TODAY = Path(__file__).parents[1] / "frontend" / "plan-today.js"
|
PLAN_TODAY = Path(__file__).parents[1] / "frontend" / "plan-today.js"
|
||||||
|
PLAN_TODAY_PREVIEW = Path(__file__).parents[1] / "frontend" / "plan-today-preview.js"
|
||||||
SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,6 +79,53 @@ process.stdout.write(JSON.stringify({{result, calls, snapshot:planner.snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_today_preview_preserves_draft_scroll_and_adds_item_once_on_return():
|
||||||
|
script = f"""
|
||||||
|
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
|
||||||
|
const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))});
|
||||||
|
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
|
||||||
|
const events = [];
|
||||||
|
let scroll = 318;
|
||||||
|
const planner = createPlanToday({{
|
||||||
|
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
|
||||||
|
save: () => true,
|
||||||
|
}});
|
||||||
|
planner.open([item(1)], [item(1), item(2)]);
|
||||||
|
const preview = createPlanTodayPreview({{
|
||||||
|
planner,
|
||||||
|
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
|
||||||
|
getScroll: () => scroll,
|
||||||
|
setScroll: value => {{ scroll = value; }},
|
||||||
|
onOpen: value => events.push(['open', value.number]),
|
||||||
|
onClose: (value, trigger) => events.push(['close', value.number, trigger]),
|
||||||
|
}});
|
||||||
|
preview.open(item(2), 'preview-2');
|
||||||
|
scroll = 0;
|
||||||
|
const first = preview.close({{add:true}});
|
||||||
|
preview.open(item(2), 'preview-2');
|
||||||
|
scroll = 0;
|
||||||
|
const second = preview.close({{add:true}});
|
||||||
|
process.stdout.write(JSON.stringify({{first, second, scroll, events, planner:planner.snapshot()}}));
|
||||||
|
"""
|
||||||
|
assert run_node(script) == {
|
||||||
|
"first": "added",
|
||||||
|
"second": "already-added",
|
||||||
|
"scroll": 318,
|
||||||
|
"events": [
|
||||||
|
["open", 2],
|
||||||
|
["close", 2, "preview-2"],
|
||||||
|
["open", 2],
|
||||||
|
["close", 2, "preview-2"],
|
||||||
|
],
|
||||||
|
"planner": {
|
||||||
|
"open": True,
|
||||||
|
"ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"],
|
||||||
|
"count": 2,
|
||||||
|
"limit": 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -96,6 +144,13 @@ async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
||||||
assert "padding-bottom:calc(12px + env(safe-area-inset-bottom))" in html
|
assert "padding-bottom:calc(12px + env(safe-area-inset-bottom))" in html
|
||||||
assert ".plan-today-item { grid-template-columns:1fr; }" in html
|
assert ".plan-today-item { grid-template-columns:1fr; }" in html
|
||||||
assert ".plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr);" in html
|
assert ".plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr);" in html
|
||||||
|
assert ".plan-preview-actions { position:fixed; z-index:76;" in html
|
||||||
|
assert '<script src="static/plan-today-preview.js"></script>' in html
|
||||||
|
assert 'data-plan-preview="' in html
|
||||||
|
assert 'id="add-plan-preview"' in html
|
||||||
|
assert "taskOverlayHistory.open('plan-today-preview')" in html
|
||||||
|
assert "planTodayPreview.close({ add:true })" in html
|
||||||
|
assert "Preview unavailable offline" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
@ -113,5 +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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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
|
||||||
|
|
|
||||||
|
|
@ -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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" 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-v64" in source
|
assert "stackchain-dashboard-shell-v65" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -365,6 +365,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/card-planning.js",
|
"/dashboard/static/card-planning.js",
|
||||||
"/dashboard/static/today-work.js",
|
"/dashboard/static/today-work.js",
|
||||||
"/dashboard/static/plan-today.js",
|
"/dashboard/static/plan-today.js",
|
||||||
|
"/dashboard/static/plan-today-preview.js",
|
||||||
"/dashboard/static/today-sync.js",
|
"/dashboard/static/today-sync.js",
|
||||||
"/dashboard/static/update-ownership.js",
|
"/dashboard/static/update-ownership.js",
|
||||||
"/dashboard/static/later-work.js",
|
"/dashboard/static/later-work.js",
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,44 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_today_preview_is_a_nested_browser_history_layer():
|
||||||
|
script = f"""
|
||||||
|
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
|
||||||
|
const listeners = {{}};
|
||||||
|
const changes = [];
|
||||||
|
const stack = [{{ page:'dashboard' }}];
|
||||||
|
let cursor = 0;
|
||||||
|
const history = {{
|
||||||
|
get state() {{ return stack[cursor]; }},
|
||||||
|
pushState(state) {{ stack.splice(cursor + 1); stack.push(state); cursor += 1; }},
|
||||||
|
back() {{ cursor -= 1; listeners.popstate({{state:stack[cursor]}}); }},
|
||||||
|
}};
|
||||||
|
const controller = createTaskOverlayHistory({{
|
||||||
|
history,
|
||||||
|
eventTarget: {{ addEventListener(name, callback) {{ listeners[name] = callback; }} }},
|
||||||
|
onChange(kind, previous) {{ changes.push([kind, previous]); }},
|
||||||
|
}});
|
||||||
|
controller.start();
|
||||||
|
controller.open('plan-today');
|
||||||
|
const previewed = controller.open('plan-today-preview');
|
||||||
|
controller.close();
|
||||||
|
process.stdout.write(JSON.stringify({{previewed, cursor, current:controller.current(), changes}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"previewed": True,
|
||||||
|
"cursor": 1,
|
||||||
|
"current": "plan-today",
|
||||||
|
"changes": [
|
||||||
|
["plan-today", None],
|
||||||
|
["plan-today-preview", "plan-today"],
|
||||||
|
["plan-today", "plan-today-preview"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_routes_mobile_task_overlays_through_browser_history():
|
def test_dashboard_routes_mobile_task_overlays_through_browser_history():
|
||||||
html = dashboard_bundle_text()
|
html = dashboard_bundle_text()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-v64" in source
|
assert "stackchain-dashboard-shell-v65" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user