feat: review mobile issue payload before filing (Closes #833)
This commit is contained in:
parent
e125019f57
commit
59a2e4f616
|
|
@ -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 button[aria-pressed="true"] { border-color:#60a5fa; background:#173a64; color:#fff; }
|
||||||
.issue-evidence-editor-apply { width:100%; }
|
.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)); } }
|
@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-more { min-height:44px; width:100%; }
|
||||||
.create-issue-repository-picker { min-width:0; display:grid; gap:8px; }
|
.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; }
|
.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; }
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@
|
||||||
let pullReviewState = null;
|
let pullReviewState = null;
|
||||||
let creatingIssue = false;
|
let creatingIssue = false;
|
||||||
let createAndStartRequested = false;
|
let createAndStartRequested = false;
|
||||||
|
let pendingIssueFilingIntent = 'create-and-assign';
|
||||||
let findingWork = false;
|
let findingWork = false;
|
||||||
let availablePagination = { page: 1, total: 0, has_more: false };
|
let availablePagination = { page: 1, total: 0, has_more: false };
|
||||||
let progress = null;
|
let progress = null;
|
||||||
|
|
@ -551,6 +552,20 @@
|
||||||
backgroundSync: backgroundIssueSync,
|
backgroundSync: backgroundIssueSync,
|
||||||
getOwnerLogin: () => confirmedOwnerLogin,
|
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({
|
const authoredOutbox = createAuthoredOutbox({
|
||||||
storage: localStorage, fetchJson: fetchReviewJson, coordinator: outboxCoordinator,
|
storage: localStorage, fetchJson: fetchReviewJson, coordinator: outboxCoordinator,
|
||||||
backgroundSync: backgroundIssueSync,
|
backgroundSync: backgroundIssueSync,
|
||||||
|
|
@ -3477,6 +3492,11 @@
|
||||||
.map(input => Number(input.value)).filter(Number.isInteger);
|
.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) {
|
function renderAvailableIssues(items) {
|
||||||
const list = qs('#find-work-list');
|
const list = qs('#find-work-list');
|
||||||
const selection = findWorkController.selection();
|
const selection = findWorkController.selection();
|
||||||
|
|
@ -5083,26 +5103,12 @@
|
||||||
qs('#create-issue-anyway').hidden = true;
|
qs('#create-issue-anyway').hidden = true;
|
||||||
qs('#create-issue-form').requestSubmit();
|
qs('#create-issue-form').requestSubmit();
|
||||||
});
|
});
|
||||||
qs('#create-issue-form').addEventListener('submit', async event => {
|
async function admitReviewedIssue(review) {
|
||||||
event.preventDefault();
|
const durableDraft = review.draft;
|
||||||
if (event.submitter) createAndStartRequested = event.submitter?.id === 'create-and-start-issue';
|
const followUpNextRequested = review.intent === 'follow-up-and-next';
|
||||||
const followUpNextRequested = event.submitter?.id === 'create-follow-up-next';
|
createAndStartRequested = review.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()) {
|
if (createAndStartRequested && !createAndStart.available()) {
|
||||||
qs('#create-issue-status').textContent = 'Today is full. Remove an item before creating and starting another.';
|
throw new Error('Today is full now. Go back and remove an item before creating and starting another.');
|
||||||
qs('#create-and-start-issue').focus();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const button = qs('#submit-new-issue');
|
const button = qs('#submit-new-issue');
|
||||||
const startButton = qs('#create-and-start-issue');
|
const startButton = qs('#create-and-start-issue');
|
||||||
|
|
@ -5113,13 +5119,6 @@
|
||||||
qs('#create-issue-status').textContent = createAndStartRequested ?
|
qs('#create-issue-status').textContent = createAndStartRequested ?
|
||||||
'Creating issue and adding it to Today…' : 'Saving for background delivery…';
|
'Creating issue and adding it to Today…' : 'Saving for background delivery…';
|
||||||
try {
|
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({
|
const admission = followUpNextRequested ? (await updateFollowUp.complete({
|
||||||
admit: () => editingOutboxId ? issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
admit: () => editingOutboxId ? issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
||||||
issueOutbox.enqueueDurably(durableDraft),
|
issueOutbox.enqueueDurably(durableDraft),
|
||||||
|
|
@ -5128,6 +5127,7 @@
|
||||||
})).admission : (editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
})).admission : (editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
||||||
await issueOutbox.enqueueDurably(durableDraft));
|
await issueOutbox.enqueueDurably(durableDraft));
|
||||||
const queued = admission.item;
|
const queued = admission.item;
|
||||||
|
pendingIssueFilingIntent = 'create-and-assign';
|
||||||
const fS = dFS.current();
|
const fS = dFS.current();
|
||||||
if (rUC && ((!durableDraft.attachment && !durableDraft.attachments) || admission.background)) {
|
if (rUC && ((!durableDraft.attachment && !durableDraft.attachments) || admission.background)) {
|
||||||
await unfiledCaptures.completeResume(rUC);
|
await unfiledCaptures.completeResume(rUC);
|
||||||
|
|
@ -5141,8 +5141,6 @@
|
||||||
editingOutboxId = queued.id;
|
editingOutboxId = queued.id;
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
qs('#create-issue-status').textContent = 'Saved for next launch; background delivery unavailable.';
|
qs('#create-issue-status').textContent = 'Saved for next launch; background delivery unavailable.';
|
||||||
button.disabled = false;
|
|
||||||
startButton.disabled = !createAndStart.available();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editingOutboxId = null;
|
editingOutboxId = null;
|
||||||
|
|
@ -5160,9 +5158,54 @@
|
||||||
createAndStartRequested = false;
|
createAndStartRequested = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.';
|
qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
startButton.disabled = !createAndStart.available();
|
startButton.disabled = !createAndStart.available();
|
||||||
followUpButton.disabled = false;
|
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();
|
qs('#create-issue-title').focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -849,6 +849,33 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
<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">
|
<section class="draft-capacity-panel">
|
||||||
<h3 id="draft-capacity-heading">Drafts full — nothing was deleted.</h3>
|
<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-review.js"></script>
|
||||||
<script src="static/issue-evidence-editor.js"></script>
|
<script src="static/issue-evidence-editor.js"></script>
|
||||||
<script src="static/issue-attachment.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/issue-sheet.js"></script>
|
||||||
<script src="static/create-issue-sheet.js"></script>
|
<script src="static/create-issue-sheet.js"></script>
|
||||||
<script src="static/create-and-start.js"></script>
|
<script src="static/create-and-start.js"></script>
|
||||||
|
|
|
||||||
93
frontend/issue-filing-review.js
Normal file
93
frontend/issue-filing-review.js
Normal 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;
|
||||||
|
});
|
||||||
|
|
@ -67,6 +67,7 @@ const SHELL = [
|
||||||
BASE + 'static/issue-evidence-review.js',
|
BASE + 'static/issue-evidence-review.js',
|
||||||
BASE + 'static/issue-evidence-editor.js',
|
BASE + 'static/issue-evidence-editor.js',
|
||||||
BASE + 'static/issue-attachment.js',
|
BASE + 'static/issue-attachment.js',
|
||||||
|
BASE + 'static/issue-filing-review.js',
|
||||||
BASE + 'static/issue-sheet.js',
|
BASE + 'static/issue-sheet.js',
|
||||||
BASE + 'static/create-issue-sheet.js',
|
BASE + 'static/create-issue-sheet.js',
|
||||||
BASE + 'static/create-and-start.js',
|
BASE + 'static/create-and-start.js',
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ FEATURE_SOURCES = {
|
||||||
"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/pick-work.js", "static/batch-find-work.js",
|
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||||
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
|
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
|
||||||
"static/issue-attachment.js",
|
"static/issue-attachment.js", "static/issue-filing-review.js",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
CACHE_DECLARATION = re.compile(
|
CACHE_DECLARATION = re.compile(
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ def test_mobile_capture_wires_distinct_create_and_start_intent_into_the_offline_
|
||||||
assert '<button id="create-and-start-issue" type="submit">Create & start</button>' in html
|
assert '<button id="create-and-start-issue" type="submit">Create & start</button>' in html
|
||||||
assert '<script src="static/create-and-start.js"></script>' in html
|
assert '<script src="static/create-and-start.js"></script>' in html
|
||||||
assert "const createAndStart = createCreateAndStart({" in dashboard
|
assert "const createAndStart = createCreateAndStart({" in dashboard
|
||||||
assert "event.submitter?.id === 'create-and-start-issue'" in dashboard
|
assert "event.submitter.id === 'create-and-start-issue'" in dashboard
|
||||||
|
assert "review.intent === 'create-and-start'" in dashboard
|
||||||
assert "createAndStart.complete(created)" in dashboard
|
assert "createAndStart.complete(created)" in dashboard
|
||||||
assert ".create-issue-actions" in css and "grid-template-columns" in css
|
assert ".create-issue-actions" in css and "grid-template-columns" in css
|
||||||
assert "BASE + 'static/create-and-start.js'" in worker
|
assert "BASE + 'static/create-and-start.js'" in worker
|
||||||
|
|
|
||||||
144
tests/test_issue_filing_review.py
Normal file
144
tests/test_issue_filing_review.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE = Path(__file__).parents[1] / "frontend" / "issue-filing-review.js"
|
||||||
|
|
||||||
|
|
||||||
|
def run_node(script: str) -> dict:
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", "-e", script], capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_requires_one_confirmation_and_preserves_the_reviewed_payload():
|
||||||
|
script = f"""
|
||||||
|
const createReview = require({json.dumps(str(MODULE))});
|
||||||
|
function target() {{
|
||||||
|
const listeners = {{}};
|
||||||
|
return {{
|
||||||
|
disabled:false, hidden:true, textContent:'', children:[],
|
||||||
|
addEventListener:(name, fn)=>listeners[name]=fn,
|
||||||
|
dispatch:(name, event={{}})=>listeners[name]?.({{preventDefault(){{}}, ...event}}),
|
||||||
|
focus(){{this.focused=true;}}, replaceChildren(...items){{this.children=items;}},
|
||||||
|
setAttribute(){{}}, appendChild(item){{this.children.push(item);}},
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
const sheet=target(), confirm=target(), back=target(), trigger=target(), evidence=target();
|
||||||
|
const documentRef={{
|
||||||
|
createElement:tag=>({{tag, textContent:'', children:[], appendChild(item){{this.children.push(item);}}}}),
|
||||||
|
addEventListener:()=>{{}},
|
||||||
|
}};
|
||||||
|
const admitted=[];
|
||||||
|
const review=createReview({{
|
||||||
|
sheet, confirmButton:confirm, backButton:back, evidenceList:evidence,
|
||||||
|
repository:target(), intent:target(), title:target(), body:target(), metadata:target(),
|
||||||
|
document:documentRef,
|
||||||
|
onConfirm:async payload=>{{ admitted.push(payload); return {{ok:true}}; }},
|
||||||
|
}});
|
||||||
|
const payload={{
|
||||||
|
draft:{{repository:'stackchain/dashboard',title:'Ship it',body:'Full note',labels:['P1'],
|
||||||
|
milestone:{{title:'Sprint'}},dueDate:'2026-08-20',attachments:[
|
||||||
|
{{filename:'first.png',note:'Before'}},{{filename:'second.png',note:'After'}}
|
||||||
|
]}},
|
||||||
|
intent:'create-and-start',
|
||||||
|
}};
|
||||||
|
review.open(payload, trigger);
|
||||||
|
payload.draft.title='Mutated outside';
|
||||||
|
confirm.dispatch('click');
|
||||||
|
confirm.dispatch('click');
|
||||||
|
setImmediate(()=>process.stdout.write(JSON.stringify({{
|
||||||
|
admissions:admitted.length,
|
||||||
|
reviewedTitle:admitted[0].draft.title,
|
||||||
|
evidence:evidence.children.map(item=>item.textContent),
|
||||||
|
hidden:sheet.hidden,
|
||||||
|
triggerFocused:trigger.focused || false,
|
||||||
|
}})));
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = run_node(script)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"admissions": 1,
|
||||||
|
"reviewedTitle": "Ship it",
|
||||||
|
"evidence": ["1. first.png — Before", "2. second.png — After"],
|
||||||
|
"hidden": True,
|
||||||
|
"triggerFocused": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_escape_returns_to_the_unchanged_issue_form():
|
||||||
|
script = f"""
|
||||||
|
const createReview = require({json.dumps(str(MODULE))});
|
||||||
|
function target() {{
|
||||||
|
const listeners={{}};
|
||||||
|
return {{hidden:true,disabled:false,textContent:'',children:[],
|
||||||
|
addEventListener:(name,fn)=>listeners[name]=fn,
|
||||||
|
replaceChildren(...items){{this.children=items;}},
|
||||||
|
focus(){{this.focused=true;}}, dispatch:(name,event)=>listeners[name]?.(event)}};
|
||||||
|
}}
|
||||||
|
const listeners={{}}, documentRef={{
|
||||||
|
createElement:()=>({{textContent:''}}),
|
||||||
|
addEventListener:(name,fn)=>listeners[name]=fn,
|
||||||
|
dispatch:(name,event)=>listeners[name]?.(event),
|
||||||
|
}};
|
||||||
|
const sheet=target(), trigger=target();
|
||||||
|
const review=createReview({{
|
||||||
|
sheet,confirmButton:target(),backButton:target(),evidenceList:target(),
|
||||||
|
repository:target(),intent:target(),title:target(),body:target(),metadata:target(),
|
||||||
|
document:documentRef,onConfirm:async()=>{{}},
|
||||||
|
}});
|
||||||
|
const payload={{draft:{{repository:'o/r',title:'Original',body:'Note'}},intent:'create-and-assign'}};
|
||||||
|
review.open(payload,trigger);
|
||||||
|
documentRef.dispatch('keydown',{{key:'Escape',preventDefault(){{this.prevented=true;}}}});
|
||||||
|
process.stdout.write(JSON.stringify({{hidden:sheet.hidden,focused:trigger.focused||false,title:payload.draft.title}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {"hidden": True, "focused": True, "title": "Original"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_actions_review_the_complete_payload_before_durable_admission():
|
||||||
|
root = Path(__file__).parents[1]
|
||||||
|
html = (root / "frontend" / "index.html").read_text()
|
||||||
|
dashboard = (root / "frontend" / "dashboard.js").read_text()
|
||||||
|
css = (root / "frontend" / "dashboard.css").read_text()
|
||||||
|
bundle = (root / "src" / "frontend_bundle.py").read_text()
|
||||||
|
|
||||||
|
assert 'id="issue-filing-review" role="dialog" aria-modal="true"' in html
|
||||||
|
assert 'id="issue-filing-review-repository"' in html
|
||||||
|
assert 'id="issue-filing-review-intent"' in html
|
||||||
|
assert 'id="issue-filing-review-title"' in html
|
||||||
|
assert 'id="issue-filing-review-body"' in html
|
||||||
|
assert 'id="issue-filing-review-evidence"' in html
|
||||||
|
assert 'id="back-to-issue-edit"' in html
|
||||||
|
assert 'id="confirm-issue-filing"' in html
|
||||||
|
assert '<script src="static/issue-filing-review.js"></script>' in html
|
||||||
|
|
||||||
|
submit_handler = dashboard.split(
|
||||||
|
"qs('#create-issue-form').addEventListener('submit'", 1
|
||||||
|
)[1].split("qs('#close-issue-sheet').addEventListener", 1)[0]
|
||||||
|
assert "filingReview.open" in submit_handler
|
||||||
|
assert "issueOutbox.enqueueDurably" not in submit_handler
|
||||||
|
assert "async function admitReviewedIssue" in dashboard
|
||||||
|
assert "issueOutbox.enqueueDurably" in dashboard.split(
|
||||||
|
"async function admitReviewedIssue", 1
|
||||||
|
)[1].split("qs('#create-issue-form').addEventListener", 1)[0]
|
||||||
|
|
||||||
|
assert ".issue-filing-review" in css
|
||||||
|
assert "max-height:100dvh" in css
|
||||||
|
assert "overflow-x:hidden" in css
|
||||||
|
assert "env(safe-area-inset-bottom)" in css
|
||||||
|
assert '"static/issue-filing-review.js"' in bundle
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_acknowledgement_preserves_the_selected_filing_intent():
|
||||||
|
dashboard = (Path(__file__).parents[1] / "frontend" / "dashboard.js").read_text()
|
||||||
|
|
||||||
|
assert "let pendingIssueFilingIntent = 'create-and-assign';" in dashboard
|
||||||
|
assert "if (event.submitter) pendingIssueFilingIntent" in dashboard
|
||||||
|
assert "const intent = pendingIssueFilingIntent;" in dashboard
|
||||||
|
assert "pendingIssueFilingIntent = 'create-and-assign';" in dashboard.split(
|
||||||
|
"async function admitReviewedIssue", 1
|
||||||
|
)[1].split("qs('#create-issue-form').addEventListener", 1)[0]
|
||||||
|
|
@ -6333,9 +6333,12 @@ async def test_mobile_issue_capture_warns_before_queuing_a_possible_duplicate():
|
||||||
assert "issueCapture.findDuplicates(draft)" in html
|
assert "issueCapture.findDuplicates(draft)" in html
|
||||||
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in html
|
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in html
|
||||||
assert "issueCapture.acknowledgeDuplicates(captureDraft)" in html
|
assert "issueCapture.acknowledgeDuplicates(captureDraft)" in html
|
||||||
assert html.index("issueCapture.needsDuplicateAcknowledgement(captureDraft)") < html.index(
|
submit_handler = html.split("qs('#create-issue-form').addEventListener('submit'", 1)[1].split(
|
||||||
"issueOutbox.enqueueDurably(durableDraft)"
|
"qs('#close-issue-sheet').addEventListener", 1
|
||||||
)
|
)[0]
|
||||||
|
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in submit_handler
|
||||||
|
assert "filingReview.open" in submit_handler
|
||||||
|
assert "issueOutbox.enqueueDurably" not in submit_handler
|
||||||
assert ".create-issue-duplicate-card" in html
|
assert ".create-issue-duplicate-card" in html
|
||||||
assert ".create-issue-duplicate-card a" in html and "min-height:44px" in html
|
assert ".create-issue-duplicate-card a" in html and "min-height:44px" in html
|
||||||
assert "overflow-wrap:anywhere" in html
|
assert "overflow-wrap:anywhere" in html
|
||||||
|
|
|
||||||
|
|
@ -840,6 +840,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/issue-evidence-review.js",
|
"/dashboard/static/issue-evidence-review.js",
|
||||||
"/dashboard/static/issue-evidence-editor.js",
|
"/dashboard/static/issue-evidence-editor.js",
|
||||||
"/dashboard/static/issue-attachment.js",
|
"/dashboard/static/issue-attachment.js",
|
||||||
|
"/dashboard/static/issue-filing-review.js",
|
||||||
"/dashboard/static/issue-sheet.js",
|
"/dashboard/static/issue-sheet.js",
|
||||||
"/dashboard/static/create-issue-sheet.js",
|
"/dashboard/static/create-issue-sheet.js",
|
||||||
"/dashboard/static/create-and-start.js",
|
"/dashboard/static/create-and-start.js",
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,8 @@ async def test_follow_up_capture_offers_durable_create_and_next_flow():
|
||||||
assert 'id="create-follow-up-next"' in html
|
assert 'id="create-follow-up-next"' in html
|
||||||
assert '>Create follow-up & next</button>' in html
|
assert '>Create follow-up & next</button>' in html
|
||||||
assert "updateFollowUp.stageSource(source.item)" in html
|
assert "updateFollowUp.stageSource(source.item)" in html
|
||||||
assert "event.submitter?.id === 'create-follow-up-next'" in html
|
assert "event.submitter.id === 'create-follow-up-next'" in html
|
||||||
|
assert "review.intent === 'follow-up-and-next'" in html
|
||||||
assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html
|
assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html
|
||||||
assert "advance: source => notificationReader.acceptReadAndNext(lastMyWork, source)" in html
|
assert "advance: source => notificationReader.acceptReadAndNext(lastMyWork, source)" in html
|
||||||
assert ".create-issue-actions button { min-height:44px;" in html
|
assert ".create-issue-actions button { min-height:44px;" in html
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user