feat: hand off assigned issues from mobile (#281)
This commit is contained in:
1 parent
1fdf6f417d
commit
04bb6839ae
8 files changed
+471
-3
No files matched your search
@@ -189,6 +189,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.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; }
|
||||
.issue-handoff { margin-top:14px; padding:12px; border:1px solid #2a496e; border-radius:12px; }
|
||||
.issue-handoff > div { display:grid; gap:8px; margin-top:10px; }
|
||||
.issue-handoff select { width:100%; max-width:100%; padding:8px; border:1px solid #1f3a5f; border-radius:8px; background:#0b1526; color:var(--text); }
|
||||
.issue-handoff select, .issue-handoff button { min-height:44px; }
|
||||
.issue-retry { min-height:44px; width:100%; margin-top:10px; }
|
||||
.new-issue { min-height:44px; }
|
||||
.find-work-action { min-height:44px; }
|
||||
@@ -519,6 +523,16 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div id="issue-milestone-status" class="small" aria-live="assertive">Expand planning controls to load milestones.</div>
|
||||
</fieldset>
|
||||
</details>
|
||||
<details class="issue-handoff" id="issue-handoff">
|
||||
<summary>Hand off to teammate</summary>
|
||||
<div>
|
||||
<button id="load-issue-handoff" type="button">Choose teammate</button>
|
||||
<label for="issue-handoff-recipient" class="small">Eligible repository assignee</label>
|
||||
<select id="issue-handoff-recipient" disabled><option value="">Select a teammate</option></select>
|
||||
<button id="confirm-issue-handoff" type="button" disabled>Confirm handoff</button>
|
||||
<div id="issue-handoff-status" class="small" aria-live="assertive">Load teammates to transfer ownership.</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="issue-sheet-actions">
|
||||
<button class="share-work-route" type="button">Share</button>
|
||||
<button id="release-issue" type="button">Release assignment</button>
|
||||
@@ -1803,6 +1817,12 @@ 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-handoff').open = false;
|
||||
qs('#issue-handoff-recipient').innerHTML = '<option value="">Select a teammate</option>';
|
||||
qs('#issue-handoff-recipient').disabled = true;
|
||||
qs('#confirm-issue-handoff').disabled = true;
|
||||
qs('#load-issue-handoff').disabled = false;
|
||||
qs('#issue-handoff-status').textContent = 'Load teammates to transfer ownership.';
|
||||
qs('#issue-planning').open = false;
|
||||
qs('#retry-issue-planning').hidden = true;
|
||||
qs('#issue-label-list').textContent = '';
|
||||
@@ -3130,6 +3150,60 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
button.focus();
|
||||
}
|
||||
});
|
||||
qs('#load-issue-handoff').addEventListener('click', async () => {
|
||||
if (!selectedIssue) return;
|
||||
const button = qs('#load-issue-handoff');
|
||||
const select = qs('#issue-handoff-recipient');
|
||||
button.disabled = true;
|
||||
qs('#issue-handoff-status').textContent = 'Loading eligible teammates…';
|
||||
try {
|
||||
const candidates = await issueController.loadHandoffCandidates(selectedIssue);
|
||||
select.textContent = '';
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = candidates.length ? 'Select a teammate' : 'No eligible teammates';
|
||||
select.appendChild(placeholder);
|
||||
candidates.forEach(candidate => {
|
||||
const option = document.createElement('option');
|
||||
option.value = candidate.login;
|
||||
option.textContent = candidate.name + (candidate.name === candidate.login ? '' : ' (@' + candidate.login + ')');
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.disabled = !candidates.length;
|
||||
qs('#confirm-issue-handoff').disabled = true;
|
||||
qs('#issue-handoff-status').textContent = candidates.length ?
|
||||
'Choose who should own this issue next.' : 'No other eligible assignees were found.';
|
||||
if (candidates.length) select.focus();
|
||||
} catch (error) {
|
||||
qs('#issue-handoff-status').textContent = error.message + ' Retry loading teammates.';
|
||||
button.disabled = false;
|
||||
button.focus();
|
||||
}
|
||||
});
|
||||
qs('#issue-handoff-recipient').addEventListener('change', event => {
|
||||
qs('#confirm-issue-handoff').disabled = !event.target.value;
|
||||
});
|
||||
qs('#confirm-issue-handoff').addEventListener('click', async () => {
|
||||
const recipient = qs('#issue-handoff-recipient').value;
|
||||
if (!selectedIssue || !recipient || !window.confirm('Hand off ' + selectedIssue.key + ' to @' + recipient + '?')) return;
|
||||
const handingOff = selectedIssue;
|
||||
const button = qs('#confirm-issue-handoff');
|
||||
button.disabled = true;
|
||||
qs('#issue-handoff-status').textContent = 'Confirming handoff…';
|
||||
try {
|
||||
await issueController.handoff(selectedIssue, recipient, lastContextSnapshot?.user?.login);
|
||||
lastContextSnapshot = buildMyWork.removeIssue(
|
||||
lastContextSnapshot, handingOff.repository, handingOff.number
|
||||
);
|
||||
closeIssueSheet();
|
||||
paintMyWork(lastContextSnapshot);
|
||||
qs('#my-work-action-status').textContent = handingOff.key + ' handed off to @' + recipient + '.';
|
||||
} catch (error) {
|
||||
qs('#issue-handoff-status').textContent = error.message + ' The issue remains in My Work; retry.';
|
||||
button.disabled = false;
|
||||
button.focus();
|
||||
}
|
||||
});
|
||||
qs('#close-issue').addEventListener('click', async () => {
|
||||
if (!selectedIssue || !window.confirm('Close ' + selectedIssue.key + '?')) return;
|
||||
const closing = selectedIssue;
|
||||
|
||||
@@ -22,6 +22,7 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
let commentRequest = null;
|
||||
let closeRequest = null;
|
||||
let releaseRequest = null;
|
||||
let handoffRequest = null;
|
||||
let labelRequest = null;
|
||||
let editRequest = null;
|
||||
let dueDateRequest = null;
|
||||
@@ -61,6 +62,11 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
loadHandoffCandidates(item) {
|
||||
return fetchJson(issuePath(item) + '/handoff-candidates', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
loadDraft(item) {
|
||||
try { return storage?.getItem(draftKey(item)) || ''; }
|
||||
catch (_error) { return ''; }
|
||||
@@ -209,6 +215,26 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
}).finally(() => { releaseRequest = null; });
|
||||
return releaseRequest;
|
||||
},
|
||||
handoff(item, recipient, currentLogin) {
|
||||
if (handoffRequest) return handoffRequest;
|
||||
handoffRequest = fetchJson(issuePath(item) + '/handoff', {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ recipient }),
|
||||
}).then(result => {
|
||||
if (
|
||||
result?.number !== item.number ||
|
||||
result?.recipient !== recipient ||
|
||||
!Array.isArray(result.assignees) ||
|
||||
!result.assignees.includes(recipient) ||
|
||||
result.assignees.includes(currentLogin)
|
||||
) {
|
||||
throw new Error('Issue handoff was not confirmed.');
|
||||
}
|
||||
return result;
|
||||
}).finally(() => { handoffRequest = null; });
|
||||
return handoffRequest;
|
||||
},
|
||||
comment(item, body) {
|
||||
if (commentRequest) return commentRequest;
|
||||
this.saveDraft(item, body);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v20';
|
||||
const CACHE = 'stackchain-dashboard-shell-v21';
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const SHELL = [
|
||||
BASE,
|
||||
|
||||
Reference in new issue
Block a user