feat: reassign delegated filings from mobile Filed (Closes #895)
This commit is contained in:
1 parent
498d7e772a
commit
3c5c58fc38
8 files changed
+374
-12
No files matched your search
@@ -539,6 +539,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
#issue-sheet.read-only .detail-defer,
|
||||
#issue-sheet.read-only #issue-blockers,
|
||||
#issue-sheet.read-only #retry-issue-comment-actions { display:none; }
|
||||
#issue-sheet.read-only.filed-reassignable #issue-handoff { display:block; }
|
||||
.filed-claim-actions { position:sticky; bottom:0; z-index:4; box-sizing:border-box; width:min(560px,100%); display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:14px; padding:10px 4px calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #4ade80; }
|
||||
.filed-claim-actions[hidden] { display:none; }
|
||||
.filed-claim-actions button { min-width:0; min-height:44px; }
|
||||
|
||||
+34
-8
@@ -3383,7 +3383,10 @@
|
||||
if (!item) return;
|
||||
const readOnly = issueController.readOnly(item);
|
||||
const withdrawable = item.is_filed && !item.is_assigned && !item.is_completed && item.state === 'open';
|
||||
const reassignable = item.is_filed && !item.is_completed && item.state === 'open' &&
|
||||
Array.isArray(item.assignees) && item.assignees.length > 0;
|
||||
qs('#issue-sheet').classList.toggle('read-only', readOnly);
|
||||
qs('#issue-sheet').classList.toggle('filed-reassignable', reassignable);
|
||||
issueDetailPosition.open(workDetailIdentity('issue', item));
|
||||
qs('#issue-planning').inert = false;
|
||||
qs('#issue-handoff').inert = false;
|
||||
@@ -3417,12 +3420,15 @@
|
||||
qs('#issue-comment-title').textContent = readOnly ? 'Add follow-up' : 'Add comment';
|
||||
qs('#issue-comment-status').textContent = '';
|
||||
qs('#issue-handoff').open = false;
|
||||
qs('#issue-handoff-summary').textContent = reassignable ? 'Change delegate' : 'Hand off to teammate';
|
||||
qs('#issue-handoff-recipient').innerHTML = '<option value="">Select a teammate</option>';
|
||||
qs('#issue-handoff-recipient').disabled = true;
|
||||
qs('#confirm-issue-handoff').disabled = true;
|
||||
qs('#confirm-issue-handoff').textContent = workSession.checkpointed(item) ? 'Hand off & next' : 'Confirm handoff';
|
||||
qs('#confirm-issue-handoff').textContent = reassignable ? 'Confirm change' :
|
||||
(workSession.checkpointed(item) ? 'Hand off & next' : 'Confirm handoff');
|
||||
qs('#load-issue-handoff').disabled = false;
|
||||
qs('#issue-handoff-status').textContent = 'Load teammates to transfer ownership.';
|
||||
qs('#issue-handoff-status').textContent = reassignable ?
|
||||
'Load teammates to change the current delegate.' : 'Load teammates to transfer ownership.';
|
||||
qs('#issue-planning').open = false;
|
||||
qs('#retry-issue-planning').hidden = true;
|
||||
qs('#issue-label-list').textContent = '';
|
||||
@@ -5939,22 +5945,24 @@
|
||||
qs('#issue-handoff-status').textContent = 'Loading eligible teammates…';
|
||||
try {
|
||||
const candidates = await issueController.loadHandoffCandidates(selectedIssue);
|
||||
const currentAssignees = new Set(selectedIssue.is_filed ? (selectedIssue.assignees || []) : []);
|
||||
const availableCandidates = candidates.filter(candidate => !currentAssignees.has(candidate.login));
|
||||
select.textContent = '';
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = candidates.length ? 'Select a teammate' : 'No eligible teammates';
|
||||
placeholder.textContent = availableCandidates.length ? 'Select a teammate' : 'No eligible teammates';
|
||||
select.appendChild(placeholder);
|
||||
candidates.forEach(candidate => {
|
||||
availableCandidates.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;
|
||||
select.disabled = !availableCandidates.length;
|
||||
qs('#confirm-issue-handoff').disabled = true;
|
||||
qs('#issue-handoff-status').textContent = candidates.length ?
|
||||
qs('#issue-handoff-status').textContent = availableCandidates.length ?
|
||||
'Choose who should own this issue next.' : 'No other eligible assignees were found.';
|
||||
if (candidates.length) select.focus();
|
||||
if (availableCandidates.length) select.focus();
|
||||
} catch (error) {
|
||||
qs('#issue-handoff-status').textContent = error.message + ' Retry loading teammates.';
|
||||
button.disabled = false;
|
||||
@@ -5966,13 +5974,31 @@
|
||||
});
|
||||
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;
|
||||
if (!selectedIssue || !recipient) return;
|
||||
const changingDelegate = selectedIssue.is_filed && !selectedIssue.is_completed &&
|
||||
Array.isArray(selectedIssue.assignees) && selectedIssue.assignees.length > 0;
|
||||
const previousDelegates = changingDelegate ? selectedIssue.assignees.join(', @') : '';
|
||||
const confirmation = changingDelegate ?
|
||||
'Change delegate from @' + previousDelegates + ' to @' + recipient + ' for ' + selectedIssue.key + '?' :
|
||||
'Hand off ' + selectedIssue.key + ' to @' + recipient + '?';
|
||||
if (!window.confirm(confirmation)) return;
|
||||
const handingOff = selectedIssue;
|
||||
const continuingSession = workSession.checkpointed(handingOff);
|
||||
const button = qs('#confirm-issue-handoff');
|
||||
button.disabled = true;
|
||||
qs('#issue-handoff-status').textContent = 'Confirming handoff…';
|
||||
try {
|
||||
if (changingDelegate) {
|
||||
const confirmed = await issueController.reassign(selectedIssue, recipient);
|
||||
Object.assign(handingOff, { assignees:confirmed.assignees, updated_at:confirmed.updated_at || handingOff.updated_at });
|
||||
selectedIssue = handingOff;
|
||||
if (selectedIssueDetail) selectedIssueDetail.assignees = confirmed.assignees;
|
||||
qs('#issue-assignees').textContent = 'Assigned to ' + recipient;
|
||||
qs('#issue-handoff').open = false;
|
||||
qs('#issue-handoff-status').textContent = 'Delegate changed to @' + recipient + '.';
|
||||
refreshMyWorkView({ reconcileSession:false });
|
||||
return;
|
||||
}
|
||||
await issueController.handoff(selectedIssue, recipient, lastContextSnapshot?.user?.login);
|
||||
lastContextSnapshot = buildMyWork.removeIssue(
|
||||
lastContextSnapshot, handingOff.repository, handingOff.number
|
||||
|
||||
+1
-1
@@ -689,7 +689,7 @@
|
||||
</fieldset>
|
||||
</details>
|
||||
<details class="issue-handoff" id="issue-handoff">
|
||||
<summary>Hand off to teammate</summary>
|
||||
<summary 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>
|
||||
|
||||
+25
-1
@@ -23,6 +23,7 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
let closeRequest = null;
|
||||
let releaseRequest = null;
|
||||
let handoffRequest = null;
|
||||
let reassignRequest = null;
|
||||
let labelRequest = null;
|
||||
let editRequest = null;
|
||||
let dueDateRequest = null;
|
||||
@@ -70,7 +71,8 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
});
|
||||
},
|
||||
loadHandoffCandidates(item) {
|
||||
return fetchJson(issuePath(item) + '/handoff-candidates', {
|
||||
const access = item?.is_filed ? '?access=filed' : '';
|
||||
return fetchJson(issuePath(item) + '/handoff-candidates' + access, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
@@ -262,6 +264,28 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||
}).finally(() => { handoffRequest = null; });
|
||||
return handoffRequest;
|
||||
},
|
||||
reassign(item, recipient) {
|
||||
if (reassignRequest) return reassignRequest;
|
||||
const expectedAssignees = Array.isArray(item?.assignees) ? item.assignees.slice() : [];
|
||||
reassignRequest = fetchJson(issuePath(item) + '/reassign', {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ recipient, expected_assignees: expectedAssignees }),
|
||||
}).then(result => {
|
||||
if (
|
||||
result?.number !== item.number ||
|
||||
result?.recipient !== recipient ||
|
||||
!Array.isArray(result.assignees) ||
|
||||
result.assignees.length !== 1 ||
|
||||
result.assignees[0] !== recipient ||
|
||||
JSON.stringify(result.previous_assignees) !== JSON.stringify(expectedAssignees)
|
||||
) {
|
||||
throw new Error('Issue reassignment was not confirmed.');
|
||||
}
|
||||
return result;
|
||||
}).finally(() => { reassignRequest = null; });
|
||||
return reassignRequest;
|
||||
},
|
||||
comment(item, body) {
|
||||
if (commentRequest) return commentRequest;
|
||||
this.saveDraft(item, body);
|
||||
|
||||
Reference in new issue
Block a user