Compare commits
No commits in common. "8edc83893ff371844432a0b24724b1b22920af83" and "2402af9e82ce43c13249ffda39fe39483123c01d" have entirely different histories.
8edc83893f
...
2402af9e82
|
|
@ -113,7 +113,6 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||||
let templateRequest = 0;
|
let templateRequest = 0;
|
||||||
let duplicateRequest = 0;
|
let duplicateRequest = 0;
|
||||||
let repositorySearchRequest = 0;
|
let repositorySearchRequest = 0;
|
||||||
let filingMetadataRequest = 0;
|
|
||||||
let blockerSearchRequest = 0;
|
let blockerSearchRequest = 0;
|
||||||
let duplicateState = {status: 'idle', key: '', candidates: []};
|
let duplicateState = {status: 'idle', key: '', candidates: []};
|
||||||
let acknowledgedDuplicateKey = '';
|
let acknowledgedDuplicateKey = '';
|
||||||
|
|
@ -327,32 +326,6 @@ 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 = []) {
|
function applyTemplate(draft, template, availableLabels = []) {
|
||||||
const source = {...(draft || {})};
|
const source = {...(draft || {})};
|
||||||
const capturedBody = source.templateName ? String(source.capturedBody || '') : String(source.body || '');
|
const capturedBody = source.templateName ? String(source.capturedBody || '') : String(source.body || '');
|
||||||
|
|
@ -452,28 +425,6 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||||
load(repository, selectedId) {
|
load(repository, selectedId) {
|
||||||
return loadTemplateOptions(repository, elements, 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) {
|
changeRepository(draft) {
|
||||||
const restored = restoreTemplate(draft, labels);
|
const restored = restoreTemplate(draft, labels);
|
||||||
callbacks.setDraft(restored);
|
callbacks.setDraft(restored);
|
||||||
|
|
@ -482,92 +433,6 @@ 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) {
|
function loadRepositoryPage(page) {
|
||||||
const safePage = Math.max(1, Math.floor(Number(page) || 1));
|
const safePage = Math.max(1, Math.floor(Number(page) || 1));
|
||||||
if (repositoryPageRequests.has(safePage)) return repositoryPageRequests.get(safePage);
|
if (repositoryPageRequests.has(safePage)) return repositoryPageRequests.get(safePage);
|
||||||
|
|
@ -690,8 +555,8 @@ 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,
|
applyTemplate, setTemplateState, templateFields, restoreTemplate, loadTemplateOptions,
|
||||||
selectTemplate, bindTemplatePicker, bindFilingMetadata, loadRepositoryPage,
|
selectTemplate, bindTemplatePicker, loadRepositoryPage,
|
||||||
searchRepositories, searchBlockers, findDuplicates,
|
searchRepositories, searchBlockers, findDuplicates,
|
||||||
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
||||||
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,6 @@
|
||||||
let issueCapture = null;
|
let issueCapture = null;
|
||||||
let issueOwnerPicker = null;
|
let issueOwnerPicker = null;
|
||||||
let issueTemplatePicker = null;
|
let issueTemplatePicker = null;
|
||||||
let issueFilingMetadata = null;
|
|
||||||
let updateFollowUp = null;
|
let updateFollowUp = null;
|
||||||
const unfiledAttachmentStore = 'indexedDB' in window ? createUnfiledAttachmentStore() : null;
|
const unfiledAttachmentStore = 'indexedDB' in window ? createUnfiledAttachmentStore() : null;
|
||||||
const unfiledCaptures = createUnfiledCaptures({
|
const unfiledCaptures = createUnfiledCaptures({
|
||||||
|
|
@ -658,15 +657,6 @@
|
||||||
setDraft:draft=>{ qs('#create-issue-title').value=draft.title; qs('#create-issue-body').value=draft.body; },
|
setDraft:draft=>{ qs('#create-issue-title').value=draft.title; qs('#create-issue-body').value=draft.body; },
|
||||||
changed:()=>{ saveIssueCaptureDraft(); scheduleIssueDuplicateCheck(); },
|
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 });
|
updateFollowUp = createUpdateFollowUp({ storage:localStorage, getLogin:()=>confirmedOwnerLogin });
|
||||||
}
|
}
|
||||||
if (!sharedLaunchHandled && Object.values(sharedLaunch).some(Boolean)) {
|
if (!sharedLaunchHandled && Object.values(sharedLaunch).some(Boolean)) {
|
||||||
|
|
@ -3811,7 +3801,8 @@
|
||||||
qs('#create-issue-repository-results').hidden = true;
|
qs('#create-issue-repository-results').hidden = true;
|
||||||
qs('#create-issue-repository-status').textContent = 'Selected ' + repository + '.';
|
qs('#create-issue-repository-status').textContent = 'Selected ' + repository + '.';
|
||||||
issueOwnerPicker.reset(repository);
|
issueOwnerPicker.reset(repository);
|
||||||
loadIssueFilingMetadata(repository);
|
loadIssueLabels(repository);
|
||||||
|
loadIssueMilestones(repository);
|
||||||
saveIssueCaptureDraft();
|
saveIssueCaptureDraft();
|
||||||
scheduleIssueDuplicateCheck();
|
scheduleIssueDuplicateCheck();
|
||||||
updateIssueCreateActions();
|
updateIssueCreateActions();
|
||||||
|
|
@ -3868,8 +3859,57 @@
|
||||||
}, 350);
|
}, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadIssueFilingMetadata(repository, selected = {}) {
|
async function loadIssueLabels(repository, selectedIds = []) {
|
||||||
return issueFilingMetadata.load(repository, selected);
|
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.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSharedLaunchUrl() {
|
function clearSharedLaunchUrl() {
|
||||||
|
|
@ -3924,7 +3964,9 @@
|
||||||
renderIssueCaptureBlockers(captureDraft.blockers || []);
|
renderIssueCaptureBlockers(captureDraft.blockers || []);
|
||||||
qs('#create-issue-blocker-search').value = '';
|
qs('#create-issue-blocker-search').value = '';
|
||||||
qs('#create-issue-blocker-results').hidden = true;
|
qs('#create-issue-blocker-results').hidden = true;
|
||||||
loadIssueFilingMetadata(qs('#create-issue-repository').value, captureDraft);
|
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);
|
||||||
scheduleIssueDuplicateCheck();
|
scheduleIssueDuplicateCheck();
|
||||||
qs('#create-issue-status').textContent = issueCaptureRepositories.length ? '' : 'No accessible repositories are available.';
|
qs('#create-issue-status').textContent = issueCaptureRepositories.length ? '' : 'No accessible repositories are available.';
|
||||||
qs('#create-follow-up-next').hidden = !updateFollowUp?.source();
|
qs('#create-follow-up-next').hidden = !updateFollowUp?.source();
|
||||||
|
|
@ -5113,7 +5155,8 @@
|
||||||
qs('#create-issue-repository').addEventListener('change', event => {
|
qs('#create-issue-repository').addEventListener('change', event => {
|
||||||
issueTemplatePicker.changeRepository(currentIssueCaptureDraft());
|
issueTemplatePicker.changeRepository(currentIssueCaptureDraft());
|
||||||
issueOwnerPicker.reset(event.target.value);
|
issueOwnerPicker.reset(event.target.value);
|
||||||
loadIssueFilingMetadata(event.target.value);
|
loadIssueLabels(event.target.value).then(() => loadIssueTemplates(event.target.value));
|
||||||
|
loadIssueMilestones(event.target.value);
|
||||||
qs('#create-issue-repository-search').value = event.target.value;
|
qs('#create-issue-repository-search').value = event.target.value;
|
||||||
saveIssueCaptureDraft();
|
saveIssueCaptureDraft();
|
||||||
scheduleIssueDuplicateCheck();
|
scheduleIssueDuplicateCheck();
|
||||||
|
|
|
||||||
45
src/main.py
45
src/main.py
|
|
@ -4484,51 +4484,6 @@ async def update_assigned_issue_release_plan(
|
||||||
return JSONResponse(result)
|
return JSONResponse(result)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/repos/{owner}/{repo}/issue-filing-metadata")
|
|
||||||
async def repository_issue_filing_metadata(owner: str, repo: str):
|
|
||||||
repository = f"{owner}/{repo}"
|
|
||||||
|
|
||||||
async def load_metadata():
|
|
||||||
if await gitea_proxy.repository_access(repository) is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Repository not found")
|
|
||||||
names = ("labels", "milestones", "templates")
|
|
||||||
loaders = (
|
|
||||||
gitea_proxy.repo_labels(repository),
|
|
||||||
gitea_proxy.repo_milestones(repository),
|
|
||||||
gitea_proxy.repo_issue_templates(repository),
|
|
||||||
)
|
|
||||||
results = await asyncio.gather(*loaders, return_exceptions=True)
|
|
||||||
messages = {
|
|
||||||
"labels": "Labels could not be loaded.",
|
|
||||||
"milestones": "Milestones could not be loaded.",
|
|
||||||
"templates": "Issue types could not be loaded.",
|
|
||||||
}
|
|
||||||
payload = {}
|
|
||||||
for name, result in zip(names, results):
|
|
||||||
if isinstance(result, BaseException):
|
|
||||||
payload[name] = {
|
|
||||||
"available": False,
|
|
||||||
"items": [],
|
|
||||||
"error": messages[name],
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
payload[name] = {"available": True, "items": result}
|
|
||||||
return JSONResponse(payload, headers={"Cache-Control": "no-store"})
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
load_metadata(), timeout=ISSUE_ACTION_TIMEOUT_SECONDS
|
|
||||||
)
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": "Issue filing details could not be loaded. Your draft is safe; please retry."},
|
|
||||||
status_code=503,
|
|
||||||
headers={"Retry-After": "1", "Cache-Control": "no-store"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/repos/{owner}/{repo}/milestones")
|
@app.get("/api/v1/repos/{owner}/{repo}/milestones")
|
||||||
async def repository_milestones(owner: str, repo: str):
|
async def repository_milestones(owner: str, repo: str):
|
||||||
repository = f"{owner}/{repo}"
|
repository = f"{owner}/{repo}"
|
||||||
|
|
|
||||||
|
|
@ -62,67 +62,6 @@ async def test_repository_issue_templates_route_checks_access_and_returns_blank_
|
||||||
assert calls == [("access", "stackchain/api"), ("templates", "stackchain/api")]
|
assert calls == [("access", "stackchain/api"), ("templates", "stackchain/api")]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_issue_filing_metadata_authorizes_once_and_isolates_concurrent_section_failures(monkeypatch):
|
|
||||||
calls = []
|
|
||||||
started = set()
|
|
||||||
all_started = asyncio.Event()
|
|
||||||
|
|
||||||
async def access(repository):
|
|
||||||
calls.append(("access", repository))
|
|
||||||
return {"full_name": repository}
|
|
||||||
|
|
||||||
async def section(name, result=None, error=None):
|
|
||||||
calls.append((name, "stackchain/api"))
|
|
||||||
started.add(name)
|
|
||||||
if len(started) == 3:
|
|
||||||
all_started.set()
|
|
||||||
await asyncio.wait_for(all_started.wait(), timeout=0.2)
|
|
||||||
if error:
|
|
||||||
raise error
|
|
||||||
return result
|
|
||||||
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "repository_access", access)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
main.gitea_proxy, "repo_labels",
|
|
||||||
lambda repository: section("labels", [{"id": 3, "name": "P0"}]),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
main.gitea_proxy, "repo_milestones",
|
|
||||||
lambda repository: section("milestones", [{"id": 9, "title": "August RC"}]),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
main.gitea_proxy, "repo_issue_templates",
|
|
||||||
lambda repository: section("templates", error=RuntimeError("templates unavailable")),
|
|
||||||
)
|
|
||||||
|
|
||||||
transport = httpx.ASGITransport(app=main.app)
|
|
||||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
||||||
response = await client.get(
|
|
||||||
"/api/v1/repos/stackchain/api/issue-filing-metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == {
|
|
||||||
"labels": {"available": True, "items": [{"id": 3, "name": "P0"}]},
|
|
||||||
"milestones": {
|
|
||||||
"available": True,
|
|
||||||
"items": [{"id": 9, "title": "August RC"}],
|
|
||||||
},
|
|
||||||
"templates": {
|
|
||||||
"available": False,
|
|
||||||
"items": [],
|
|
||||||
"error": "Issue types could not be loaded.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert calls.count(("access", "stackchain/api")) == 1
|
|
||||||
assert set(calls[1:]) == {
|
|
||||||
("labels", "stackchain/api"),
|
|
||||||
("milestones", "stackchain/api"),
|
|
||||||
("templates", "stackchain/api"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_owned_comment_mutations_verify_thread_and_author_before_writing():
|
async def test_owned_comment_mutations_verify_thread_and_author_before_writing():
|
||||||
requests = []
|
requests = []
|
||||||
|
|
|
||||||
|
|
@ -1844,7 +1844,7 @@ async def test_new_issue_sheet_exposes_touch_safe_release_planning_controls():
|
||||||
assert 'id="create-issue-due-date" type="date"' in html
|
assert 'id="create-issue-due-date" type="date"' in html
|
||||||
assert 'id="create-issue-milestone-status" aria-live="polite"' in html
|
assert 'id="create-issue-milestone-status" aria-live="polite"' in html
|
||||||
assert '.create-issue-form select, .create-issue-form input[type="date"] { min-height:44px;' in html
|
assert '.create-issue-form select, .create-issue-form input[type="date"] { min-height:44px;' in html
|
||||||
assert 'issueFilingMetadata.load(repository, selected)' in html
|
assert 'issueCapture.loadMilestones(repository)' in html
|
||||||
assert "issueOwnerPicker.draft(selectedIssueLabelIds(), issueCaptureBlockers" in html
|
assert "issueOwnerPicker.draft(selectedIssueLabelIds(), issueCaptureBlockers" in html
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -4157,110 +4157,14 @@ async def test_mobile_issue_capture_lazily_selects_an_initial_owner():
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_issue_filing_exposes_repository_issue_types_without_blocking_blank_filing():
|
async def test_mobile_issue_filing_exposes_repository_issue_types_without_blocking_blank_filing():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
source = CREATE_ISSUE_SHEET.read_text()
|
|
||||||
assert 'id="create-issue-template"' in html
|
assert 'id="create-issue-template"' in html
|
||||||
assert '<option value="">Blank issue</option>' in html
|
assert '<option value="">Blank issue</option>' in html
|
||||||
assert 'id="create-issue-template-status" class="small" aria-live="polite"' in html
|
assert 'id="create-issue-template-status" class="small" aria-live="polite"' in html
|
||||||
assert 'templatePicker.render(repository, metadata.templates' in source
|
assert 'issueTemplatePicker.load(repository' in html
|
||||||
assert 'issueCapture.bindTemplatePicker(' in html
|
assert 'issueCapture.bindTemplatePicker(' in html
|
||||||
assert 'id="issue-filing-review-template"' in html
|
assert 'id="issue-filing-review-template"' in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_every_mobile_repository_selection_uses_the_resilient_metadata_loader():
|
|
||||||
html = await dashboard()
|
|
||||||
source = CREATE_ISSUE_SHEET.read_text()
|
|
||||||
|
|
||||||
assert "async function loadIssueFilingMetadata(repository" in html
|
|
||||||
assert "issueCapture.bindFilingMetadata(" in html
|
|
||||||
assert "issueFilingMetadata.load(repository, selected)" in html
|
|
||||||
assert "const metadata = await loadFilingMetadata(repository)" in source
|
|
||||||
assert "renderLabels(repository, metadata.labels" in source
|
|
||||||
assert "renderMilestones(repository, metadata.milestones" in source
|
|
||||||
assert "templatePicker.render(repository, metadata.templates" in source
|
|
||||||
assert html.count("loadIssueFilingMetadata(") == 4
|
|
||||||
assert "loadIssueLabels(repository);\n loadIssueMilestones(repository);" not in html
|
|
||||||
assert "loadIssueLabels(event.target.value).then" not in html
|
|
||||||
assert "loadIssueLabels(qs('#create-issue-repository').value" not in html
|
|
||||||
|
|
||||||
|
|
||||||
def test_issue_capture_loads_all_filing_metadata_in_one_section_isolated_request():
|
|
||||||
script = f"""
|
|
||||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
|
||||||
const calls = [];
|
|
||||||
const capture = createIssueCapture({{
|
|
||||||
storage:{{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
|
|
||||||
fetchJson:url=>{{
|
|
||||||
calls.push(url);
|
|
||||||
return Promise.resolve({{
|
|
||||||
labels:{{available:true,items:[{{id:3,name:'P0'}}]}},
|
|
||||||
milestones:{{available:false,items:[],error:'Milestones could not be loaded.'}},
|
|
||||||
templates:{{available:true,items:[{{id:'bug',name:'Bug report'}}]}},
|
|
||||||
}});
|
|
||||||
}},
|
|
||||||
}});
|
|
||||||
capture.loadFilingMetadata('stackchain/api').then(result=>
|
|
||||||
process.stdout.write(JSON.stringify({{calls,result}})));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(
|
|
||||||
["node", "-e", script], check=True, capture_output=True, text=True
|
|
||||||
)
|
|
||||||
output = json.loads(result.stdout)
|
|
||||||
|
|
||||||
assert output == {
|
|
||||||
"calls": ["api/v1/repos/stackchain/api/issue-filing-metadata"],
|
|
||||||
"result": {
|
|
||||||
"status": "ready",
|
|
||||||
"repository": "stackchain/api",
|
|
||||||
"labels": {"available": True, "items": [{"id": 3, "name": "P0"}]},
|
|
||||||
"milestones": {
|
|
||||||
"available": False,
|
|
||||||
"items": [],
|
|
||||||
"error": "Milestones could not be loaded.",
|
|
||||||
},
|
|
||||||
"templates": {
|
|
||||||
"available": True,
|
|
||||||
"items": [{"id": "bug", "name": "Bug report"}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_issue_capture_rejects_filing_metadata_from_a_stale_repository_request():
|
|
||||||
script = f"""
|
|
||||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
|
||||||
const releases = {{}};
|
|
||||||
const capture = createIssueCapture({{
|
|
||||||
storage:{{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
|
|
||||||
fetchJson:url=>new Promise(resolve=>{{ releases[url] = resolve; }}),
|
|
||||||
}});
|
|
||||||
const first = capture.loadFilingMetadata('stackchain/old');
|
|
||||||
const second = capture.loadFilingMetadata('stackchain/current');
|
|
||||||
const payload = {{
|
|
||||||
labels:{{available:true,items:[]}}, milestones:{{available:true,items:[]}},
|
|
||||||
templates:{{available:true,items:[]}},
|
|
||||||
}};
|
|
||||||
releases['api/v1/repos/stackchain/current/issue-filing-metadata'](payload);
|
|
||||||
Promise.resolve().then(()=>{{
|
|
||||||
releases['api/v1/repos/stackchain/old/issue-filing-metadata'](payload);
|
|
||||||
return Promise.all([first, second]);
|
|
||||||
}}).then(results=>process.stdout.write(JSON.stringify(results)));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(
|
|
||||||
["node", "-e", script], check=True, capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert json.loads(result.stdout) == [
|
|
||||||
{"status": "stale", "repository": "stackchain/old"},
|
|
||||||
{
|
|
||||||
"status": "ready", "repository": "stackchain/current",
|
|
||||||
"labels": {"available": True, "items": []},
|
|
||||||
"milestones": {"available": True, "items": []},
|
|
||||||
"templates": {"available": True, "items": []},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_assigned_issue_sheet_exposes_touch_sized_content_editor():
|
async def test_mobile_assigned_issue_sheet_exposes_touch_sized_content_editor():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -6547,7 +6451,7 @@ async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet(
|
||||||
assert 'createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage })' in html
|
assert 'createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage })' in html
|
||||||
assert 'lastMyWork = buildMyWork(lastContextSnapshot);' in html
|
assert 'lastMyWork = buildMyWork(lastContextSnapshot);' in html
|
||||||
assert 'openRoutedWork(created' in html
|
assert 'openRoutedWork(created' in html
|
||||||
assert 'issueCapture.bindFilingMetadata({' in html
|
assert 'issueCapture.loadLabels(repository)' in html
|
||||||
assert "input[name=\"create-issue-label\"]:checked" in html
|
assert "input[name=\"create-issue-label\"]:checked" in html
|
||||||
assert '.create-issue-label-option' in html and 'min-height:44px' in html
|
assert '.create-issue-label-option' in html and 'min-height:44px' in html
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user