feat: claim work from unread updates (#321)
CI / lint (pull_request) Successful in 32s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-08 17:19:13 +00:00
1 parent d6c7a495ed
commit 0ea264a942
11 files changed
+244 -7

No files matched your search

+30
View File
@@ -371,6 +371,33 @@
authoredOutbox,
onStatus: message => { qs('#update-reply-status').textContent = message; },
});
const updateOwnership = createUpdateOwnership({
claim: item => fetchReviewJson(
'api/v1/repos/' + item.repository.split('/').map(encodeURIComponent).join('/') +
'/issues/' + encodeURIComponent(item.number) + '/claim',
{ method:'PATCH', headers:{ Accept:'application/json' } }
),
addToday: item => {
const result = todayWork.add(item);
refreshMyWorkView();
return result;
},
onClaimed: item => {
if (!lastContextSnapshot) return;
const issues = (lastContextSnapshot.issues || []).filter(candidate =>
candidate.repository !== item.repository || candidate.number !== item.number
);
lastContextSnapshot = { ...lastContextSnapshot, issues:issues.concat(item) };
paintMyWork(lastContextSnapshot);
},
onState: state => {
const button = qs('#update-ownership-action');
button.hidden = state.action === 'hidden';
button.disabled = state.busy;
button.textContent = state.action === 'today' ? 'Add to Today' : 'Take ownership';
if (state.message) qs('#update-sheet-status').textContent = state.message;
},
});
const notificationReader = createNotificationReader({
load: fetchNotificationDetail,
loadConversation: fetchNotificationConversation,
@@ -390,6 +417,7 @@
qs('#update-reply').value = notificationReplier.loadDraft(item);
qs('#update-reply-status').textContent = '';
qs('#send-update-reply').disabled = false;
qs('#update-ownership-action').hidden = true;
qs('#retry-update-load').hidden = true;
qs('#keep-update-unread').focus();
},
@@ -399,6 +427,7 @@
qs('#update-subject-state').textContent = detail.state || '';
qs('#update-subject-body').textContent = detail.subject_body || 'No subject context was provided.';
qs('#open-update-gitea').href = detail.url || selectedUpdate?.url || '#';
updateOwnership.open(detail, selectedUpdate);
qs('#retry-update-load').hidden = true;
},
onConversation: renderUpdateConversation,
@@ -2700,6 +2729,7 @@
}
});
qs('#keep-update-unread').addEventListener('click', () => closeUpdateSheet(true));
qs('#update-ownership-action').addEventListener('click', () => updateOwnership.act());
qs('#retry-update-load').addEventListener('click', () => {
if (selectedUpdate) notificationReader.open(selectedUpdate, lastMyWork);
});
+2
View File
@@ -378,6 +378,7 @@
<div id="update-reply-status" class="small" aria-live="assertive"></div>
</section>
<div class="update-sheet-actions">
<button id="update-ownership-action" type="button" hidden aria-describedby="update-sheet-status">Take ownership</button>
<button class="share-work-route" type="button">Share</button>
<button id="mark-update-read-next" type="button">Mark read &amp; next</button>
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
@@ -519,6 +520,7 @@
<script src="static/offline-work.js"></script>
<script src="static/my-work.js"></script>
<script src="static/today-work.js"></script>
<script src="static/update-ownership.js"></script>
<script src="static/later-work.js"></script>
<script src="static/detail-defer.js"></script>
<script src="static/pick-work.js"></script>
+2 -1
View File
@@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v34';
const CACHE = 'stackchain-dashboard-shell-v35';
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const SHELL = [
BASE,
@@ -22,6 +22,7 @@ const SHELL = [
BASE + 'static/offline-work.js',
BASE + 'static/my-work.js',
BASE + 'static/today-work.js',
BASE + 'static/update-ownership.js',
BASE + 'static/later-work.js',
BASE + 'static/detail-defer.js',
BASE + 'static/pick-work.js',
+78
View File
@@ -0,0 +1,78 @@
function createUpdateOwnership({ claim, addToday, onClaimed, onState }) {
let detail = null;
let update = null;
let claimedItem = null;
let request = null;
function state(value) {
onState?.(value);
}
function open(nextDetail, nextUpdate) {
detail = nextDetail;
update = nextUpdate;
claimedItem = null;
request = null;
state({
action: nextDetail?.issue?.claimable ? 'claim' : 'hidden',
busy: false,
message: '',
});
}
function act() {
if (request) return request;
if (claimedItem) {
const result = addToday(claimedItem);
const messages = {
added: 'Added to Today.',
exists: 'This issue is already in Today.',
full: 'Today is limited to 5 items. Remove one before adding more.',
unavailable: 'Could not save Today on this device.',
};
state({ action: 'today', busy: false, message: messages[result] || messages.unavailable });
return Promise.resolve(result);
}
if (!detail?.issue?.claimable) return Promise.resolve('hidden');
state({ action: 'claim', busy: true, message: 'Assigning…' });
request = Promise.resolve(claim({
repository: detail.repository,
number: detail.issue.number,
})).then(result => {
claimedItem = {
...result,
repository: detail.repository,
kind: 'issue',
notification_id: update?.notification_id,
updated_at: update?.updated_at || result.updated_at,
has_update: true,
};
detail.issue.claimable = false;
onClaimed?.(claimedItem);
state({ action: 'today', busy: false, message: 'Assigned to you. The update is still unread.' });
return claimedItem;
}).catch(error => {
if (error?.status === 409) {
detail.issue.claimable = false;
state({
action: 'hidden',
busy: false,
message: 'Someone else claimed or closed this issue. The update is still unread.',
});
return 'conflict';
}
state({
action: 'claim',
busy: false,
message: (error?.message || 'The issue could not be assigned.') + ' The update and your reply draft are safe; retry.',
});
return 'retry';
}).finally(() => { request = null; });
return request;
}
return { open, act };
}
if (typeof module !== 'undefined' && module.exports) module.exports = createUpdateOwnership;