feat: plan issues during mobile capture (#221)
This commit is contained in:
1 parent
ce26f54618
commit
19dfc93b85
7 files changed
+280
-7
No files matched your search
@@ -15,18 +15,25 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
(Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0)
|
||||
)).slice(0, 20);
|
||||
const emptyDraft = () => ({ repository: '', title: '', body: '', labelIds: [] });
|
||||
const safeMilestoneId = value => Number.isInteger(Number(value)) && Number(value) > 0 ? Number(value) : null;
|
||||
const safeDueDate = value => /^\d{4}-\d{2}-\d{2}$/.test(String(value || '')) ? String(value) : '';
|
||||
|
||||
function loadStored() {
|
||||
try {
|
||||
const parsed = JSON.parse(storage.getItem(storageKey) || 'null');
|
||||
if (!parsed || typeof parsed !== 'object') return {...emptyDraft(), operationId: ''};
|
||||
return {
|
||||
const draft = {
|
||||
repository: String(parsed.repository || ''),
|
||||
title: String(parsed.title || ''),
|
||||
body: String(parsed.body || ''),
|
||||
labelIds: safeLabelIds(parsed.labelIds),
|
||||
operationId: String(parsed.operationId || '').slice(0, 128),
|
||||
};
|
||||
const milestoneId = safeMilestoneId(parsed.milestoneId);
|
||||
const dueDate = safeDueDate(parsed.dueDate);
|
||||
if (milestoneId !== null) draft.milestoneId = milestoneId;
|
||||
if (dueDate) draft.dueDate = dueDate;
|
||||
return draft;
|
||||
} catch (_error) {
|
||||
return {...emptyDraft(), operationId: ''};
|
||||
}
|
||||
@@ -45,7 +52,12 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
body: String(draft?.body || ''),
|
||||
labelIds: safeLabelIds(draft?.labelIds),
|
||||
};
|
||||
const unchanged = ['repository', 'title', 'body'].every(key => previous[key] === safe[key]) &&
|
||||
const milestoneId = safeMilestoneId(draft?.milestoneId);
|
||||
const dueDate = safeDueDate(draft?.dueDate);
|
||||
if (milestoneId !== null) safe.milestoneId = milestoneId;
|
||||
if (dueDate) safe.dueDate = dueDate;
|
||||
const unchanged = ['repository', 'title', 'body', 'milestoneId', 'dueDate']
|
||||
.every(key => (previous[key] || '') === (safe[key] || '')) &&
|
||||
JSON.stringify(previous.labelIds) === JSON.stringify(safe.labelIds);
|
||||
writeStored({...safe, operationId: unchanged ? previous.operationId : ''});
|
||||
return safe;
|
||||
@@ -73,6 +85,13 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
);
|
||||
}
|
||||
|
||||
function loadMilestones(repository) {
|
||||
const encoded = String(repository || '').split('/').map(encodeURIComponent).join('/');
|
||||
return fetchJson('api/v1/repos/' + encoded + '/milestones').then(milestones =>
|
||||
Array.isArray(milestones) ? milestones : []
|
||||
);
|
||||
}
|
||||
|
||||
function submit(draft) {
|
||||
if (pending) return pending;
|
||||
const saved = saveDraft(draft);
|
||||
@@ -88,6 +107,8 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: saved.title, body: saved.body, label_ids: saved.labelIds,
|
||||
...(saved.milestoneId ? {milestone_id: saved.milestoneId} : {}),
|
||||
...(saved.dueDate ? {due_date: saved.dueDate + 'T23:59:59Z'} : {}),
|
||||
}),
|
||||
}).then(issue => {
|
||||
clearDraft();
|
||||
@@ -96,7 +117,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
return pending;
|
||||
}
|
||||
|
||||
return { saveDraft, loadDraft, loadLabels, submit };
|
||||
return { saveDraft, loadDraft, loadLabels, loadMilestones, submit };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture;
|
||||
+43
-2
@@ -206,7 +206,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.create-issue-header button, .create-issue-actions button { min-height:44px; }
|
||||
.create-issue-form { display:grid; gap:12px; }
|
||||
.create-issue-form label { display:grid; gap:6px; }
|
||||
.create-issue-form select { min-height:44px; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:#e5e7eb; }
|
||||
.create-issue-form select, .create-issue-form input[type="date"] { min-height:44px; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:#e5e7eb; }
|
||||
.create-issue-labels { display:grid; gap:8px; margin:0; padding:0; border:0; }
|
||||
.create-issue-label-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:8px; }
|
||||
.create-issue-label-option { min-height:44px; display:flex !important; grid-template-columns:auto 1fr !important; align-items:center; gap:8px; padding:8px 10px; border:1px solid #2a496e; border-radius:10px; background:#10213a; }
|
||||
@@ -519,6 +519,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<label for="create-issue-body">Description <span class="small">Optional</span>
|
||||
<textarea id="create-issue-body" maxlength="10000"></textarea>
|
||||
</label>
|
||||
<label for="create-issue-milestone">Milestone <span class="small">Optional</span>
|
||||
<select id="create-issue-milestone" aria-describedby="create-issue-milestone-status">
|
||||
<option value="">No milestone</option>
|
||||
</select>
|
||||
<span class="small" id="create-issue-milestone-status" aria-live="polite">Choose a repository to load milestones.</span>
|
||||
</label>
|
||||
<label for="create-issue-due-date">Due date <span class="small">Optional</span>
|
||||
<input id="create-issue-due-date" type="date" />
|
||||
</label>
|
||||
<fieldset class="create-issue-labels" id="create-issue-labels" aria-describedby="create-issue-label-status">
|
||||
<legend>Labels <span class="small">Optional</span></legend>
|
||||
<div class="small" id="create-issue-label-status" aria-live="polite">Choose a repository to load labels.</div>
|
||||
@@ -1757,6 +1766,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
title: qs('#create-issue-title').value,
|
||||
body: qs('#create-issue-body').value,
|
||||
labelIds: selectedIssueLabelIds(),
|
||||
milestoneId: Number(qs('#create-issue-milestone').value) || null,
|
||||
dueDate: qs('#create-issue-due-date').value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1783,6 +1794,30 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
}
|
||||
|
||||
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 openCreateIssueSheet() {
|
||||
const captureDraft = issueCapture.loadDraft();
|
||||
const repositories = (lastContextSnapshot?.repos || []).map(repository => repository.full_name).filter(Boolean);
|
||||
@@ -1795,7 +1830,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
if (captureDraft.repository) qs('#create-issue-repository').value = captureDraft.repository;
|
||||
qs('#create-issue-title').value = captureDraft.title;
|
||||
qs('#create-issue-body').value = captureDraft.body;
|
||||
qs('#create-issue-due-date').value = captureDraft.dueDate || '';
|
||||
loadIssueLabels(qs('#create-issue-repository').value, captureDraft.labelIds);
|
||||
loadIssueMilestones(qs('#create-issue-repository').value, captureDraft.milestoneId);
|
||||
qs('#create-issue-status').textContent = repositories.length ? '' : 'No accessible repositories are available.';
|
||||
qs('#submit-new-issue').disabled = !repositories.length;
|
||||
qs('#create-issue-sheet').classList.add('open');
|
||||
@@ -2367,14 +2404,16 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
saveIssueCaptureDraft();
|
||||
closeCreateIssueSheet();
|
||||
});
|
||||
['#create-issue-title', '#create-issue-body'].forEach(selector =>
|
||||
['#create-issue-title', '#create-issue-body', '#create-issue-due-date'].forEach(selector =>
|
||||
qs(selector).addEventListener('input', saveIssueCaptureDraft)
|
||||
);
|
||||
qs('#create-issue-repository').addEventListener('change', event => {
|
||||
loadIssueLabels(event.target.value);
|
||||
loadIssueMilestones(event.target.value);
|
||||
saveIssueCaptureDraft();
|
||||
});
|
||||
qs('#create-issue-label-list').addEventListener('change', saveIssueCaptureDraft);
|
||||
qs('#create-issue-milestone').addEventListener('change', saveIssueCaptureDraft);
|
||||
qs('#create-issue-form').addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const captureDraft = {
|
||||
@@ -2382,6 +2421,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
title: qs('#create-issue-title').value.trim(),
|
||||
body: qs('#create-issue-body').value.trim(),
|
||||
labelIds: selectedIssueLabelIds(),
|
||||
milestoneId: Number(qs('#create-issue-milestone').value) || null,
|
||||
dueDate: qs('#create-issue-due-date').value,
|
||||
};
|
||||
if (!captureDraft.repository || !captureDraft.title) {
|
||||
qs('#create-issue-status').textContent = 'Choose a repository and add a title.';
|
||||
|
||||
Reference in new issue
Block a user