File related issues from confirmed filing receipts #869
|
|
@ -19,6 +19,30 @@ function normalizeSharedContent(value = {}) {
|
||||||
return { title, body };
|
return { title, body };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRelatedDraft(value = {}, template = null) {
|
||||||
|
const repository = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(String(value.repository || ''))
|
||||||
|
? String(value.repository) : '';
|
||||||
|
const labelIds = Array.from(new Set((Array.isArray(value.labelIds) ? value.labelIds : [])
|
||||||
|
.filter(id => Number.isInteger(id) && id > 0))).slice(0, 20);
|
||||||
|
const draft = {repository, title:'', body:'', labelIds};
|
||||||
|
const milestoneId = Number(value.milestoneId);
|
||||||
|
if (Number.isInteger(milestoneId) && milestoneId > 0) draft.milestoneId = milestoneId;
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(String(value.dueDate || ''))) draft.dueDate = String(value.dueDate);
|
||||||
|
if (value.unassigned === true) draft.unassigned = true;
|
||||||
|
else if (/^[A-Za-z0-9_.-]+$/.test(String(value.assignee || ''))) {
|
||||||
|
draft.assignee = String(value.assignee);
|
||||||
|
draft.assigneeName = String(value.assigneeName || value.assignee).replace(/\s+/g, ' ').trim().slice(0, 255);
|
||||||
|
}
|
||||||
|
const templateId = String(value.templateId || '').slice(0, 80);
|
||||||
|
if (templateId && template && String(template.id || '') === templateId) {
|
||||||
|
draft.templateId = templateId;
|
||||||
|
draft.templateName = String(value.templateName || template.name || 'Issue template').trim().slice(0, 80);
|
||||||
|
draft.capturedBody = '';
|
||||||
|
draft.body = String(template.body || '').trim().slice(0, 9000);
|
||||||
|
}
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
function createIssueOwnerPicker(issueCapture, documentRef, onChange) {
|
function createIssueOwnerPicker(issueCapture, documentRef, onChange) {
|
||||||
const NO_OWNER = '__unassigned__';
|
const NO_OWNER = '__unassigned__';
|
||||||
const select = documentRef.querySelector('#create-issue-assignee');
|
const select = documentRef.querySelector('#create-issue-assignee');
|
||||||
|
|
@ -460,6 +484,11 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||||
return {draft: applied, template};
|
return {draft: applied, template};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function relatedDraft(draft) {
|
||||||
|
const template = issueTemplates.find(item => String(item?.id || '') === String(draft?.templateId || '')) || null;
|
||||||
|
return buildRelatedDraft(draft, template);
|
||||||
|
}
|
||||||
|
|
||||||
function bindTemplatePicker(elements, callbacks) {
|
function bindTemplatePicker(elements, callbacks) {
|
||||||
let labels = [];
|
let labels = [];
|
||||||
elements.select.addEventListener('change', event => {
|
elements.select.addEventListener('change', event => {
|
||||||
|
|
@ -718,7 +747,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||||
return {
|
return {
|
||||||
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, loadOwners, loadTemplates,
|
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, loadOwners, loadTemplates,
|
||||||
loadFilingMetadata, applyTemplate, setTemplateState, templateFields, restoreTemplate, loadTemplateOptions,
|
loadFilingMetadata, applyTemplate, setTemplateState, templateFields, restoreTemplate, loadTemplateOptions,
|
||||||
selectTemplate, bindTemplatePicker, bindFilingMetadata, loadRepositoryPage,
|
selectTemplate, buildRelatedDraft:relatedDraft, bindTemplatePicker, bindFilingMetadata, loadRepositoryPage,
|
||||||
searchRepositories, searchBlockers, findDuplicates,
|
searchRepositories, searchBlockers, findDuplicates,
|
||||||
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
||||||
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
||||||
|
|
@ -728,5 +757,6 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||||
|
|
||||||
createIssueCapture.normalizeSharedContent = normalizeSharedContent;
|
createIssueCapture.normalizeSharedContent = normalizeSharedContent;
|
||||||
createIssueCapture.createOwnerPicker = createIssueOwnerPicker;
|
createIssueCapture.createOwnerPicker = createIssueOwnerPicker;
|
||||||
|
createIssueCapture.buildRelatedDraft = buildRelatedDraft;
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture;
|
if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture;
|
||||||
|
|
|
||||||
|
|
@ -3966,9 +3966,16 @@
|
||||||
root:qs('#issue-filing-receipt'), heading:qs('#issue-filing-receipt-heading'),
|
root:qs('#issue-filing-receipt'), heading:qs('#issue-filing-receipt-heading'),
|
||||||
key:qs('#issue-filing-receipt-key'), title:qs('#issue-filing-receipt-title'),
|
key:qs('#issue-filing-receipt-key'), title:qs('#issue-filing-receipt-title'),
|
||||||
ownership:qs('#issue-filing-receipt-ownership'), openLink:qs('#issue-filing-receipt-open'),
|
ownership:qs('#issue-filing-receipt-ownership'), openLink:qs('#issue-filing-receipt-open'),
|
||||||
shareButton:qs('#issue-filing-receipt-share'), fileAnotherButton:qs('#issue-filing-receipt-another'),
|
shareButton:qs('#issue-filing-receipt-share'), relatedButton:qs('#issue-filing-receipt-related'),
|
||||||
|
fileAnotherButton:qs('#issue-filing-receipt-another'),
|
||||||
doneButton:qs('#issue-filing-receipt-done'), status:qs('#issue-filing-receipt-status'),
|
doneButton:qs('#issue-filing-receipt-done'), status:qs('#issue-filing-receipt-status'),
|
||||||
navigator, onFileAnother:() => qs('#new-issue').click(),
|
navigator,
|
||||||
|
onFileRelated:plan => {
|
||||||
|
issueCapture.saveDraft(plan);
|
||||||
|
createIssueAttachmentController.clear();
|
||||||
|
qs('#new-issue').click();
|
||||||
|
},
|
||||||
|
onFileAnother:() => qs('#new-issue').click(),
|
||||||
});
|
});
|
||||||
function closeCreateIssueSheet(navigate = true, preserveDraft = true) {
|
function closeCreateIssueSheet(navigate = true, preserveDraft = true) {
|
||||||
if (navigate && taskOverlayHistory.current() === 'new') {
|
if (navigate && taskOverlayHistory.current() === 'new') {
|
||||||
|
|
@ -4018,7 +4025,9 @@
|
||||||
);
|
);
|
||||||
const completionHandled = startedCompletions.has(confirmed.repository + '#' + confirmed.number);
|
const completionHandled = startedCompletions.has(confirmed.repository + '#' + confirmed.number);
|
||||||
if (!(confirmed.assignees || []).includes(activeFlushLogin)) {
|
if (!(confirmed.assignees || []).includes(activeFlushLogin)) {
|
||||||
issueFilingReceipt.show(confirmed, qs('#new-issue'));
|
const filing = (result.filings || []).find(candidate => candidate.issue?.repository === confirmed.repository &&
|
||||||
|
candidate.issue?.number === confirmed.number);
|
||||||
|
issueFilingReceipt.show(confirmed, qs('#new-issue'), filing?.relatedDraft);
|
||||||
}
|
}
|
||||||
if (startCreated && !(result.completions || []).length && created) {
|
if (startCreated && !(result.completions || []).length && created) {
|
||||||
const outcome = createAndStart.complete(created);
|
const outcome = createAndStart.complete(created);
|
||||||
|
|
@ -5221,6 +5230,7 @@
|
||||||
});
|
});
|
||||||
async function admitReviewedIssue(review) {
|
async function admitReviewedIssue(review) {
|
||||||
const durableDraft = review.draft;
|
const durableDraft = review.draft;
|
||||||
|
const deliveryDraft = {...durableDraft, relatedDraft:issueCapture.buildRelatedDraft(durableDraft)};
|
||||||
const followUpNextRequested = review.intent === 'follow-up-and-next';
|
const followUpNextRequested = review.intent === 'follow-up-and-next';
|
||||||
createAndStartRequested = review.intent === 'create-and-start';
|
createAndStartRequested = review.intent === 'create-and-start';
|
||||||
if (createAndStartRequested && !createAndStart.available()) {
|
if (createAndStartRequested && !createAndStart.available()) {
|
||||||
|
|
@ -5236,12 +5246,12 @@
|
||||||
'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 admission = followUpNextRequested ? (await updateFollowUp.complete({
|
const admission = followUpNextRequested ? (await updateFollowUp.complete({
|
||||||
admit: () => editingOutboxId ? issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
admit: () => editingOutboxId ? issueOutbox.updateDurably(editingOutboxId, deliveryDraft) :
|
||||||
issueOutbox.enqueueDurably(durableDraft),
|
issueOutbox.enqueueDurably(deliveryDraft),
|
||||||
queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId),
|
queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId),
|
||||||
advance: source => notificationReader.acceptReadAndNext(lastMyWork, source),
|
advance: source => notificationReader.acceptReadAndNext(lastMyWork, source),
|
||||||
})).admission : (editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, durableDraft) :
|
})).admission : (editingOutboxId ? await issueOutbox.updateDurably(editingOutboxId, deliveryDraft) :
|
||||||
await issueOutbox.enqueueDurably(durableDraft));
|
await issueOutbox.enqueueDurably(deliveryDraft));
|
||||||
const queued = admission.item;
|
const queued = admission.item;
|
||||||
pendingIssueFilingIntent = 'create-and-assign';
|
pendingIssueFilingIntent = 'create-and-assign';
|
||||||
const fS = dFS.current();
|
const fS = dFS.current();
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,7 @@
|
||||||
<div class="issue-filing-receipt-actions">
|
<div class="issue-filing-receipt-actions">
|
||||||
<a id="issue-filing-receipt-open" class="button-link" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="issue-filing-receipt-open" class="button-link" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
<button id="issue-filing-receipt-share" type="button">Share link</button>
|
<button id="issue-filing-receipt-share" type="button">Share link</button>
|
||||||
|
<button id="issue-filing-receipt-related" type="button">File related issue</button>
|
||||||
<button id="issue-filing-receipt-another" type="button">File another</button>
|
<button id="issue-filing-receipt-another" type="button">File another</button>
|
||||||
<button id="issue-filing-receipt-done" type="button">Done</button>
|
<button id="issue-filing-receipt-done" type="button">Done</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,18 @@
|
||||||
|
|
||||||
function createIssueFilingReceipt({
|
function createIssueFilingReceipt({
|
||||||
root, heading, key, title, ownership, openLink, shareButton,
|
root, heading, key, title, ownership, openLink, shareButton,
|
||||||
fileAnotherButton, doneButton, status, navigator = {}, clipboard = navigator.clipboard,
|
relatedButton, fileAnotherButton, doneButton, status, navigator = {}, clipboard = navigator.clipboard,
|
||||||
|
onFileRelated = function () {},
|
||||||
onFileAnother = function () {},
|
onFileAnother = function () {},
|
||||||
}) {
|
}) {
|
||||||
let active = null;
|
let active = null;
|
||||||
|
let relatedPlan = null;
|
||||||
let restoreFocus = null;
|
let restoreFocus = null;
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
root.hidden = true;
|
root.hidden = true;
|
||||||
active = null;
|
active = null;
|
||||||
|
relatedPlan = null;
|
||||||
const target = restoreFocus;
|
const target = restoreFocus;
|
||||||
restoreFocus = null;
|
restoreFocus = null;
|
||||||
target?.focus?.();
|
target?.focus?.();
|
||||||
|
|
@ -44,8 +47,9 @@
|
||||||
status.textContent = 'Issue link copied.';
|
status.textContent = 'Issue link copied.';
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(issue, returnTarget) {
|
function show(issue, returnTarget, reusablePlan = null) {
|
||||||
active = issue;
|
active = issue;
|
||||||
|
relatedPlan = reusablePlan;
|
||||||
restoreFocus = returnTarget || null;
|
restoreFocus = returnTarget || null;
|
||||||
key.textContent = issue.repository + '#' + issue.number;
|
key.textContent = issue.repository + '#' + issue.number;
|
||||||
title.textContent = issue.title;
|
title.textContent = issue.title;
|
||||||
|
|
@ -54,6 +58,7 @@
|
||||||
: 'Created with no owner.';
|
: 'Created with no owner.';
|
||||||
openLink.href = issue.url;
|
openLink.href = issue.url;
|
||||||
status.textContent = '';
|
status.textContent = '';
|
||||||
|
if (relatedButton) relatedButton.hidden = !relatedPlan;
|
||||||
root.hidden = false;
|
root.hidden = false;
|
||||||
heading.focus();
|
heading.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +70,12 @@
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
doneButton.addEventListener('click', close);
|
doneButton.addEventListener('click', close);
|
||||||
|
relatedButton?.addEventListener('click', () => {
|
||||||
|
if (!relatedPlan) return;
|
||||||
|
const plan = relatedPlan;
|
||||||
|
close();
|
||||||
|
onFileRelated(plan);
|
||||||
|
});
|
||||||
fileAnotherButton.addEventListener('click', () => {
|
fileAnotherButton.addEventListener('click', () => {
|
||||||
close();
|
close();
|
||||||
onFileAnother();
|
onFileAnother();
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,31 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
return Number.isInteger(estimate) && estimate >= 5 && estimate <= 1440 ? estimate : undefined;
|
return Number.isInteger(estimate) && estimate >= 5 && estimate <= 1440 ? estimate : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureRelatedDraft(value) {
|
||||||
|
const repository = String(value?.repository || '');
|
||||||
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return undefined;
|
||||||
|
const plan = {
|
||||||
|
repository, title:'', body:String(value?.body || '').slice(0, 9000),
|
||||||
|
labelIds:Array.from(new Set((Array.isArray(value?.labelIds) ? value.labelIds : [])
|
||||||
|
.filter(id => Number.isInteger(id) && id > 0))).slice(0, 20),
|
||||||
|
};
|
||||||
|
const milestoneId = Number(value?.milestoneId);
|
||||||
|
if (Number.isInteger(milestoneId) && milestoneId > 0) plan.milestoneId = milestoneId;
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(String(value?.dueDate || ''))) plan.dueDate = String(value.dueDate);
|
||||||
|
if (value?.unassigned === true) plan.unassigned = true;
|
||||||
|
else if (/^[A-Za-z0-9_.-]+$/.test(String(value?.assignee || ''))) {
|
||||||
|
plan.assignee = String(value.assignee);
|
||||||
|
plan.assigneeName = String(value.assigneeName || value.assignee).replace(/\s+/g, ' ').trim().slice(0, 255);
|
||||||
|
}
|
||||||
|
const templateId = String(value?.templateId || '').slice(0, 80);
|
||||||
|
if (templateId) {
|
||||||
|
plan.templateId = templateId;
|
||||||
|
plan.templateName = String(value?.templateName || 'Issue template').trim().slice(0, 80);
|
||||||
|
plan.capturedBody = '';
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
function read() {
|
function read() {
|
||||||
try {
|
try {
|
||||||
const record = JSON.parse(storage?.getItem(storageKey) || 'null');
|
const record = JSON.parse(storage?.getItem(storageKey) || 'null');
|
||||||
|
|
@ -107,6 +132,8 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
if (attachments) item.attachments = attachments;
|
if (attachments) item.attachments = attachments;
|
||||||
const blockers = captureBlockers(draft?.blockers);
|
const blockers = captureBlockers(draft?.blockers);
|
||||||
if (blockers) item.blockers = blockers;
|
if (blockers) item.blockers = blockers;
|
||||||
|
const relatedDraft = captureRelatedDraft(draft?.relatedDraft);
|
||||||
|
if (relatedDraft) item.relatedDraft = relatedDraft;
|
||||||
item.operationId = item.id;
|
item.operationId = item.id;
|
||||||
if (Number.isInteger(Number(draft?.milestoneId)) && Number(draft.milestoneId) > 0) {
|
if (Number.isInteger(Number(draft?.milestoneId)) && Number(draft.milestoneId) > 0) {
|
||||||
item.milestoneId = Number(draft.milestoneId);
|
item.milestoneId = Number(draft.milestoneId);
|
||||||
|
|
@ -220,6 +247,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
const attachment = captureAttachment(draft?.attachment);
|
const attachment = captureAttachment(draft?.attachment);
|
||||||
const attachments = captureAttachments(draft?.attachments);
|
const attachments = captureAttachments(draft?.attachments);
|
||||||
const blockers = captureBlockers(draft?.blockers);
|
const blockers = captureBlockers(draft?.blockers);
|
||||||
|
const relatedDraft = captureRelatedDraft(draft?.relatedDraft);
|
||||||
const attachmentChanged = JSON.stringify(item.attachment || null) !== JSON.stringify(attachment || null) ||
|
const attachmentChanged = JSON.stringify(item.attachment || null) !== JSON.stringify(attachment || null) ||
|
||||||
JSON.stringify(item.attachments || null) !== JSON.stringify(attachments || null);
|
JSON.stringify(item.attachments || null) !== JSON.stringify(attachments || null);
|
||||||
const changed = item.repository !== repository || item.title !== title || item.body !== body
|
const changed = item.repository !== repository || item.title !== title || item.body !== body
|
||||||
|
|
@ -228,13 +256,14 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
|| item.assignee !== assignee || item.assigneeName !== assigneeName
|
|| item.assignee !== assignee || item.assigneeName !== assigneeName
|
||||||
|| item.milestoneId !== milestoneId || item.dueDate !== dueDate
|
|| item.milestoneId !== milestoneId || item.dueDate !== dueDate
|
||||||
|| item.estimateMinutes !== captureEstimate(draft?.estimateMinutes)
|
|| item.estimateMinutes !== captureEstimate(draft?.estimateMinutes)
|
||||||
|| attachmentChanged || JSON.stringify(item.blockers || null) !== JSON.stringify(blockers || null);
|
|| attachmentChanged || JSON.stringify(item.blockers || null) !== JSON.stringify(blockers || null)
|
||||||
|
|| JSON.stringify(item.relatedDraft || null) !== JSON.stringify(relatedDraft || null);
|
||||||
updated = {
|
updated = {
|
||||||
...item,
|
...item,
|
||||||
repository, title, body, labelIds,
|
repository, title, body, labelIds,
|
||||||
unassigned: unassigned || undefined,
|
unassigned: unassigned || undefined,
|
||||||
assignee, assigneeName, milestoneId, dueDate,
|
assignee, assigneeName, milestoneId, dueDate,
|
||||||
attachment, attachments, blockers,
|
attachment, attachments, blockers, relatedDraft,
|
||||||
operationId: changed ? String(operationId()).slice(0, 128) : item.operationId,
|
operationId: changed ? String(operationId()).slice(0, 128) : item.operationId,
|
||||||
status: 'queued',
|
status: 'queued',
|
||||||
};
|
};
|
||||||
|
|
@ -255,6 +284,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
if (attachment === undefined) delete updated.attachment;
|
if (attachment === undefined) delete updated.attachment;
|
||||||
if (attachments === undefined) delete updated.attachments;
|
if (attachments === undefined) delete updated.attachments;
|
||||||
if (blockers === undefined) delete updated.blockers;
|
if (blockers === undefined) delete updated.blockers;
|
||||||
|
if (relatedDraft === undefined) delete updated.relatedDraft;
|
||||||
if (attachmentChanged) {
|
if (attachmentChanged) {
|
||||||
delete updated.attachmentMarkdown;
|
delete updated.attachmentMarkdown;
|
||||||
delete updated.attachmentMarkdowns;
|
delete updated.attachmentMarkdowns;
|
||||||
|
|
@ -465,6 +495,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
async function flushQueue(currentLogin) {
|
async function flushQueue(currentLogin) {
|
||||||
const confirmed = [];
|
const confirmed = [];
|
||||||
const completions = [];
|
const completions = [];
|
||||||
|
const filings = [];
|
||||||
let blocked = 0;
|
let blocked = 0;
|
||||||
currentLogin = String(currentLogin || '').trim();
|
currentLogin = String(currentLogin || '').trim();
|
||||||
for (const item of read()) {
|
for (const item of read()) {
|
||||||
|
|
@ -473,6 +504,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
const result = await sendItem(item, currentLogin);
|
const result = await sendItem(item, currentLogin);
|
||||||
if (result.issue) {
|
if (result.issue) {
|
||||||
confirmed.push(result.issue);
|
confirmed.push(result.issue);
|
||||||
|
if (result.item?.relatedDraft) filings.push({issue:result.issue, relatedDraft:result.item.relatedDraft});
|
||||||
if (result.item?.completionIntent) completions.push({
|
if (result.item?.completionIntent) completions.push({
|
||||||
id: result.item.id,
|
id: result.item.id,
|
||||||
intent: result.item.completionIntent,
|
intent: result.item.completionIntent,
|
||||||
|
|
@ -484,7 +516,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
}
|
}
|
||||||
if (result.transient) break;
|
if (result.transient) break;
|
||||||
}
|
}
|
||||||
return { confirmed, completions, remaining: read(), blocked };
|
return { confirmed, completions, filings, remaining: read(), blocked };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function flush(currentLogin) {
|
async function flush(currentLogin) {
|
||||||
|
|
@ -513,6 +545,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
||||||
const result = await sendItem(queued, currentLogin);
|
const result = await sendItem(queued, currentLogin);
|
||||||
return {
|
return {
|
||||||
confirmed: result.issue ? [result.issue] : [],
|
confirmed: result.issue ? [result.issue] : [],
|
||||||
|
filings: result.issue && result.item?.relatedDraft ? [{issue:result.issue, relatedDraft:result.item.relatedDraft}] : [],
|
||||||
completions: result.issue && result.item?.completionIntent ? [{
|
completions: result.issue && result.item?.completionIntent ? [{
|
||||||
id: result.item.id,
|
id: result.item.id,
|
||||||
intent: result.item.completionIntent,
|
intent: result.item.completionIntent,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,26 @@ controller.show({{repository:'stackchain/dashboard',number:867,title:'Unowned fo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_related_action_returns_only_the_confirmed_reusable_plan():
|
||||||
|
script = f"""
|
||||||
|
const createReceipt = require({json.dumps(str(RECEIPT))});
|
||||||
|
const element = () => ({{hidden:true,focusCount:0,focus(){{this.focusCount++;}},addEventListener(name, fn){{this[name]=fn;}}}});
|
||||||
|
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),relatedButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
|
||||||
|
const plans=[];
|
||||||
|
const controller=createReceipt({{...elements,onFileRelated:plan=>plans.push(plan)}});
|
||||||
|
const plan={{repository:'stackchain/dashboard',title:'',body:'## Bug',labelIds:[7],templateId:'bug.yml'}};
|
||||||
|
controller.show({{repository:'stackchain/dashboard',number:868,title:'Filed',assignees:[],url:'https://forge.example/issues/868'}},elements.doneButton,plan);
|
||||||
|
elements.relatedButton.click();
|
||||||
|
process.stdout.write(JSON.stringify({{plans,hidden:elements.root.hidden,restored:elements.doneButton.focusCount}}));
|
||||||
|
"""
|
||||||
|
output = run_node(script)
|
||||||
|
|
||||||
|
assert output == {"plans": [{
|
||||||
|
"repository": "stackchain/dashboard", "title": "", "body": "## Bug",
|
||||||
|
"labelIds": [7], "templateId": "bug.yml",
|
||||||
|
}], "hidden": True, "restored": 1}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
|
def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
|
||||||
index = INDEX.read_text()
|
index = INDEX.read_text()
|
||||||
css = CSS.read_text()
|
css = CSS.read_text()
|
||||||
|
|
@ -78,6 +98,7 @@ def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
|
||||||
assert 'id="issue-filing-receipt-heading"' in index
|
assert 'id="issue-filing-receipt-heading"' in index
|
||||||
assert 'id="issue-filing-receipt-open"' in index
|
assert 'id="issue-filing-receipt-open"' in index
|
||||||
assert 'id="issue-filing-receipt-share"' in index
|
assert 'id="issue-filing-receipt-share"' in index
|
||||||
|
assert 'id="issue-filing-receipt-related"' in index
|
||||||
assert 'id="issue-filing-receipt-another"' in index
|
assert 'id="issue-filing-receipt-another"' in index
|
||||||
assert 'id="issue-filing-receipt-done"' in index
|
assert 'id="issue-filing-receipt-done"' in index
|
||||||
assert '<script src="static/issue-filing-receipt.js"></script>' in index
|
assert '<script src="static/issue-filing-receipt.js"></script>' in index
|
||||||
|
|
@ -86,4 +107,7 @@ def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
|
||||||
assert '.issue-filing-receipt-actions' in css
|
assert '.issue-filing-receipt-actions' in css
|
||||||
assert 'min-height:44px' in css
|
assert 'min-height:44px' in css
|
||||||
assert 'createIssueFilingReceipt({' in dashboard
|
assert 'createIssueFilingReceipt({' in dashboard
|
||||||
assert "issueFilingReceipt.show(confirmed, qs('#new-issue'))" in dashboard
|
assert "relatedButton:qs('#issue-filing-receipt-related')" in dashboard
|
||||||
|
assert 'relatedDraft:issueCapture.buildRelatedDraft(durableDraft)' in dashboard
|
||||||
|
assert "issueCapture.saveDraft(plan)" in dashboard
|
||||||
|
assert "issueFilingReceipt.show(confirmed, qs('#new-issue'), filing?.relatedDraft)" in dashboard
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,61 @@ confirmedLogin = 'alexander';
|
||||||
assert len(output["calls"]) == 1
|
assert len(output["calls"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirmed_delivery_returns_the_safe_related_plan_without_sending_it_to_gitea():
|
||||||
|
script = f"""
|
||||||
|
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||||
|
const values=new Map();
|
||||||
|
const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}};
|
||||||
|
let sentBody;
|
||||||
|
const outbox=createIssueOutbox({{
|
||||||
|
storage,getOwnerLogin:()=> 'timmy',createOperationId:()=> 'first-operation',
|
||||||
|
fetchJson:async (_url,options)=>{{sentBody=JSON.parse(options.body);return {{repository:'stackchain/dashboard',number:869,title:'Related source',assignees:[]}};}},
|
||||||
|
}});
|
||||||
|
outbox.enqueue({{
|
||||||
|
repository:'stackchain/dashboard',title:'Related source',body:'Filled report',labelIds:[7],
|
||||||
|
relatedDraft:{{repository:'stackchain/dashboard',title:'',body:'## Bug',labelIds:[7],templateId:'bug.yml',templateName:'Bug report',capturedBody:''}},
|
||||||
|
}});
|
||||||
|
outbox.flush('timmy').then(result=>process.stdout.write(JSON.stringify({{result,sentBody}})));
|
||||||
|
"""
|
||||||
|
output = run_node(script)
|
||||||
|
|
||||||
|
assert output["result"]["filings"] == [{
|
||||||
|
"issue": {"repository": "stackchain/dashboard", "number": 869,
|
||||||
|
"title": "Related source", "assignees": []},
|
||||||
|
"relatedDraft": {"repository": "stackchain/dashboard", "title": "",
|
||||||
|
"body": "## Bug", "labelIds": [7], "templateId": "bug.yml",
|
||||||
|
"templateName": "Bug report", "capturedBody": ""},
|
||||||
|
}]
|
||||||
|
assert "relatedDraft" not in output["sentBody"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_editing_a_queued_issue_refreshes_its_related_plan():
|
||||||
|
script = f"""
|
||||||
|
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||||
|
const values=new Map();
|
||||||
|
const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}};
|
||||||
|
let sequence=0;
|
||||||
|
const outbox=createIssueOutbox({{
|
||||||
|
storage,getOwnerLogin:()=> 'timmy',createOperationId:()=> 'operation-' + (++sequence),
|
||||||
|
fetchJson:async ()=>({{repository:'stackchain/dashboard',number:870,title:'Edited',assignees:[]}}),
|
||||||
|
}});
|
||||||
|
const queued=outbox.enqueue({{
|
||||||
|
repository:'stackchain/dashboard',title:'Original',body:'One',
|
||||||
|
relatedDraft:{{repository:'stackchain/dashboard',title:'',body:'Old scaffold',labelIds:[1]}},
|
||||||
|
}});
|
||||||
|
outbox.update(queued.id,{{
|
||||||
|
repository:'stackchain/dashboard',title:'Edited',body:'Two',
|
||||||
|
relatedDraft:{{repository:'stackchain/dashboard',title:'',body:'New scaffold',labelIds:[2]}},
|
||||||
|
}});
|
||||||
|
outbox.flush('timmy').then(result=>process.stdout.write(JSON.stringify(result.filings[0].relatedDraft)));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"repository": "stackchain/dashboard", "title": "", "body": "New scaffold",
|
||||||
|
"labelIds": [2],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_issue_outbox_queues_multiple_planned_issues_with_stable_operation_ids():
|
def test_issue_outbox_queues_multiple_planned_issues_with_stable_operation_ids():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||||
|
|
@ -968,7 +1023,7 @@ async def test_mobile_dashboard_queues_offline_captures_and_exposes_outbox_actio
|
||||||
|
|
||||||
assert '<script src="static/issue-outbox.js"></script>' in html
|
assert '<script src="static/issue-outbox.js"></script>' in html
|
||||||
assert "const issueOutbox = createIssueOutbox({" in html
|
assert "const issueOutbox = createIssueOutbox({" in html
|
||||||
assert "await issueOutbox.enqueueDurably(durableDraft)" in html
|
assert "await issueOutbox.enqueueDurably(deliveryDraft)" in html
|
||||||
assert "Saving for background delivery…" in html
|
assert "Saving for background delivery…" in html
|
||||||
assert "Saved for next launch; background delivery unavailable." in html
|
assert "Saved for next launch; background delivery unavailable." in html
|
||||||
assert "throw new Error('Background Sync unavailable')" in html
|
assert "throw new Error('Background Sync unavailable')" in html
|
||||||
|
|
|
||||||
|
|
@ -1578,6 +1578,40 @@ Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.string
|
||||||
assert output["results"][0]["available"] is True
|
assert output["results"][0]["available"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_related_issue_draft_reuses_plan_but_resets_issue_specific_work():
|
||||||
|
script = f"""
|
||||||
|
const createIssueSheet = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||||
|
const related = createIssueSheet.buildRelatedDraft({{
|
||||||
|
repository:'stackchain/dashboard', title:'Completed discovery', body:'Filled private report',
|
||||||
|
labelIds:[7, 7, 9], milestoneId:4, dueDate:'2026-08-20',
|
||||||
|
assignee:'alex', assigneeName:'Alexander', unassigned:false,
|
||||||
|
templateId:'bug.yml', templateName:'Bug report', capturedBody:'Original notes',
|
||||||
|
blockers:[{{repository:'stackchain/api',number:2}}], estimateMinutes:45,
|
||||||
|
completionIntent:'create-and-start', operationId:'already-delivered',
|
||||||
|
attachment:{{name:'secret.png'}}, duplicateAcknowledged:true,
|
||||||
|
}}, {{id:'bug.yml', name:'Bug report', body:'## What happened?\\n\\n## Expected'}});
|
||||||
|
process.stdout.write(JSON.stringify(related));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
output = json.loads(result.stdout)
|
||||||
|
|
||||||
|
assert output == {
|
||||||
|
"repository": "stackchain/dashboard",
|
||||||
|
"title": "",
|
||||||
|
"body": "## What happened?\n\n## Expected",
|
||||||
|
"labelIds": [7, 9],
|
||||||
|
"milestoneId": 4,
|
||||||
|
"dueDate": "2026-08-20",
|
||||||
|
"assignee": "alex",
|
||||||
|
"assigneeName": "Alexander",
|
||||||
|
"templateId": "bug.yml",
|
||||||
|
"templateName": "Bug report",
|
||||||
|
"capturedBody": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_issue_release_rejects_response_that_still_assigns_current_user():
|
def test_issue_release_rejects_response_that_still_assigns_current_user():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user