feat: review mobile issue payload before filing (Closes #833)
CI / lint (pull_request) Successful in 1m42s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 15:36:54 +00:00
1 parent e125019f57
commit 59a2e4f616
11 files changed
+362 -34

No files matched your search

+13
View File
@@ -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; }
+71 -28
View File
@@ -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();
}
});
+28
View File
@@ -849,6 +849,33 @@
</section>
</div>
<section class="issue-filing-review" id="issue-filing-review" role="dialog" aria-modal="true" aria-labelledby="issue-filing-review-heading" hidden>
<div class="issue-filing-review-panel">
<header>
<div>
<h2 id="issue-filing-review-heading">Review issue before filing</h2>
<p class="small">Confirm the complete payload that will be saved for delivery.</p>
</div>
</header>
<dl class="issue-filing-review-summary">
<div><dt>Repository</dt><dd id="issue-filing-review-repository"></dd></div>
<div><dt>Action</dt><dd id="issue-filing-review-intent"></dd></div>
<div><dt>Title</dt><dd id="issue-filing-review-title"></dd></div>
<div><dt>Note</dt><dd id="issue-filing-review-body"></dd></div>
<div><dt>Planning</dt><dd id="issue-filing-review-metadata"></dd></div>
</dl>
<section aria-labelledby="issue-filing-review-evidence-heading">
<h3 id="issue-filing-review-evidence-heading">Evidence in filing order</h3>
<ol id="issue-filing-review-evidence"></ol>
</section>
<div class="issue-filing-review-actions">
<button id="back-to-issue-edit" type="button">Back to edit</button>
<button id="confirm-issue-filing" type="button">Confirm filing</button>
</div>
<div class="small" id="issue-filing-review-status" aria-live="assertive"></div>
</div>
</section>
<div class="draft-capacity-sheet" id="draft-capacity-sheet" role="dialog" aria-modal="true" aria-labelledby="draft-capacity-heading" hidden>
<section class="draft-capacity-panel">
<h3 id="draft-capacity-heading">Drafts full — nothing was deleted.</h3>
@@ -1200,6 +1227,7 @@
<script src="static/issue-evidence-review.js"></script>
<script src="static/issue-evidence-editor.js"></script>
<script src="static/issue-attachment.js"></script>
<script src="static/issue-filing-review.js"></script>
<script src="static/issue-sheet.js"></script>
<script src="static/create-issue-sheet.js"></script>
<script src="static/create-and-start.js"></script>
+93
View File
@@ -0,0 +1,93 @@
(function(root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
else root.createIssueFilingReview = api;
})(typeof self !== 'undefined' ? self : this, function() {
'use strict';
const INTENT_LABELS = {
'create-and-assign': 'Create & assign to me',
'create-and-start': 'Create & start',
'follow-up-and-next': 'Create follow-up & next',
};
function clone(value) {
if (typeof structuredClone === 'function') return structuredClone(value);
return JSON.parse(JSON.stringify(value));
}
function create(options) {
let reviewed = null;
let trigger = null;
let busy = false;
function close() {
options.sheet.hidden = true;
const previousTrigger = trigger;
trigger = null;
reviewed = null;
busy = false;
options.confirmButton.disabled = false;
previousTrigger?.focus();
}
function render(payload) {
const draft = payload.draft;
options.repository.textContent = draft.repository || 'No repository';
options.intent.textContent = INTENT_LABELS[payload.intent] || payload.intent;
options.title.textContent = draft.title;
options.body.textContent = draft.body || 'No note provided.';
const labels = (draft.labels || []).map(label => typeof label === 'string' ? label : label.name);
const milestone = draft.milestone?.title || draft.milestoneTitle || 'No milestone';
const dueDate = draft.dueDate || draft.due_date || 'No due date';
options.metadata.textContent = [
labels.length ? 'Labels: ' + labels.join(', ') : 'No labels',
'Milestone: ' + milestone,
'Due: ' + dueDate,
'Assigned to you',
].join(' · ');
const attachments = (draft.attachments || (draft.attachment ? [draft.attachment] : [])).filter(Boolean);
options.evidenceList.replaceChildren(...attachments.map((attachment, index) => {
const item = options.document.createElement('li');
item.textContent = (index + 1) + '. ' + (attachment.filename || 'Screenshot') +
(attachment.note ? ' — ' + attachment.note : '');
return item;
}));
}
function open(payload, opener) {
reviewed = clone(payload);
trigger = opener || null;
busy = false;
options.confirmButton.disabled = false;
render(reviewed);
options.sheet.hidden = false;
options.backButton.focus();
}
options.backButton.addEventListener('click', close);
options.confirmButton.addEventListener('click', async () => {
if (!reviewed || busy) return;
busy = true;
options.confirmButton.disabled = true;
try {
await options.onConfirm(reviewed);
close();
} catch (error) {
busy = false;
options.confirmButton.disabled = false;
if (options.status) options.status.textContent = error.message;
options.confirmButton.focus();
}
});
options.document.addEventListener('keydown', event => {
if (!options.sheet.hidden && event.key === 'Escape' && !busy) {
event.preventDefault();
close();
}
});
return { close, open };
}
return create;
});
+1
View File
@@ -67,6 +67,7 @@ const SHELL = [
BASE + 'static/issue-evidence-review.js',
BASE + 'static/issue-evidence-editor.js',
BASE + 'static/issue-attachment.js',
BASE + 'static/issue-filing-review.js',
BASE + 'static/issue-sheet.js',
BASE + 'static/create-issue-sheet.js',
BASE + 'static/create-and-start.js',