feat: load resilient issue filing metadata
This commit is contained in:
1 parent
2402af9e82
commit
a226addcc4
5 files changed
+357
-63
No files matched your search
@@ -113,6 +113,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
let templateRequest = 0;
|
||||
let duplicateRequest = 0;
|
||||
let repositorySearchRequest = 0;
|
||||
let filingMetadataRequest = 0;
|
||||
let blockerSearchRequest = 0;
|
||||
let duplicateState = {status: 'idle', key: '', candidates: []};
|
||||
let acknowledgedDuplicateKey = '';
|
||||
@@ -326,6 +327,32 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
);
|
||||
}
|
||||
|
||||
async function loadFilingMetadata(repository) {
|
||||
const selectedRepository = String(repository || '');
|
||||
const encoded = selectedRepository.split('/').map(encodeURIComponent).join('/');
|
||||
const request = ++filingMetadataRequest;
|
||||
const payload = await fetchJson(
|
||||
'api/v1/repos/' + encoded + '/issue-filing-metadata'
|
||||
);
|
||||
if (request !== filingMetadataRequest) {
|
||||
return {status:'stale', repository:selectedRepository};
|
||||
}
|
||||
const section = name => {
|
||||
const value = payload?.[name];
|
||||
const available = value?.available === true;
|
||||
return {
|
||||
available,
|
||||
items:available && Array.isArray(value?.items) ? value.items : [],
|
||||
...(!available && typeof value?.error === 'string' ? {error:value.error} : {}),
|
||||
};
|
||||
};
|
||||
return {
|
||||
status:'ready', repository:selectedRepository,
|
||||
labels:section('labels'), milestones:section('milestones'),
|
||||
templates:section('templates'),
|
||||
};
|
||||
}
|
||||
|
||||
function applyTemplate(draft, template, availableLabels = []) {
|
||||
const source = {...(draft || {})};
|
||||
const capturedBody = source.templateName ? String(source.capturedBody || '') : String(source.body || '');
|
||||
@@ -425,6 +452,28 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
load(repository, selectedId) {
|
||||
return loadTemplateOptions(repository, elements, selectedId);
|
||||
},
|
||||
render(repository, section, selectedId = '') {
|
||||
++templateRequest;
|
||||
issueTemplates = [];
|
||||
elements.select.innerHTML = '<option value="">Blank issue</option>';
|
||||
elements.field.hidden = true;
|
||||
if (!repository || elements.getRepository() !== repository) return;
|
||||
if (section?.available !== true) {
|
||||
elements.status.textContent = 'Issue types could not be loaded. Blank issue filing is still available.';
|
||||
return;
|
||||
}
|
||||
issueTemplates = Array.isArray(section.items) ? section.items.slice(0, 20) : [];
|
||||
issueTemplates.forEach(template => {
|
||||
const option = elements.document.createElement('option');
|
||||
option.value = template.id;
|
||||
option.textContent = template.name;
|
||||
elements.select.appendChild(option);
|
||||
});
|
||||
elements.field.hidden = issueTemplates.length === 0;
|
||||
elements.select.value = issueTemplates.some(template => template.id === selectedId) ? selectedId : '';
|
||||
elements.status.textContent = issueTemplates.length ?
|
||||
'Choose a repository guide or keep a blank issue.' : 'Blank issue selected.';
|
||||
},
|
||||
changeRepository(draft) {
|
||||
const restored = restoreTemplate(draft, labels);
|
||||
callbacks.setDraft(restored);
|
||||
@@ -433,6 +482,92 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
};
|
||||
}
|
||||
|
||||
function bindFilingMetadata(elements, templatePicker) {
|
||||
function renderLabels(repository, section, selectedIds = []) {
|
||||
elements.labelList.replaceChildren();
|
||||
if (!repository) {
|
||||
elements.labelStatus.textContent = 'Choose a repository to load labels.';
|
||||
return;
|
||||
}
|
||||
if (section?.available !== true) {
|
||||
templatePicker.setLabels([]);
|
||||
elements.labelStatus.textContent = 'Labels could not be loaded. You can still create the issue without labels.';
|
||||
return;
|
||||
}
|
||||
const priorities = new Set(['p0', 'priority-high', 'critical']);
|
||||
const labels = (Array.isArray(section.items) ? section.items : []).slice().sort((left, right) =>
|
||||
Number(priorities.has(String(right?.name || '').toLowerCase())) -
|
||||
Number(priorities.has(String(left?.name || '').toLowerCase()))
|
||||
);
|
||||
templatePicker.setLabels(labels);
|
||||
const selected = new Set(selectedIds.map(Number));
|
||||
labels.forEach(label => {
|
||||
const option = elements.document.createElement('label');
|
||||
option.className = 'create-issue-label-option';
|
||||
const input = elements.document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.name = 'create-issue-label';
|
||||
input.value = String(Number(label.id));
|
||||
input.checked = selected.has(Number(label.id));
|
||||
const name = elements.document.createElement('span');
|
||||
name.textContent = label.name;
|
||||
option.append(input, name);
|
||||
elements.labelList.appendChild(option);
|
||||
});
|
||||
elements.labelStatus.textContent = labels.length ?
|
||||
'Select labels to triage this issue.' : 'This repository has no labels.';
|
||||
}
|
||||
|
||||
function renderMilestones(repository, section, selectedId = null) {
|
||||
elements.milestoneSelect.replaceChildren();
|
||||
const blank = elements.document.createElement('option');
|
||||
blank.value = '';
|
||||
blank.textContent = 'No milestone';
|
||||
elements.milestoneSelect.appendChild(blank);
|
||||
if (!repository) {
|
||||
elements.milestoneStatus.textContent = 'Choose a repository to load milestones.';
|
||||
return;
|
||||
}
|
||||
if (section?.available !== true) {
|
||||
elements.milestoneStatus.textContent = 'Milestones could not be loaded. You can still create an unplanned issue.';
|
||||
return;
|
||||
}
|
||||
const milestones = Array.isArray(section.items) ? section.items : [];
|
||||
milestones.forEach(milestone => {
|
||||
const option = elements.document.createElement('option');
|
||||
option.value = String(Number(milestone.id));
|
||||
option.textContent = milestone.title;
|
||||
elements.milestoneSelect.appendChild(option);
|
||||
});
|
||||
if (selectedId) elements.milestoneSelect.value = String(selectedId);
|
||||
elements.milestoneStatus.textContent = milestones.length ?
|
||||
'Choose the release lane for this issue.' : 'This repository has no open milestones.';
|
||||
}
|
||||
|
||||
async function load(repository, selected = {}) {
|
||||
renderLabels(repository, null, selected.labelIds || []);
|
||||
renderMilestones(repository, null, selected.milestoneId);
|
||||
templatePicker.render(repository, null, selected.templateId);
|
||||
if (!repository) return;
|
||||
elements.labelStatus.textContent = 'Loading labels…';
|
||||
elements.milestoneStatus.textContent = 'Loading milestones…';
|
||||
elements.templateStatus.textContent = 'Loading issue types…';
|
||||
try {
|
||||
const metadata = await loadFilingMetadata(repository);
|
||||
if (metadata.status !== 'ready' || elements.getRepository() !== repository) return;
|
||||
renderLabels(repository, metadata.labels, selected.labelIds || []);
|
||||
renderMilestones(repository, metadata.milestones, selected.milestoneId);
|
||||
templatePicker.render(repository, metadata.templates, selected.templateId);
|
||||
} catch (_error) {
|
||||
if (elements.getRepository() !== repository) return;
|
||||
renderLabels(repository, {available:false}, selected.labelIds || []);
|
||||
renderMilestones(repository, {available:false}, selected.milestoneId);
|
||||
templatePicker.render(repository, {available:false}, selected.templateId);
|
||||
}
|
||||
}
|
||||
return {load};
|
||||
}
|
||||
|
||||
function loadRepositoryPage(page) {
|
||||
const safePage = Math.max(1, Math.floor(Number(page) || 1));
|
||||
if (repositoryPageRequests.has(safePage)) return repositoryPageRequests.get(safePage);
|
||||
@@ -555,8 +690,8 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
|
||||
return {
|
||||
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, loadOwners, loadTemplates,
|
||||
applyTemplate, setTemplateState, templateFields, restoreTemplate, loadTemplateOptions,
|
||||
selectTemplate, bindTemplatePicker, loadRepositoryPage,
|
||||
loadFilingMetadata, applyTemplate, setTemplateState, templateFields, restoreTemplate, loadTemplateOptions,
|
||||
selectTemplate, bindTemplatePicker, bindFilingMetadata, loadRepositoryPage,
|
||||
searchRepositories, searchBlockers, findDuplicates,
|
||||
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
||||
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
||||
|
||||
+15
-58
@@ -521,6 +521,7 @@
|
||||
let issueCapture = null;
|
||||
let issueOwnerPicker = null;
|
||||
let issueTemplatePicker = null;
|
||||
let issueFilingMetadata = null;
|
||||
let updateFollowUp = null;
|
||||
const unfiledAttachmentStore = 'indexedDB' in window ? createUnfiledAttachmentStore() : null;
|
||||
const unfiledCaptures = createUnfiledCaptures({
|
||||
@@ -657,6 +658,15 @@
|
||||
setDraft:draft=>{ qs('#create-issue-title').value=draft.title; qs('#create-issue-body').value=draft.body; },
|
||||
changed:()=>{ saveIssueCaptureDraft(); scheduleIssueDuplicateCheck(); },
|
||||
});
|
||||
issueFilingMetadata = issueCapture.bindFilingMetadata({
|
||||
document,
|
||||
labelList:qs('#create-issue-label-list'),
|
||||
labelStatus:qs('#create-issue-label-status'),
|
||||
milestoneSelect:qs('#create-issue-milestone'),
|
||||
milestoneStatus:qs('#create-issue-milestone-status'),
|
||||
templateStatus:qs('#create-issue-template-status'),
|
||||
getRepository:()=>qs('#create-issue-repository').value,
|
||||
}, issueTemplatePicker);
|
||||
updateFollowUp = createUpdateFollowUp({ storage:localStorage, getLogin:()=>confirmedOwnerLogin });
|
||||
}
|
||||
if (!sharedLaunchHandled && Object.values(sharedLaunch).some(Boolean)) {
|
||||
@@ -3801,8 +3811,7 @@
|
||||
qs('#create-issue-repository-results').hidden = true;
|
||||
qs('#create-issue-repository-status').textContent = 'Selected ' + repository + '.';
|
||||
issueOwnerPicker.reset(repository);
|
||||
loadIssueLabels(repository);
|
||||
loadIssueMilestones(repository);
|
||||
loadIssueFilingMetadata(repository);
|
||||
saveIssueCaptureDraft();
|
||||
scheduleIssueDuplicateCheck();
|
||||
updateIssueCreateActions();
|
||||
@@ -3859,57 +3868,8 @@
|
||||
}, 350);
|
||||
}
|
||||
|
||||
async function loadIssueLabels(repository, selectedIds = []) {
|
||||
const list = qs('#create-issue-label-list');
|
||||
const status = qs('#create-issue-label-status');
|
||||
list.innerHTML = '';
|
||||
if (!repository) {
|
||||
status.textContent = 'Choose a repository to load labels.';
|
||||
return;
|
||||
}
|
||||
status.textContent = 'Loading labels…';
|
||||
try {
|
||||
const labels = await issueCapture.loadLabels(repository);
|
||||
if (qs('#create-issue-repository').value !== repository) return;
|
||||
issueTemplatePicker.setLabels(labels);
|
||||
const selected = new Set(selectedIds.map(Number));
|
||||
list.innerHTML = labels.map(label =>
|
||||
'<label class="create-issue-label-option"><input type="checkbox" name="create-issue-label" value="' +
|
||||
Number(label.id) + '"' + (selected.has(Number(label.id)) ? ' checked' : '') + '><span>' +
|
||||
escapeHtml(label.name) + '</span></label>'
|
||||
).join('');
|
||||
status.textContent = labels.length ? 'Select labels to triage this issue.' : 'This repository has no labels.';
|
||||
} catch (error) {
|
||||
status.textContent = 'Labels could not be loaded. You can still create the issue without labels.';
|
||||
}
|
||||
}
|
||||
|
||||
function loadIssueTemplates(repository, selectedId = '') {
|
||||
return issueTemplatePicker.load(repository, selectedId);
|
||||
}
|
||||
|
||||
async function loadIssueMilestones(repository, selectedId = null) {
|
||||
const select = qs('#create-issue-milestone');
|
||||
const status = qs('#create-issue-milestone-status');
|
||||
select.innerHTML = '<option value="">No milestone</option>';
|
||||
if (!repository) {
|
||||
status.textContent = 'Choose a repository to load milestones.';
|
||||
return;
|
||||
}
|
||||
status.textContent = 'Loading milestones…';
|
||||
try {
|
||||
const milestones = await issueCapture.loadMilestones(repository);
|
||||
if (qs('#create-issue-repository').value !== repository) return;
|
||||
select.innerHTML += milestones.map(milestone =>
|
||||
'<option value="' + Number(milestone.id) + '">' + escapeHtml(milestone.title) + '</option>'
|
||||
).join('');
|
||||
if (selectedId) select.value = String(selectedId);
|
||||
status.textContent = milestones.length ?
|
||||
'Choose the release lane for this issue.' : 'This repository has no open milestones.';
|
||||
} catch (_error) {
|
||||
if (qs('#create-issue-repository').value !== repository) return;
|
||||
status.textContent = 'Milestones could not be loaded. You can still create an unplanned issue.';
|
||||
}
|
||||
async function loadIssueFilingMetadata(repository, selected = {}) {
|
||||
return issueFilingMetadata.load(repository, selected);
|
||||
}
|
||||
|
||||
function clearSharedLaunchUrl() {
|
||||
@@ -3964,9 +3924,7 @@
|
||||
renderIssueCaptureBlockers(captureDraft.blockers || []);
|
||||
qs('#create-issue-blocker-search').value = '';
|
||||
qs('#create-issue-blocker-results').hidden = true;
|
||||
loadIssueLabels(qs('#create-issue-repository').value, captureDraft.labelIds).then(() =>
|
||||
loadIssueTemplates(qs('#create-issue-repository').value, captureDraft.templateId));
|
||||
loadIssueMilestones(qs('#create-issue-repository').value, captureDraft.milestoneId);
|
||||
loadIssueFilingMetadata(qs('#create-issue-repository').value, captureDraft);
|
||||
scheduleIssueDuplicateCheck();
|
||||
qs('#create-issue-status').textContent = issueCaptureRepositories.length ? '' : 'No accessible repositories are available.';
|
||||
qs('#create-follow-up-next').hidden = !updateFollowUp?.source();
|
||||
@@ -5155,8 +5113,7 @@
|
||||
qs('#create-issue-repository').addEventListener('change', event => {
|
||||
issueTemplatePicker.changeRepository(currentIssueCaptureDraft());
|
||||
issueOwnerPicker.reset(event.target.value);
|
||||
loadIssueLabels(event.target.value).then(() => loadIssueTemplates(event.target.value));
|
||||
loadIssueMilestones(event.target.value);
|
||||
loadIssueFilingMetadata(event.target.value);
|
||||
qs('#create-issue-repository-search').value = event.target.value;
|
||||
saveIssueCaptureDraft();
|
||||
scheduleIssueDuplicateCheck();
|
||||
|
||||
Reference in new issue
Block a user