feat: reprioritize assigned issues from mobile (#173)
This commit is contained in:
1 parent
b673911e36
commit
390ad0d4d2
7 files changed
+418
No files matched your search
@@ -134,6 +134,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.issue-comment { padding:10px 0; border-bottom:1px solid #1b2d45; }
|
||||
.issue-comment-composer { display:grid; gap:8px; margin-top:16px; }
|
||||
.issue-comment-composer button { min-height:44px; width:100%; }
|
||||
.issue-label-editor { max-width:100%; margin:14px 0; padding:12px; border:1px solid #2a496e; border-radius:12px; }
|
||||
.issue-label-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr)); gap:8px; max-width:100%; }
|
||||
.issue-label-option { min-height:44px; max-width:100%; display:flex; align-items:center; gap:10px; padding:8px; border:1px solid #2a496e; border-radius:10px; overflow-wrap:anywhere; }
|
||||
.issue-label-option input { width:20px; height:20px; flex:0 0 auto; }
|
||||
.issue-label-editor button { min-height:44px; width:100%; margin-top:10px; }
|
||||
.issue-sheet-actions { position:sticky; bottom:0; z-index:3; display:grid; gap:8px; margin-top:14px; padding:10px 4px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
|
||||
.issue-sheet-actions button, .issue-sheet-actions a { min-height:44px; display:flex; align-items:center; justify-content:center; }
|
||||
.issue-sheet-actions a { border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||
@@ -315,6 +320,12 @@ 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>Retry loading issue</button>
|
||||
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
||||
<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>
|
||||
<button id="save-issue-labels" type="button" disabled>Save labels</button>
|
||||
</fieldset>
|
||||
<p class="issue-sheet-content" id="issue-sheet-body"></p>
|
||||
<h2>Recent discussion</h2>
|
||||
<div id="issue-comments"></div>
|
||||
@@ -947,6 +958,36 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
escapeHtml(comment.body || 'No comment body provided.') + '</div></div>';
|
||||
}
|
||||
|
||||
async function loadIssueLabelEditor(item, confirmedNames) {
|
||||
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))
|
||||
);
|
||||
list.innerHTML = labels.map(label =>
|
||||
'<label class="issue-label-option"><input type="checkbox" name="issue-label" value="' +
|
||||
Number(label.id) + '"' + (selectedIds.has(Number(label.id)) ? ' checked' : '') + '><span>' +
|
||||
escapeHtml(label.name) + '</span></label>'
|
||||
).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() {
|
||||
return Array.from(document.querySelectorAll('input[name="issue-label"]:checked'))
|
||||
.map(input => Number(input.value)).filter(Number.isInteger);
|
||||
}
|
||||
|
||||
async function openIssueSheet(item, trigger) {
|
||||
if (!item) return;
|
||||
selectedIssue = item;
|
||||
@@ -961,6 +1002,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#issue-comments').textContent = '';
|
||||
qs('#issue-comment').value = issueController.loadDraft(item);
|
||||
qs('#issue-comment-status').textContent = '';
|
||||
qs('#issue-label-list').textContent = '';
|
||||
qs('#issue-label-status').textContent = 'Loading labels…';
|
||||
qs('#save-issue-labels').disabled = true;
|
||||
qs('#retry-issue-load').hidden = true;
|
||||
qs('#open-issue-gitea').href = item.url || '#';
|
||||
qs('#send-issue-comment').disabled = false;
|
||||
@@ -974,6 +1018,7 @@ 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 || []);
|
||||
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
||||
'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported';
|
||||
qs('#issue-comments').innerHTML = (detail.comments || []).length ?
|
||||
@@ -1487,6 +1532,29 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#issue-comment').addEventListener('input', event => {
|
||||
if (selectedIssue) issueController.saveDraft(selectedIssue, event.target.value);
|
||||
});
|
||||
qs('#save-issue-labels').addEventListener('click', async () => {
|
||||
if (!selectedIssue || !lastContextSnapshot) return;
|
||||
const editing = selectedIssue;
|
||||
const button = qs('#save-issue-labels');
|
||||
button.disabled = true;
|
||||
qs('#issue-label-status').textContent = 'Saving labels…';
|
||||
try {
|
||||
const confirmed = await issueController.updateLabels(selectedIssue, selectedEditIssueLabelIds());
|
||||
lastContextSnapshot = buildMyWork.replaceIssueLabels(
|
||||
lastContextSnapshot, editing.repository, editing.number, confirmed.labels
|
||||
);
|
||||
selectedIssue = { ...editing, labels: confirmed.labels };
|
||||
qs('#issue-labels').innerHTML = confirmed.labels.map(label =>
|
||||
'<span class="pill">' + escapeHtml(label) + '</span>'
|
||||
).join(' ');
|
||||
paintMyWork(lastContextSnapshot);
|
||||
qs('#issue-label-status').textContent = 'Labels saved. My Work reprioritized.';
|
||||
} catch (error) {
|
||||
qs('#issue-label-status').textContent = error.message + ' Your selection is safe; retry.';
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
qs('#send-issue-comment').addEventListener('click', async () => {
|
||||
if (!selectedIssue) return;
|
||||
const body = qs('#issue-comment').value.trim();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
function createIssueSheet({ fetchJson, storage }) {
|
||||
let commentRequest = null;
|
||||
let closeRequest = null;
|
||||
let labelRequest = null;
|
||||
const issuePath = item => 'api/v1/repos/' + item.repository.split('/').map(encodeURIComponent).join('/') +
|
||||
'/issues/' + encodeURIComponent(item.number);
|
||||
const draftKey = item => 'stackchain.issue-comment.v1:' + item.repository + '#' + item.number;
|
||||
const labelDraftKey = item => 'stackchain.issue-labels.v1:' + item.repository + '#' + item.number;
|
||||
|
||||
return {
|
||||
load(item) {
|
||||
@@ -11,6 +13,11 @@ function createIssueSheet({ fetchJson, storage }) {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
loadLabels(item) {
|
||||
return fetchJson(issuePath(item) + '/labels', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
loadDraft(item) {
|
||||
try { return storage?.getItem(draftKey(item)) || ''; }
|
||||
catch (_error) { return ''; }
|
||||
@@ -19,6 +26,30 @@ function createIssueSheet({ fetchJson, storage }) {
|
||||
try { storage?.setItem(draftKey(item), body); }
|
||||
catch (_error) { /* The textarea remains the fallback. */ }
|
||||
},
|
||||
loadLabelDraft(item) {
|
||||
try {
|
||||
const value = JSON.parse(storage?.getItem(labelDraftKey(item)) || '[]');
|
||||
return Array.isArray(value) ? value.filter(Number.isInteger) : [];
|
||||
} catch (_error) { return []; }
|
||||
},
|
||||
updateLabels(item, labelIds) {
|
||||
if (labelRequest) return labelRequest;
|
||||
try { storage?.setItem(labelDraftKey(item), JSON.stringify(labelIds)); }
|
||||
catch (_error) { /* The checked controls remain the fallback. */ }
|
||||
labelRequest = fetchJson(issuePath(item) + '/labels', {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ label_ids: labelIds }),
|
||||
}).then(result => {
|
||||
if (result?.number !== item.number || !Array.isArray(result.labels)) {
|
||||
throw new Error('Issue labels were not confirmed.');
|
||||
}
|
||||
try { storage?.removeItem(labelDraftKey(item)); }
|
||||
catch (_error) { /* The upstream label set is authoritative. */ }
|
||||
return result;
|
||||
}).finally(() => { labelRequest = null; });
|
||||
return labelRequest;
|
||||
},
|
||||
close(item) {
|
||||
if (closeRequest) return closeRequest;
|
||||
closeRequest = fetchJson(issuePath(item) + '/close', {
|
||||
|
||||
@@ -326,6 +326,15 @@ function filterMyWork(items, selectedFilter) {
|
||||
return items.filter((item) => item.kind === selectedFilter);
|
||||
}
|
||||
|
||||
function replaceIssueLabels(data, repository, number, labels) {
|
||||
return {
|
||||
...data,
|
||||
issues: (data.issues || []).map(item =>
|
||||
item.repository === repository && item.number === number ? { ...item, labels: [...labels] } : item
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeMyWork(items) {
|
||||
const updates = items.filter((item) => item.has_update).length;
|
||||
const reviews = items.filter((item) => item.is_review).length;
|
||||
@@ -348,6 +357,7 @@ function countMyWork(items) {
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
buildMyWork.filterMyWork = filterMyWork;
|
||||
buildMyWork.replaceIssueLabels = replaceIssueLabels;
|
||||
buildMyWork.summarizeMyWork = summarizeMyWork;
|
||||
buildMyWork.countMyWork = countMyWork;
|
||||
buildMyWork.acknowledgeNotification = acknowledgeNotification;
|
||||
|
||||
Reference in new issue
Block a user