diff --git a/frontend/dashboard.css b/frontend/dashboard.css index e79f7b1..298350d 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -646,6 +646,19 @@ textarea { resize: vertical; min-height: 120px; } .issue-evidence-editor button[aria-pressed="true"] { border-color:#60a5fa; background:#173a64; color:#fff; } .issue-evidence-editor-apply { width:100%; } @media(max-width:320px) { .issue-evidence-editor-panel { padding:12px; } .issue-evidence-editor-tools { grid-template-columns:repeat(2,minmax(0,1fr)); } } +.issue-filing-review { position:fixed; inset:0; z-index:72; display:grid; place-items:end center; overflow-x:hidden; background:rgba(2,6,15,.92); } +.issue-filing-review[hidden] { display:none; } +.issue-filing-review-panel { box-sizing:border-box; width:min(620px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; display:grid; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #31577f; border-radius:18px 18px 0 0; background:#0b1526; } +.issue-filing-review-panel h2, .issue-filing-review-panel h3, .issue-filing-review-panel p { margin:.2rem 0; } +.issue-filing-review-summary { display:grid; gap:10px; margin:0; min-width:0; } +.issue-filing-review-summary > div { display:grid; gap:4px; padding:10px; border:1px solid #2a496e; border-radius:10px; min-width:0; } +.issue-filing-review-summary dt { color:#94a3b8; font-size:12px; font-weight:700; text-transform:uppercase; } +.issue-filing-review-summary dd { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; } +.issue-filing-review ol { display:grid; gap:8px; margin:0; padding-left:24px; overflow-wrap:anywhere; } +.issue-filing-review ol:empty::after { content:'No screenshots attached.'; display:list-item; color:#94a3b8; } +.issue-filing-review-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0 calc(10px + env(safe-area-inset-bottom)); background:#0b1526; } +.issue-filing-review-actions button { min-height:44px; width:100%; } +@media(max-width:320px) { .issue-filing-review-panel { padding:12px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); } } .create-issue-repository-more { min-height:44px; width:100%; } .create-issue-repository-picker { min-width:0; display:grid; gap:8px; } .create-issue-repository-picker input { min-width:0; min-height:44px; width:100%; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:#e5e7eb; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 53a1fcc..37f0d23 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -187,6 +187,7 @@ let pullReviewState = null; let creatingIssue = false; let createAndStartRequested = false; + let pendingIssueFilingIntent = 'create-and-assign'; let findingWork = false; let availablePagination = { page: 1, total: 0, has_more: false }; let progress = null; @@ -551,6 +552,20 @@ backgroundSync: backgroundIssueSync, getOwnerLogin: () => confirmedOwnerLogin, }); + const filingReview = createIssueFilingReview({ + sheet: qs('#issue-filing-review'), + confirmButton: qs('#confirm-issue-filing'), + backButton: qs('#back-to-issue-edit'), + evidenceList: qs('#issue-filing-review-evidence'), + repository: qs('#issue-filing-review-repository'), + intent: qs('#issue-filing-review-intent'), + title: qs('#issue-filing-review-title'), + body: qs('#issue-filing-review-body'), + metadata: qs('#issue-filing-review-metadata'), + status: qs('#issue-filing-review-status'), + document, + onConfirm: admitReviewedIssue, + }); const authoredOutbox = createAuthoredOutbox({ storage: localStorage, fetchJson: fetchReviewJson, coordinator: outboxCoordinator, backgroundSync: backgroundIssueSync, @@ -3477,6 +3492,11 @@ .map(input => Number(input.value)).filter(Number.isInteger); } + function selectedIssueLabelNames() { + return Array.from(document.querySelectorAll('input[name="create-issue-label"]:checked')) + .map(input => input.closest('label')?.querySelector('span')?.textContent?.trim()).filter(Boolean); + } + function renderAvailableIssues(items) { const list = qs('#find-work-list'); const selection = findWorkController.selection(); @@ -5083,26 +5103,12 @@ qs('#create-issue-anyway').hidden = true; qs('#create-issue-form').requestSubmit(); }); - qs('#create-issue-form').addEventListener('submit', async event => { - event.preventDefault(); - if (event.submitter) createAndStartRequested = event.submitter?.id === 'create-and-start-issue'; - const followUpNextRequested = event.submitter?.id === 'create-follow-up-next'; - const captureDraft = currentIssueCaptureDraft(); - if (!captureDraft.repository || !captureDraft.title) { - qs('#create-issue-status').textContent = 'Choose a repository and add a title.'; - qs('#create-issue-title').focus(); - return; - } - if (issueCapture.needsDuplicateAcknowledgement(captureDraft)) { - qs('#create-issue-status').textContent = 'Review the possible existing issues, or choose Create anyway.'; - qs('#create-issue-anyway').hidden = false; - qs('#create-issue-anyway').focus(); - return; - } + async function admitReviewedIssue(review) { + const durableDraft = review.draft; + const followUpNextRequested = review.intent === 'follow-up-and-next'; + createAndStartRequested = review.intent === 'create-and-start'; if (createAndStartRequested && !createAndStart.available()) { - qs('#create-issue-status').textContent = 'Today is full. Remove an item before creating and starting another.'; - qs('#create-and-start-issue').focus(); - return; + throw new Error('Today is full now. Go back and remove an item before creating and starting another.'); } const button = qs('#submit-new-issue'); const startButton = qs('#create-and-start-issue'); @@ -5113,13 +5119,6 @@ qs('#create-issue-status').textContent = createAndStartRequested ? 'Creating issue and adding it to Today…' : 'Saving for background delivery…'; try { - const evidence = await createIssueAttachmentController.serialize(); - const durableDraft = { - ...captureDraft, - ...(Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence}), - ...(rUC ? { sourceCaptureId: rUC } : {}), - ...(createAndStartRequested ? { completionIntent: 'create-and-start' } : {}), - }; const admission = followUpNextRequested ? (await updateFollowUp.complete({ admit: () => editingOutboxId ? issueOutbox.updateDurably(editingOutboxId, durableDraft) : issueOutbox.enqueueDurably(durableDraft), @@ -5128,6 +5127,7 @@ })).admission : (editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, durableDraft) : await issueOutbox.enqueueDurably(durableDraft)); const queued = admission.item; + pendingIssueFilingIntent = 'create-and-assign'; const fS = dFS.current(); if (rUC && ((!durableDraft.attachment && !durableDraft.attachments) || admission.background)) { await unfiledCaptures.completeResume(rUC); @@ -5141,8 +5141,6 @@ editingOutboxId = queued.id; refreshMyWorkView(); qs('#create-issue-status').textContent = 'Saved for next launch; background delivery unavailable.'; - button.disabled = false; - startButton.disabled = !createAndStart.available(); return; } editingOutboxId = null; @@ -5160,9 +5158,54 @@ createAndStartRequested = false; } catch (error) { qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.'; + throw error; + } finally { button.disabled = false; startButton.disabled = !createAndStart.available(); followUpButton.disabled = false; + } + } + + qs('#create-issue-form').addEventListener('submit', async event => { + event.preventDefault(); + if (event.submitter) pendingIssueFilingIntent = + event.submitter.id === 'create-and-start-issue' ? 'create-and-start' : + (event.submitter.id === 'create-follow-up-next' ? 'follow-up-and-next' : 'create-and-assign'); + const intent = pendingIssueFilingIntent; + createAndStartRequested = intent === 'create-and-start'; + const captureDraft = currentIssueCaptureDraft(); + if (!captureDraft.repository || !captureDraft.title) { + qs('#create-issue-status').textContent = 'Choose a repository and add a title.'; + qs('#create-issue-title').focus(); + return; + } + if (issueCapture.needsDuplicateAcknowledgement(captureDraft)) { + qs('#create-issue-status').textContent = 'Review the possible existing issues, or choose Create anyway.'; + qs('#create-issue-anyway').hidden = false; + qs('#create-issue-anyway').focus(); + return; + } + if (createAndStartRequested && !createAndStart.available()) { + qs('#create-issue-status').textContent = 'Today is full. Remove an item before creating and starting another.'; + qs('#create-and-start-issue').focus(); + return; + } + qs('#create-issue-status').textContent = 'Preparing complete filing review…'; + try { + const evidence = await createIssueAttachmentController.serialize(); + const milestoneOption = qs('#create-issue-milestone').selectedOptions?.[0]; + const durableDraft = { + ...captureDraft, + labels: selectedIssueLabelNames(), + milestoneTitle: captureDraft.milestoneId ? milestoneOption?.textContent?.trim() : '', + ...(Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence}), + ...(rUC ? { sourceCaptureId: rUC } : {}), + ...(createAndStartRequested ? { completionIntent: 'create-and-start' } : {}), + }; + filingReview.open({draft: durableDraft, intent}, event.submitter || qs('#submit-new-issue')); + qs('#create-issue-status').textContent = 'Review the complete payload, then confirm filing.'; + } catch (error) { + qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.'; qs('#create-issue-title').focus(); } }); diff --git a/frontend/index.html b/frontend/index.html index 10ac595..04993b7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -849,6 +849,33 @@ + +