diff --git a/frontend/dashboard.css b/frontend/dashboard.css index ae2a5b0..5169a76 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -211,6 +211,10 @@ textarea { resize: vertical; min-height: 120px; } .create-issue-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:8px; padding-bottom:env(safe-area-inset-bottom); } .create-issue-actions button { min-height:44px; max-width:100%; width:100%; } .create-issue-actions #create-issue-status { grid-column:1/-1; } +.draft-filing-session { position:sticky; top:0; z-index:2; display:grid; gap:6px; margin-bottom:12px; padding:10px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); border:1px solid #31577f; border-radius:12px; background:rgba(11,21,38,.98); } +.draft-filing-session[hidden] { display:none; } +.draft-filing-session-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; } +.draft-filing-session-actions button { min-height:44px; width:100%; } .my-work-card { min-height: 44px; display:grid; gap:8px; padding:12px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; color:var(--text); } .my-work-card-main { display:block; width:100%; color:var(--text); text-align:left; font:inherit; background:transparent; border:0; padding:0; } .my-work-card-main.review-trigger { width:100%; text-align:left; font:inherit; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 800ff0a..894be15 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -407,7 +407,13 @@ getCaptureLogin: () => String(lastContextSnapshot?.user?.login || '').trim(), getCurrentLogin: () => activeFlushLogin, }); - let resumedUnfiledCaptureId = ''; + const dFS = createDraftFilingSession({list:()=>unfiledCaptures.list().filter(item=>!item.quarantined)}); + dFS.attach(qs, { + captures:unfiledCaptures, issueCapture, attachment:createIssueAttachmentController, + getLogin:()=>activeFlushLogin, setResumedId:id=>{ rUC = id; }, + openSheet:openCreateIssueSheet, setFilingMode:setIssueFilingMode, + }); + let rUC = ''; let backgroundIssueSync = null; if ('indexedDB' in window) { const backgroundIssueStore = createIssueSyncStore(); @@ -2118,12 +2124,8 @@ if (!item) return; if (item.kind === 'unfiled-issue') { try { - const resumed = await unfiledCaptures.resume(item.capture_id, activeFlushLogin); - issueCapture.saveDraft(resumed); - resumedUnfiledCaptureId = item.capture_id; - await openCreateIssueSheet(); - if (resumed.attachment) createIssueAttachmentController.restore(resumed.attachment); - qs('#create-issue-status').textContent = 'Capture restored. Choose a repository to file it.'; + dFS.start(item.capture_id); + await dFS.nextCapture(); } catch (error) { qs('#my-work-action-status').textContent = error.message; } } else if (item.kind === 'new-issue') openCreateIssueSheet(); else if (item.route) workRoute.open(item.route); @@ -4053,6 +4055,7 @@ } }); qs('#new-issue').addEventListener('click', openCreateIssueSheet); + dFS.bind(); qs('#file-new-issue').addEventListener('click', () => { if (!qs('#create-issue-title').value.trim()) { qs('#create-issue-capture-status').textContent = 'Add a title before filing.'; @@ -4073,9 +4076,9 @@ }; if (showDraftCapacityDialog(unfiledCaptures)) return; const savedCapture = await unfiledCaptures.save(captureDraft); - if (resumedUnfiledCaptureId) { - await unfiledCaptures.completeResume(resumedUnfiledCaptureId); - resumedUnfiledCaptureId = ''; + if (rUC) { + await unfiledCaptures.completeResume(rUC); + rUC = ''; } issueCapture.clearDraft(); qs('#create-issue-title').value = ''; @@ -4222,9 +4225,14 @@ const admission = editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, durableDraft) : await issueOutbox.enqueueDurably(durableDraft); const queued = admission.item; - if (resumedUnfiledCaptureId && (!durableDraft.attachment || admission.background)) { - await unfiledCaptures.completeResume(resumedUnfiledCaptureId); - resumedUnfiledCaptureId = ''; + const fS = dFS.current(); + if (rUC && (!durableDraft.attachment || admission.background)) { + await unfiledCaptures.completeResume(rUC); + rUC = ''; + } + if (fS && !rUC && await dFS.advance(fS.id)) { + refreshMyWorkView(); + return; } if (!admission.background) { editingOutboxId = queued.id; diff --git a/frontend/draft-filing-session.js b/frontend/draft-filing-session.js new file mode 100644 index 0000000..66e44d5 --- /dev/null +++ b/frontend/draft-filing-session.js @@ -0,0 +1,92 @@ +function createDraftFilingSession({list}) { + let orderedIds = []; + let currentId = ''; + + function visibleIds() { + const available = new Set(list().map(item => item.id)); + orderedIds = orderedIds.filter(id => available.has(id)); + for (const item of list()) { + if (!orderedIds.includes(item.id)) orderedIds.push(item.id); + } + return orderedIds; + } + + function state() { + const ids = visibleIds(); + if (!ids.length) { + currentId = ''; + return null; + } + if (!ids.includes(currentId)) currentId = ids[0]; + const index = ids.indexOf(currentId); + return {id:currentId, position:index + 1, total:ids.length, remaining:ids.length - index - 1}; + } + + function start(id) { + orderedIds = list().map(item => item.id); + if (!orderedIds.includes(id)) throw new Error('This capture is no longer available.'); + currentId = id; + return state(); + } + + function move(offset) { + const ids = visibleIds(); + if (!ids.length) return state(); + const index = Math.max(0, ids.indexOf(currentId)); + currentId = ids[(index + offset + ids.length) % ids.length]; + return state(); + } + + function removeCurrentAndNext(removedId) { + const previousIds = orderedIds.slice(); + const removedIndex = Math.max(0, previousIds.indexOf(removedId)); + orderedIds = previousIds.filter(id => id !== removedId); + const ids = visibleIds(); + if (!ids.length) return state(); + currentId = ids[Math.min(removedIndex, ids.length - 1)]; + return state(); + } + + const session = {start, current:state, next:()=>move(1), previous:()=>move(-1), removeCurrentAndNext}; + session.attach = (qs, dependencies) => { + const openCapture = async captureId => { + const resumed = await dependencies.captures.resume(captureId, dependencies.getLogin()); + dependencies.issueCapture.saveDraft(resumed); + dependencies.setResumedId(captureId); + await dependencies.openSheet(); + dependencies.attachment[resumed.attachment ? 'restore' : 'clear'](resumed.attachment); + dependencies.setFilingMode(true); + session.render(); + }; + return Object.assign(session, { + nextCapture() { const state = session.current(); return state ? openCapture(state.id) : null; }, + render() { + const state = session.current(); + qs('#draft-filing-session').hidden = !state; + if (!state) return; + qs('#draft-filing-progress').textContent = `Draft ${state.position} of ${state.total}`; + qs('#draft-filing-remaining').textContent = state.remaining ? `${state.remaining} remaining` : 'Last Draft'; + qs('#previous-draft').disabled = qs('#skip-draft').disabled = state.total < 2; + qs('#submit-new-issue').textContent = state.remaining ? 'File & next' : 'File final Draft'; + }, + bind() { + [['#previous-draft','previous'],['#skip-draft','next']].forEach(([selector, method]) => + qs(selector).addEventListener('click', async () => { + const state = session[method](); + if (state) await openCapture(state.id); + }) + ); + }, + async advance(completedId) { + const state = session.removeCurrentAndNext(completedId); + if (state) { await openCapture(state.id); return true; } + qs('#draft-filing-session').hidden = true; + qs('#submit-new-issue').textContent = 'Create & assign to me'; + return false; + }, + }); + }; + return session; +} + +if (typeof module !== 'undefined' && module.exports) module.exports = createDraftFilingSession; diff --git a/frontend/index.html b/frontend/index.html index 472ab28..5dc775d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -550,6 +550,14 @@