|
|
|
|
@ -142,6 +142,9 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
|
|
|
|
.issue-sheet-header button { min-height:44px; }
|
|
|
|
|
.issue-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
|
|
|
|
|
.issue-planning { max-width:100%; margin-top:16px; border:1px solid #2a496e; border-radius:12px; padding:0 12px 12px; overflow-x:hidden; }
|
|
|
|
|
.issue-planning > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
|
|
|
|
.issue-planning-retry { min-height:44px; width:100%; }
|
|
|
|
|
#edit-issue-content { min-height:44px; width:100%; }
|
|
|
|
|
.issue-edit-form { display:grid; gap:8px; max-width:100%; margin:12px 0; }
|
|
|
|
|
.issue-edit-form label { display:grid; gap:6px; min-width:0; }
|
|
|
|
|
@ -417,10 +420,38 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
<div id="issue-sheet-status" class="small" aria-live="polite">Choose an issue.</div>
|
|
|
|
|
<button class="issue-retry" id="retry-issue-load" type="button" hidden>Reload latest issue</button>
|
|
|
|
|
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
|
|
|
|
<p class="issue-sheet-content" id="issue-sheet-body"></p>
|
|
|
|
|
<h2>Full conversation</h2>
|
|
|
|
|
<div id="issue-comments"></div>
|
|
|
|
|
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
|
|
|
|
<div id="issue-conversation-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
<section class="issue-comment-composer" aria-labelledby="issue-comment-title">
|
|
|
|
|
<h2 id="issue-comment-title">Add comment</h2>
|
|
|
|
|
<textarea id="issue-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
|
|
|
|
<button id="send-issue-comment" type="button">Post comment</button>
|
|
|
|
|
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
</section>
|
|
|
|
|
<details class="issue-planning" id="issue-planning">
|
|
|
|
|
<summary>Plan & edit</summary>
|
|
|
|
|
<button class="issue-planning-retry" id="retry-issue-planning" type="button" hidden>Retry planning controls</button>
|
|
|
|
|
<button id="edit-issue-content" type="button" disabled>Edit issue</button>
|
|
|
|
|
<form class="issue-edit-form" id="issue-edit-form" hidden>
|
|
|
|
|
<label for="issue-edit-title">Title
|
|
|
|
|
<input id="issue-edit-title" type="text" maxlength="255" required autocomplete="off" />
|
|
|
|
|
</label>
|
|
|
|
|
<label for="issue-edit-body">Description
|
|
|
|
|
<textarea id="issue-edit-body" maxlength="10000"></textarea>
|
|
|
|
|
</label>
|
|
|
|
|
<div class="issue-edit-actions">
|
|
|
|
|
<button id="save-issue-content" type="submit">Save issue</button>
|
|
|
|
|
<button id="cancel-issue-content" type="button">Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="issue-edit-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
</form>
|
|
|
|
|
<fieldset class="issue-label-editor" id="issue-label-editor" aria-describedby="issue-label-status">
|
|
|
|
|
<legend>Labels</legend>
|
|
|
|
|
<div class="issue-label-list" id="issue-label-list"></div>
|
|
|
|
|
<div class="small" id="issue-label-status" aria-live="polite">Load an issue to edit labels.</div>
|
|
|
|
|
<div class="small" id="issue-label-status" aria-live="polite">Expand planning controls to load labels.</div>
|
|
|
|
|
<button id="save-issue-labels" type="button" disabled>Save labels</button>
|
|
|
|
|
</fieldset>
|
|
|
|
|
<fieldset class="issue-due-editor" aria-describedby="issue-due-status">
|
|
|
|
|
@ -438,33 +469,9 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
<label for="issue-milestone" class="small">Plan this issue into a release</label>
|
|
|
|
|
<select id="issue-milestone" disabled><option value="">No milestone</option></select>
|
|
|
|
|
<button id="save-issue-milestone" type="button" disabled>Save milestone</button>
|
|
|
|
|
<div id="issue-milestone-status" class="small" aria-live="assertive">Load an issue to plan it.</div>
|
|
|
|
|
<div id="issue-milestone-status" class="small" aria-live="assertive">Expand planning controls to load milestones.</div>
|
|
|
|
|
</fieldset>
|
|
|
|
|
<p class="issue-sheet-content" id="issue-sheet-body"></p>
|
|
|
|
|
<button id="edit-issue-content" type="button" disabled>Edit issue</button>
|
|
|
|
|
<form class="issue-edit-form" id="issue-edit-form" hidden>
|
|
|
|
|
<label for="issue-edit-title">Title
|
|
|
|
|
<input id="issue-edit-title" type="text" maxlength="255" required autocomplete="off" />
|
|
|
|
|
</label>
|
|
|
|
|
<label for="issue-edit-body">Description
|
|
|
|
|
<textarea id="issue-edit-body" maxlength="10000"></textarea>
|
|
|
|
|
</label>
|
|
|
|
|
<div class="issue-edit-actions">
|
|
|
|
|
<button id="save-issue-content" type="submit">Save issue</button>
|
|
|
|
|
<button id="cancel-issue-content" type="button">Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="issue-edit-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
</form>
|
|
|
|
|
<h2>Full conversation</h2>
|
|
|
|
|
<div id="issue-comments"></div>
|
|
|
|
|
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
|
|
|
|
<div id="issue-conversation-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
<section class="issue-comment-composer" aria-labelledby="issue-comment-title">
|
|
|
|
|
<h2 id="issue-comment-title">Add comment</h2>
|
|
|
|
|
<textarea id="issue-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
|
|
|
|
<button id="send-issue-comment" type="button">Post comment</button>
|
|
|
|
|
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
|
|
|
|
</section>
|
|
|
|
|
</details>
|
|
|
|
|
<div class="issue-sheet-actions">
|
|
|
|
|
<button class="share-work-route" type="button">Share</button>
|
|
|
|
|
<button id="release-issue" type="button">Release assignment</button>
|
|
|
|
|
@ -764,6 +771,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
}
|
|
|
|
|
const reviewController = createReviewController({ fetchJson: fetchReviewJson, storage: localStorage });
|
|
|
|
|
const issueController = createIssueSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
|
|
|
|
const planningLoader = createIssueSheet.createPlanningLoader({
|
|
|
|
|
loadLabels: item => issueController.loadLabels(item),
|
|
|
|
|
loadMilestones: item => issueController.loadMilestones(item),
|
|
|
|
|
});
|
|
|
|
|
const issueCapture = createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage });
|
|
|
|
|
const pullController = createPullSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
|
|
|
|
const draftInbox = createDraftInbox({ storage: localStorage });
|
|
|
|
|
@ -1356,15 +1367,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadIssueLabelEditor(item, confirmedNames) {
|
|
|
|
|
function renderIssueLabelEditor(item, confirmedNames, labels) {
|
|
|
|
|
const list = qs('#issue-label-list');
|
|
|
|
|
const status = qs('#issue-label-status');
|
|
|
|
|
list.textContent = '';
|
|
|
|
|
qs('#save-issue-labels').disabled = true;
|
|
|
|
|
status.textContent = 'Loading labels…';
|
|
|
|
|
try {
|
|
|
|
|
const labels = await issueController.loadLabels(item);
|
|
|
|
|
if (selectedIssue !== item) return;
|
|
|
|
|
const draftIds = issueController.loadLabelDraft(item);
|
|
|
|
|
const selectedIds = draftIds.length ? new Set(draftIds) : new Set(
|
|
|
|
|
labels.filter(label => confirmedNames.includes(label.name)).map(label => Number(label.id))
|
|
|
|
|
@ -1376,9 +1382,6 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
).join('');
|
|
|
|
|
status.textContent = labels.length ? 'Choose labels, then save.' : 'This repository has no labels.';
|
|
|
|
|
qs('#save-issue-labels').disabled = false;
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
status.textContent = 'Labels could not be loaded. Retry by reopening this issue.';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedEditIssueLabelIds() {
|
|
|
|
|
@ -1386,16 +1389,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
.map(input => Number(input.value)).filter(Number.isInteger);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadIssueMilestoneEditor(item, confirmedMilestone) {
|
|
|
|
|
function renderIssueMilestoneEditor(item, confirmedMilestone, milestones) {
|
|
|
|
|
const select = qs('#issue-milestone');
|
|
|
|
|
const status = qs('#issue-milestone-status');
|
|
|
|
|
select.innerHTML = '<option value="">No milestone</option>';
|
|
|
|
|
select.disabled = true;
|
|
|
|
|
qs('#save-issue-milestone').disabled = true;
|
|
|
|
|
status.textContent = 'Loading milestones…';
|
|
|
|
|
try {
|
|
|
|
|
const milestones = await issueController.loadMilestones(item);
|
|
|
|
|
if (selectedIssue !== item) return;
|
|
|
|
|
select.innerHTML += milestones.map(milestone =>
|
|
|
|
|
'<option value="' + Number(milestone.id) + '">' + escapeHtml(milestone.title) + '</option>'
|
|
|
|
|
).join('');
|
|
|
|
|
@ -1405,8 +1402,25 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#save-issue-milestone').disabled = false;
|
|
|
|
|
status.textContent = confirmedMilestone ?
|
|
|
|
|
'Planned for ' + confirmedMilestone.title + '.' : 'No milestone set.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadIssuePlanning() {
|
|
|
|
|
const item = selectedIssue;
|
|
|
|
|
const detail = selectedIssueDetail;
|
|
|
|
|
if (!item || !detail) return;
|
|
|
|
|
qs('#retry-issue-planning').hidden = true;
|
|
|
|
|
qs('#issue-label-status').textContent = 'Loading labels…';
|
|
|
|
|
qs('#issue-milestone-status').textContent = 'Loading milestones…';
|
|
|
|
|
try {
|
|
|
|
|
const planning = await planningLoader.open(selectedIssue);
|
|
|
|
|
if (selectedIssue !== item) return;
|
|
|
|
|
renderIssueLabelEditor(item, detail.labels || [], planning.labels);
|
|
|
|
|
renderIssueMilestoneEditor(item, detail.milestone, planning.milestones);
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
status.textContent = 'Milestones could not be loaded. Retry by reopening this issue.';
|
|
|
|
|
if (selectedIssue !== item) return;
|
|
|
|
|
qs('#issue-label-status').textContent = 'Labels could not be loaded.';
|
|
|
|
|
qs('#issue-milestone-status').textContent = 'Milestones could not be loaded.';
|
|
|
|
|
qs('#retry-issue-planning').hidden = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1428,8 +1442,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#issue-conversation-status').textContent = 'Loading newest messages…';
|
|
|
|
|
qs('#issue-comment').value = issueController.loadDraft(item);
|
|
|
|
|
qs('#issue-comment-status').textContent = '';
|
|
|
|
|
qs('#issue-planning').open = false;
|
|
|
|
|
qs('#retry-issue-planning').hidden = true;
|
|
|
|
|
qs('#issue-label-list').textContent = '';
|
|
|
|
|
qs('#issue-label-status').textContent = 'Loading labels…';
|
|
|
|
|
qs('#issue-label-status').textContent = 'Expand planning controls to load labels.';
|
|
|
|
|
qs('#save-issue-labels').disabled = true;
|
|
|
|
|
qs('#issue-due-date').value = '';
|
|
|
|
|
qs('#issue-due-date').disabled = true;
|
|
|
|
|
@ -1439,7 +1455,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#issue-milestone').innerHTML = '<option value="">No milestone</option>';
|
|
|
|
|
qs('#issue-milestone').disabled = true;
|
|
|
|
|
qs('#save-issue-milestone').disabled = true;
|
|
|
|
|
qs('#issue-milestone-status').textContent = 'Loading milestones…';
|
|
|
|
|
qs('#issue-milestone-status').textContent = 'Expand planning controls to load milestones.';
|
|
|
|
|
qs('#retry-issue-load').hidden = true;
|
|
|
|
|
qs('#open-issue-gitea').href = item.url || '#';
|
|
|
|
|
qs('#send-issue-comment').disabled = false;
|
|
|
|
|
@ -1458,8 +1474,6 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#issue-labels').innerHTML = (detail.labels || []).map(label =>
|
|
|
|
|
'<span class="pill">' + escapeHtml(label) + '</span>'
|
|
|
|
|
).join(' ');
|
|
|
|
|
loadIssueLabelEditor(item, detail.labels || []);
|
|
|
|
|
loadIssueMilestoneEditor(item, detail.milestone);
|
|
|
|
|
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
|
|
|
|
'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported';
|
|
|
|
|
renderIssueConversation(issueConversation.snapshot());
|
|
|
|
|
@ -1473,6 +1487,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#clear-issue-due-date').disabled = !detail.due_date;
|
|
|
|
|
qs('#issue-due-status').textContent = detail.due_date ?
|
|
|
|
|
'Due ' + new Date(detail.due_date).toLocaleDateString() : 'No due date set.';
|
|
|
|
|
if (qs('#issue-planning').open) loadIssuePlanning();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (selectedIssue !== item) return;
|
|
|
|
|
qs('#issue-sheet-status').textContent = error.message + ' Retry here or open it in Gitea.';
|
|
|
|
|
@ -2346,6 +2361,10 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
qs('#retry-issue-load').addEventListener('click', () => {
|
|
|
|
|
if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger);
|
|
|
|
|
});
|
|
|
|
|
qs('#issue-planning').addEventListener('toggle', event => {
|
|
|
|
|
if (event.currentTarget.open) loadIssuePlanning();
|
|
|
|
|
});
|
|
|
|
|
qs('#retry-issue-planning').addEventListener('click', loadIssuePlanning);
|
|
|
|
|
qs('#edit-issue-content').addEventListener('click', () => {
|
|
|
|
|
if (!selectedIssue || !selectedIssueDetail?.updated_at) return;
|
|
|
|
|
const draft = issueController.loadEditDraft(selectedIssue) || {
|
|
|
|
|
|