Merge pull request 'Enable mobile capture before optional workspace hydration' (#1408) from timmy/1407-progressive-mobile-capture into main
This commit is contained in:
commit
dcad1da1da
|
|
@ -1203,6 +1203,12 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.voice-conversation-review textarea { box-sizing:border-box; width:100%; min-height:96px; resize:vertical; }
|
.voice-conversation-review textarea { box-sizing:border-box; width:100%; min-height:96px; resize:vertical; }
|
||||||
.create-issue-capture-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
|
.create-issue-capture-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
|
||||||
.create-issue-capture-actions[hidden] { display:none; }
|
.create-issue-capture-actions[hidden] { display:none; }
|
||||||
|
.create-issue-sheet.progressive-capture .mobile-create-issue-nav,
|
||||||
|
.create-issue-sheet.progressive-capture .create-issue-attachment,
|
||||||
|
.create-issue-sheet.progressive-capture .create-issue-filing,
|
||||||
|
.create-issue-sheet.progressive-capture #switch-to-create-pull,
|
||||||
|
.create-issue-sheet.progressive-capture .today-capture-interruption,
|
||||||
|
.create-issue-sheet.progressive-capture .shared-content-conflict { display:none; }
|
||||||
.create-issue-filing { display:grid; gap:12px; min-width:0; }
|
.create-issue-filing { display:grid; gap:12px; min-width:0; }
|
||||||
.create-issue-filing[hidden] { display:none; }
|
.create-issue-filing[hidden] { display:none; }
|
||||||
.create-issue-attachment { display:grid; gap:8px; min-width:0; }
|
.create-issue-attachment { display:grid; gap:8px; min-width:0; }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
(async function(){
|
(async function(){
|
||||||
const workspaceLifecycle = await (window.stackchainWorkspaceLifecycle || loadWorkspace({ document, window }));
|
const workspaceLifecycle = await (window.stackchainWorkspaceLifecycle || loadWorkspace({ document, window }));
|
||||||
await workspaceLifecycle.optionalReady;
|
await workspaceLifecycle.optionalReady;
|
||||||
|
const progressiveCaptureHandoff = window.stackchainProgressiveCapture?.handoff?.();
|
||||||
const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();
|
const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();
|
||||||
window.stackchainProgressiveMyWork?.stop();
|
window.stackchainProgressiveMyWork?.stop();
|
||||||
const qs = (s, el=document) => el.querySelector(s);
|
const qs = (s, el=document) => el.querySelector(s);
|
||||||
|
|
@ -6426,6 +6427,11 @@
|
||||||
if (selector === '#create-issue-title') scheduleIssueDuplicateCheck();
|
if (selector === '#create-issue-title') scheduleIssueDuplicateCheck();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (progressiveCaptureHandoff?.open && await ensureIssueCapture()) {
|
||||||
|
issueCapture.saveDraft({repository:'', labelIds:[],
|
||||||
|
title:progressiveCaptureHandoff.title, body:progressiveCaptureHandoff.body});
|
||||||
|
await openCreateIssueSheet(false);
|
||||||
|
}
|
||||||
qs('#create-issue-repository').addEventListener('change', event => {
|
qs('#create-issue-repository').addEventListener('change', event => {
|
||||||
issueTemplatePicker.changeRepository(currentIssueCaptureDraft());
|
issueTemplatePicker.changeRepository(currentIssueCaptureDraft());
|
||||||
issueOwnerPicker.reset(event.target.value);
|
issueOwnerPicker.reset(event.target.value);
|
||||||
|
|
|
||||||
|
|
@ -2285,6 +2285,7 @@
|
||||||
<script src="static/offline-today.js"></script>
|
<script src="static/offline-today.js"></script>
|
||||||
<script src="static/my-work.js"></script>
|
<script src="static/my-work.js"></script>
|
||||||
<script src="static/progressive-my-work.js"></script>
|
<script src="static/progressive-my-work.js"></script>
|
||||||
|
<script src="static/progressive-capture.js"></script>
|
||||||
<script src="static/agenda-replan.js"></script>
|
<script src="static/agenda-replan.js"></script>
|
||||||
<script src="static/agenda-calendar.js"></script>
|
<script src="static/agenda-calendar.js"></script>
|
||||||
<script src="static/protect-today.js"></script>
|
<script src="static/protect-today.js"></script>
|
||||||
|
|
|
||||||
128
frontend/progressive-capture.js
Normal file
128
frontend/progressive-capture.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
function createProgressiveCapture({
|
||||||
|
document,
|
||||||
|
storage,
|
||||||
|
getLogin = () => '',
|
||||||
|
createCaptures = globalThis.createUnfiledCaptures,
|
||||||
|
requestFullWorkspace = async () => {
|
||||||
|
const lifecycle = await globalThis.stackchainWorkspaceLifecycle;
|
||||||
|
return lifecycle?.optionalReady;
|
||||||
|
},
|
||||||
|
createId,
|
||||||
|
now,
|
||||||
|
}) {
|
||||||
|
const button = document.querySelector('[data-mobile-task="new"]');
|
||||||
|
const root = document.querySelector('#create-issue-sheet');
|
||||||
|
const title = document.querySelector('#create-issue-title');
|
||||||
|
const body = document.querySelector('#create-issue-body');
|
||||||
|
const heading = document.querySelector('#create-issue-heading');
|
||||||
|
const save = document.querySelector('#save-unfiled-issue');
|
||||||
|
const file = document.querySelector('#file-new-issue');
|
||||||
|
const cancel = document.querySelector('#cancel-new-issue');
|
||||||
|
const status = document.querySelector('#my-work-action-status');
|
||||||
|
const captures = createCaptures({
|
||||||
|
storage,
|
||||||
|
getCaptureLogin:getLogin,
|
||||||
|
getCurrentLogin:getLogin,
|
||||||
|
...(createId ? {createId} : {}),
|
||||||
|
...(now ? {now} : {}),
|
||||||
|
});
|
||||||
|
const listeners = [];
|
||||||
|
let started = false;
|
||||||
|
let saving = null;
|
||||||
|
let saved = false;
|
||||||
|
let handedOff = false;
|
||||||
|
|
||||||
|
function listen(element, name, callback) {
|
||||||
|
element?.addEventListener?.(name, callback);
|
||||||
|
listeners.push([element, name, callback]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root?.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(event) {
|
||||||
|
saved = false;
|
||||||
|
if (heading) heading.textContent = 'Capture work';
|
||||||
|
root?.classList.add('open');
|
||||||
|
title?.focus?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDraft() {
|
||||||
|
if (saved || saving) return saving;
|
||||||
|
save.disabled = true;
|
||||||
|
saving = Promise.resolve().then(() => captures.save({
|
||||||
|
title:title?.value || '', body:body?.value || '',
|
||||||
|
})).then(() => {
|
||||||
|
saved = true;
|
||||||
|
if (title) title.value = '';
|
||||||
|
if (body) body.value = '';
|
||||||
|
close();
|
||||||
|
if (status) status.textContent = 'Saved to Drafts. Choose a repository when you’re ready to file it.';
|
||||||
|
return true;
|
||||||
|
}).catch(error => {
|
||||||
|
if (status) status.textContent = error.message;
|
||||||
|
title?.focus?.();
|
||||||
|
return false;
|
||||||
|
}).finally(() => {
|
||||||
|
save.disabled = false;
|
||||||
|
saving = null;
|
||||||
|
});
|
||||||
|
return saving;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileNow() {
|
||||||
|
if (file.disabled) return false;
|
||||||
|
file.disabled = true;
|
||||||
|
if (status) status.textContent = 'Loading filing tools…';
|
||||||
|
try {
|
||||||
|
await requestFullWorkspace();
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
file.disabled = false;
|
||||||
|
if (status) status.textContent = 'Filing tools unavailable. Your capture is still editable; retry when connected.';
|
||||||
|
title?.focus?.();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (started) return false;
|
||||||
|
started = true;
|
||||||
|
root?.classList.add('progressive-capture');
|
||||||
|
listen(button, 'click', open);
|
||||||
|
listen(save, 'click', saveDraft);
|
||||||
|
listen(file, 'click', fileNow);
|
||||||
|
listen(cancel, 'click', close);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handoff() {
|
||||||
|
if (handedOff) return null;
|
||||||
|
handedOff = true;
|
||||||
|
const state = {
|
||||||
|
open:Boolean(root?.classList.contains('open')),
|
||||||
|
title:title?.value || '', body:body?.value || '',
|
||||||
|
};
|
||||||
|
root?.classList.remove('progressive-capture');
|
||||||
|
file.disabled = false;
|
||||||
|
listeners.forEach(([element, name, callback]) => element?.removeEventListener?.(name, callback));
|
||||||
|
listeners.length = 0;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {start, handoff};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined' &&
|
||||||
|
typeof createUnfiledCaptures === 'function' &&
|
||||||
|
window.matchMedia?.('(max-width: 600px)').matches === true) {
|
||||||
|
window.stackchainProgressiveCapture = createProgressiveCapture({
|
||||||
|
document,
|
||||||
|
storage:window.localStorage,
|
||||||
|
getLogin:() => window.stackchainProgressiveMyWork?.login?.() || '',
|
||||||
|
});
|
||||||
|
window.stackchainProgressiveCapture.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = createProgressiveCapture;
|
||||||
|
|
@ -9,6 +9,7 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
let selectedByUser = false;
|
let selectedByUser = false;
|
||||||
let liveSnapshot = null;
|
let liveSnapshot = null;
|
||||||
let liveSnapshotPromise = null;
|
let liveSnapshotPromise = null;
|
||||||
|
let confirmedLogin = '';
|
||||||
const deferredQueues = {
|
const deferredQueues = {
|
||||||
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
||||||
};
|
};
|
||||||
|
|
@ -60,6 +61,7 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
login() { return confirmedLogin; },
|
||||||
handoff() {
|
handoff() {
|
||||||
const state = { selectedFilter:selectedByUser ? active : null };
|
const state = { selectedFilter:selectedByUser ? active : null };
|
||||||
if (liveSnapshot) {
|
if (liveSnapshot) {
|
||||||
|
|
@ -87,6 +89,7 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
if (transferable) liveSnapshot = snapshot;
|
if (transferable) liveSnapshot = snapshot;
|
||||||
else liveSnapshotPromise = null;
|
else liveSnapshotPromise = null;
|
||||||
const context = snapshot?.context || snapshot || {};
|
const context = snapshot?.context || snapshot || {};
|
||||||
|
confirmedLogin = String(context.user?.login || '').trim();
|
||||||
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
||||||
updateCounts();
|
updateCounts();
|
||||||
render();
|
render();
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ const SHELL = [
|
||||||
BASE + 'static/offline-today.js',
|
BASE + 'static/offline-today.js',
|
||||||
BASE + 'static/my-work.js',
|
BASE + 'static/my-work.js',
|
||||||
BASE + 'static/progressive-my-work.js',
|
BASE + 'static/progressive-my-work.js',
|
||||||
|
BASE + 'static/progressive-capture.js',
|
||||||
BASE + 'static/agenda-replan.js',
|
BASE + 'static/agenda-replan.js',
|
||||||
BASE + 'static/agenda-calendar.js',
|
BASE + 'static/agenda-calendar.js',
|
||||||
BASE + 'static/protect-today.js',
|
BASE + 'static/protect-today.js',
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ COMMONJS_BROWSER_BRANCH = re.compile(
|
||||||
)
|
)
|
||||||
WORKER_RUNTIME_SOURCE = "static/background-issue-sync.js"
|
WORKER_RUNTIME_SOURCE = "static/background-issue-sync.js"
|
||||||
FEATURE_SOURCES = {
|
FEATURE_SOURCES = {
|
||||||
"work-core": ("static/my-work.js", "static/progressive-my-work.js"),
|
"work-core": (
|
||||||
|
"static/my-work.js", "static/progressive-my-work.js",
|
||||||
|
"static/unfiled-captures.js", "static/progressive-capture.js",
|
||||||
|
),
|
||||||
"comment-actions": ("static/comment-actions.js",),
|
"comment-actions": ("static/comment-actions.js",),
|
||||||
"issue-capture": (
|
"issue-capture": (
|
||||||
"static/voice-transcript-store.js", "static/voice-issue-capture.js", "static/create-issue-sheet.js", "static/create-pull-sheet.js", "static/mobile-create-issue-nav.js", "static/update-follow-up.js", "static/shared-image-capture.js",
|
"static/voice-transcript-store.js", "static/voice-issue-capture.js", "static/create-issue-sheet.js", "static/create-pull-sheet.js", "static/mobile-create-issue-nav.js", "static/update-follow-up.js", "static/shared-image-capture.js",
|
||||||
|
|
@ -42,7 +45,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-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-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-captures.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",
|
||||||
"static/today-work.js", "static/today-sync.js", "static/pick-work.js", "static/batch-find-work.js",
|
"static/today-work.js", "static/today-sync.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,11 @@ def test_my_work_has_a_small_blocking_bundle_before_optional_workspace_hydration
|
||||||
|
|
||||||
assert b"function buildMyWork" in work_core.runtime_bytes
|
assert b"function buildMyWork" in work_core.runtime_bytes
|
||||||
assert b"function createProgressiveMyWork" in work_core.runtime_bytes
|
assert b"function createProgressiveMyWork" in work_core.runtime_bytes
|
||||||
|
assert b"function createProgressiveCapture" in work_core.runtime_bytes
|
||||||
|
assert b"function createUnfiledCaptures" in work_core.runtime_bytes
|
||||||
assert b"function buildMyWork" not in today.runtime_bytes
|
assert b"function buildMyWork" not in today.runtime_bytes
|
||||||
|
assert b"function createProgressiveCapture" not in today.runtime_bytes
|
||||||
|
assert b"function createUnfiledCaptures" not in today.runtime_bytes
|
||||||
assert b"loadWorkspace({document,window})" in build.runtime_bytes
|
assert b"loadWorkspace({document,window})" in build.runtime_bytes
|
||||||
assert b"const workspaceLifecycle" not in build.runtime_bytes
|
assert b"const workspaceLifecycle" not in build.runtime_bytes
|
||||||
assert b"const workspaceLifecycle" in today.runtime_bytes
|
assert b"const workspaceLifecycle" in today.runtime_bytes
|
||||||
|
|
|
||||||
164
tests/test_progressive_capture.py
Normal file
164
tests/test_progressive_capture.py
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-capture.js"
|
||||||
|
UNFILED = Path(__file__).parents[1] / "frontend" / "unfiled-captures.js"
|
||||||
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
|
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||||
|
|
||||||
|
|
||||||
|
def run_capture(scenario: str) -> dict:
|
||||||
|
harness = f"""
|
||||||
|
const createProgressiveCapture = require({json.dumps(str(MODULE))});
|
||||||
|
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||||
|
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", "-e", harness], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_mobile_new_opens_and_saves_one_account_bound_draft():
|
||||||
|
result = run_capture(r"""
|
||||||
|
const listeners = new Map();
|
||||||
|
const button = {dataset:{mobileTask:'new'}, addEventListener(name, callback){listeners.set('new:'+name, callback);}, removeEventListener(name, callback){if(listeners.get('new:'+name)===callback) listeners.delete('new:'+name);}, setAttribute(){}, removeAttribute(){}};
|
||||||
|
const save = {disabled:false, addEventListener(name, callback){listeners.set('save:'+name, callback);}, removeEventListener(name, callback){if(listeners.get('save:'+name)===callback) listeners.delete('save:'+name);}};
|
||||||
|
const file = {disabled:false, addEventListener(){}, removeEventListener(){}};
|
||||||
|
const cancel = {addEventListener(){}, removeEventListener(){}};
|
||||||
|
const title = {value:'', focused:0, focus(){this.focused++;}};
|
||||||
|
const body = {value:''};
|
||||||
|
const heading = {textContent:''};
|
||||||
|
const status = {textContent:''};
|
||||||
|
const classes = new Set();
|
||||||
|
const root = {classList:{add:name=>classes.add(name), remove:name=>classes.delete(name), contains:name=>classes.has(name)}};
|
||||||
|
const nodes = {
|
||||||
|
'#create-issue-sheet':root, '#create-issue-title':title, '#create-issue-body':body,
|
||||||
|
'#create-issue-heading':heading, '#save-unfiled-issue':save, '#file-new-issue':file,
|
||||||
|
'#cancel-new-issue':cancel, '#my-work-action-status':status,
|
||||||
|
};
|
||||||
|
const document = {
|
||||||
|
querySelector(selector){return selector==='[data-mobile-task="new"]'?button:(nodes[selector]||null);},
|
||||||
|
};
|
||||||
|
const values = new Map();
|
||||||
|
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
|
||||||
|
const flow = createProgressiveCapture({document, storage, getLogin:()=> 'timmy', createCaptures:createUnfiledCaptures, createId:()=> 'draft-1', now:()=> 42});
|
||||||
|
flow.start();
|
||||||
|
listeners.get('new:click')({currentTarget:button});
|
||||||
|
title.value='Production outage'; body.value='Investigate mobile reports';
|
||||||
|
await listeners.get('save:click')();
|
||||||
|
await listeners.get('save:click')();
|
||||||
|
const stored=JSON.parse(values.get('stackchain.unfiled-issues.v1'));
|
||||||
|
console.log(JSON.stringify({open:classes.has('open'),focused:title.focused,heading:heading.textContent,status:status.textContent,items:stored.items,listeners:[...listeners.keys()]}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"open": False,
|
||||||
|
"focused": 1,
|
||||||
|
"heading": "Capture work",
|
||||||
|
"status": "Saved to Drafts. Choose a repository when you’re ready to file it.",
|
||||||
|
"items": [{
|
||||||
|
"id": "draft-1",
|
||||||
|
"ownerLogin": "timmy",
|
||||||
|
"title": "Production outage",
|
||||||
|
"body": "Investigate mobile reports",
|
||||||
|
"savedAt": 42,
|
||||||
|
}],
|
||||||
|
"listeners": ["new:click", "save:click"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_file_now_preserves_capture_on_failure_and_hands_it_off_once():
|
||||||
|
result = run_capture(r"""
|
||||||
|
const listeners = new Map();
|
||||||
|
const control = name => ({disabled:false,
|
||||||
|
addEventListener(event, callback){listeners.set(name+':'+event, callback);},
|
||||||
|
removeEventListener(event, callback){if(listeners.get(name+':'+event)===callback) listeners.delete(name+':'+event);},
|
||||||
|
focus(){this.focused=(this.focused||0)+1;},
|
||||||
|
});
|
||||||
|
const button=control('new'); const save=control('save'); const file=control('file'); const cancel=control('cancel');
|
||||||
|
const title=control('title'); title.value='Keep this'; const body={value:'Do not lose this note'};
|
||||||
|
const status={textContent:''}; const heading={textContent:''}; const classes=new Set();
|
||||||
|
const root={classList:{add:name=>classes.add(name),remove:name=>classes.delete(name),contains:name=>classes.has(name)}};
|
||||||
|
const nodes={'#create-issue-sheet':root,'#create-issue-title':title,'#create-issue-body':body,
|
||||||
|
'#create-issue-heading':heading,'#save-unfiled-issue':save,'#file-new-issue':file,
|
||||||
|
'#cancel-new-issue':cancel,'#my-work-action-status':status};
|
||||||
|
const document={querySelector:selector=>selector==='[data-mobile-task="new"]'?button:(nodes[selector]||null)};
|
||||||
|
let requests=0;
|
||||||
|
const flow=createProgressiveCapture({
|
||||||
|
document,storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',createCaptures:createUnfiledCaptures,
|
||||||
|
requestFullWorkspace:async()=>{requests++;throw new Error('offline');},
|
||||||
|
});
|
||||||
|
flow.start(); listeners.get('new:click')({currentTarget:button});
|
||||||
|
const filed=await listeners.get('file:click')();
|
||||||
|
const before=Array.from(listeners.keys()).sort();
|
||||||
|
const handoff=flow.handoff(); const second=flow.handoff();
|
||||||
|
console.log(JSON.stringify({filed,requests,open:classes.has('open'),status:status.textContent,
|
||||||
|
fileDisabled:file.disabled,focused:title.focused,title:title.value,body:body.value,before,
|
||||||
|
after:Array.from(listeners.keys()),handoff,second}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"filed": False,
|
||||||
|
"requests": 1,
|
||||||
|
"open": True,
|
||||||
|
"status": "Filing tools unavailable. Your capture is still editable; retry when connected.",
|
||||||
|
"fileDisabled": False,
|
||||||
|
"focused": 2,
|
||||||
|
"title": "Keep this",
|
||||||
|
"body": "Do not lose this note",
|
||||||
|
"before": ["cancel:click", "file:click", "new:click", "save:click"],
|
||||||
|
"after": [],
|
||||||
|
"handoff": {"open": True, "title": "Keep this", "body": "Do not lose this note"},
|
||||||
|
"second": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_hydrated_dashboard_adopts_open_progressive_capture_without_losing_fields():
|
||||||
|
source = DASHBOARD.read_text()
|
||||||
|
|
||||||
|
assert "const progressiveCaptureHandoff = window.stackchainProgressiveCapture?.handoff?.();" in source
|
||||||
|
assert "issueCapture.saveDraft({repository:'', labelIds:[]," in source
|
||||||
|
assert "title:progressiveCaptureHandoff.title, body:progressiveCaptureHandoff.body" in source
|
||||||
|
assert "await openCreateIssueSheet(false);" in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_capture_is_mobile_only_and_hides_unhydrated_filing_controls():
|
||||||
|
source = MODULE.read_text()
|
||||||
|
css = CSS.read_text()
|
||||||
|
|
||||||
|
assert "window.matchMedia?.('(max-width: 600px)').matches === true" in source
|
||||||
|
assert "root?.classList.add('progressive-capture')" in source
|
||||||
|
assert "root?.classList.remove('progressive-capture')" in source
|
||||||
|
assert ".create-issue-sheet.progressive-capture .mobile-create-issue-nav" in css
|
||||||
|
assert ".create-issue-sheet.progressive-capture .create-issue-attachment" in css
|
||||||
|
assert ".create-issue-sheet.progressive-capture .create-issue-filing" in css
|
||||||
|
assert ".create-issue-sheet.progressive-capture #switch-to-create-pull" in css
|
||||||
|
|
||||||
|
|
||||||
|
def test_successful_filing_handoff_reenables_the_hydrated_file_control():
|
||||||
|
result = run_capture(r"""
|
||||||
|
const listeners={};
|
||||||
|
const control=name=>({disabled:false,addEventListener(event,callback){listeners[name+event]=callback;},removeEventListener(){},focus(){}});
|
||||||
|
const button=control('new'); const save=control('save'); const file=control('file'); const cancel=control('cancel');
|
||||||
|
const title={value:'Ready to file',focus(){}}; const body={value:'Context'}; const status={textContent:''};
|
||||||
|
const classes=new Set(); const root={classList:{add:name=>classes.add(name),remove:name=>classes.delete(name),contains:name=>classes.has(name)}};
|
||||||
|
const nodes={'#create-issue-sheet':root,'#create-issue-title':title,'#create-issue-body':body,
|
||||||
|
'#create-issue-heading':{textContent:''},'#save-unfiled-issue':save,'#file-new-issue':file,
|
||||||
|
'#cancel-new-issue':cancel,'#my-work-action-status':status};
|
||||||
|
const document={querySelector:selector=>selector==='[data-mobile-task="new"]'?button:nodes[selector]};
|
||||||
|
const flow=createProgressiveCapture({document,storage:{getItem:()=>null,setItem(){}},getLogin:()=>'timmy',
|
||||||
|
createCaptures:createUnfiledCaptures,requestFullWorkspace:async()=>true});
|
||||||
|
flow.start(); listeners.newclick({currentTarget:button});
|
||||||
|
const filed=await listeners.fileclick(); const before=file.disabled; const handoff=flow.handoff();
|
||||||
|
console.log(JSON.stringify({filed,before,after:file.disabled,handoff}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"filed": True,
|
||||||
|
"before": True,
|
||||||
|
"after": False,
|
||||||
|
"handoff": {"open": True, "title": "Ready to file", "body": "Context"},
|
||||||
|
}
|
||||||
|
|
@ -219,4 +219,28 @@ const flow=context.module.exports({{document,fetchSnapshot:()=>new Promise(resol
|
||||||
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
||||||
"events": [],
|
"events": [],
|
||||||
"notifications": [],
|
"notifications": [],
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_capture_can_read_only_the_confirmed_snapshot_login():
|
||||||
|
harness = f"""
|
||||||
|
const fs=require('fs'); const vm=require('vm');
|
||||||
|
const document={{
|
||||||
|
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
||||||
|
querySelectorAll:()=>[],
|
||||||
|
}};
|
||||||
|
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
||||||
|
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
||||||
|
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
||||||
|
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
||||||
|
const flow=context.module.exports({{document,fetchSnapshot:async()=>({{
|
||||||
|
context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}}, events:[], notifications:[],
|
||||||
|
}})}});
|
||||||
|
(async()=>{{
|
||||||
|
const before=flow.login(); await flow.start(); const after=flow.login(); flow.stop();
|
||||||
|
console.log(JSON.stringify({{before,after,stopped:flow.login()}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {"before": "", "after": "timmy", "stopped": "timmy"}
|
||||||
|
|
@ -1409,6 +1409,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/offline-today.js",
|
"/dashboard/static/offline-today.js",
|
||||||
"/dashboard/static/my-work.js",
|
"/dashboard/static/my-work.js",
|
||||||
"/dashboard/static/progressive-my-work.js",
|
"/dashboard/static/progressive-my-work.js",
|
||||||
|
"/dashboard/static/progressive-capture.js",
|
||||||
"/dashboard/static/agenda-replan.js",
|
"/dashboard/static/agenda-replan.js",
|
||||||
"/dashboard/static/agenda-calendar.js",
|
"/dashboard/static/agenda-calendar.js",
|
||||||
"/dashboard/static/protect-today.js",
|
"/dashboard/static/protect-today.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user