From c594cc20fcce206489deb483483bc631a9cb370a Mon Sep 17 00:00:00 2001 From: timmy Date: Sat, 15 Aug 2026 12:59:47 +0000 Subject: [PATCH] feat: claim unowned work from Filed (Closes #893) --- frontend/dashboard.css | 3 ++ frontend/dashboard.js | 55 ++++++++++++++++++++++++++++++++++ frontend/filed-claim.js | 9 ++++++ frontend/index.html | 5 ++++ frontend/service-worker.js | 1 + src/frontend_bundle.py | 2 +- tests/test_assign_and_start.py | 42 ++++++++++++++++++++++++++ tests/test_service_worker.py | 1 + 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 frontend/filed-claim.js diff --git a/frontend/dashboard.css b/frontend/dashboard.css index c000ce3..ce0daf8 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -539,6 +539,9 @@ 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; } +.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; } .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; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index f4231a2..e933f30 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -1799,6 +1799,35 @@ }, }); + const filedAssignAndStart = createAssignAndStart({ + available: createAndStart.available, + claim: item => findWorkController.claim(item), + start: confirmed => { + const claimed = acceptClaimedIssue(confirmed); + closeIssueSheet(false); + refreshMyWorkView(); + return createAndStart.complete(claimed); + }, + queue: confirmed => { + const claimed = acceptClaimedIssue(confirmed); + const outcome = queueToday(claimed); + closeIssueSheet(false); + refreshMyWorkView(); + openRoutedWork(claimed, qs('#my-work')); + return outcome; + }, + recover: confirmed => { + const claimed = acceptClaimedIssue(confirmed); + closeIssueSheet(false); + refreshMyWorkView(); + openRoutedWork(claimed, qs('#my-work')); + }, + announce: message => { + qs('#issue-sheet-status').textContent = message; + qs('#my-work-action-status').textContent = message; + }, + }); + const batchFindWork = createBatchFindWork({ capacity: () => Math.max(0, todayWork.limit - todayWork.read().length), owner:()=>planningOwnerLogin,timeBudget:()=>{ @@ -3373,6 +3402,9 @@ candidate.repository === item.repository && candidate.number === item.number ); qs('#completed-filed-actions').hidden = !item.is_completed; + qs('#filed-claim-actions').hidden = true; + qs('#queue-filed-issue').disabled = true; + qs('#start-filed-issue').disabled = true; qs('#completed-filed-progress').textContent = item.is_completed ? 'Completed Filed issue ' + (completedPosition + 1) + ' of ' + completedItems.length : ''; qs('#issue-sheet-body').textContent = ''; @@ -3432,6 +3464,10 @@ ).join(' '); qs('#issue-assignees').textContent = (detail.assignees || []).length ? 'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported'; + const claimableFiling = !offlineDetail && filedClaimEligible(item, detail); + qs('#filed-claim-actions').hidden = !claimableFiling; + qs('#queue-filed-issue').disabled = !claimableFiling; + qs('#start-filed-issue').disabled = !claimableFiling; if (readOnly) paintIssueConversation(issueConversation.snapshot(), null); else renderIssueConversation(issueConversation.snapshot()); qs('#open-issue-gitea').href = detail.url || item.url || '#'; @@ -5481,6 +5517,25 @@ } }); qs('#close-issue-sheet').addEventListener('click', closeIssueSheet); + function runFiledClaim(destination) { + const item = selectedIssue; + if (!item || !selectedIssueDetail || !filedClaimEligible(item, selectedIssueDetail)) return; + qs('#queue-filed-issue').disabled = true; + qs('#start-filed-issue').disabled = true; + const request = destination === 'queue' ? + filedAssignAndStart.run(item, { destination:'queue' }) : filedAssignAndStart.run(item); + request.catch(error => { + if (selectedIssue !== item) return; + qs('#issue-sheet-status').textContent = 'Claim failed; this issue is still in Filed. ' + error.message + ' Retry.'; + }).finally(() => { + if (selectedIssue === item && filedClaimEligible(item, selectedIssueDetail)) { + qs('#queue-filed-issue').disabled = false; + qs('#start-filed-issue').disabled = false; + } + }); + } + qs('#queue-filed-issue').addEventListener('click', () => runFiledClaim('queue')); + qs('#start-filed-issue').addEventListener('click', () => runFiledClaim('start')); qs('#retry-issue-load').addEventListener('click', () => { if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger); }); diff --git a/frontend/filed-claim.js b/frontend/filed-claim.js new file mode 100644 index 0000000..15846e0 --- /dev/null +++ b/frontend/filed-claim.js @@ -0,0 +1,9 @@ +function filedClaimEligible(item, detail) { + return Boolean( + item?.is_filed && !item?.is_assigned && !item?.is_completed && + item?.state === 'open' && detail?.state === 'open' && + Array.isArray(detail?.assignees) && detail.assignees.length === 0 + ); +} + +if (typeof module !== 'undefined' && module.exports) module.exports = filedClaimEligible; diff --git a/frontend/index.html b/frontend/index.html index db24aca..2701660 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -698,6 +698,10 @@
Load teammates to transfer ownership.
+
@@ -1307,6 +1311,7 @@ + diff --git a/frontend/service-worker.js b/frontend/service-worker.js index cdb698c..a73e075 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -76,6 +76,7 @@ const SHELL = [ BASE + 'static/create-issue-sheet.js', BASE + 'static/create-and-start.js', BASE + 'static/assign-and-start.js', + BASE + 'static/filed-claim.js', BASE + 'static/queue-today.js', BASE + 'static/pull-sheet.js', BASE + 'static/review-sheet.js', diff --git a/src/frontend_bundle.py b/src/frontend_bundle.py index 8544c3c..082215b 100644 --- a/src/frontend_bundle.py +++ b/src/frontend_bundle.py @@ -32,7 +32,7 @@ FEATURE_SOURCES = { "today-timer": ( "static/today-completion.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js", "static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js", - "static/assign-and-start.js", "static/queue-today.js", "static/create-and-start.js", + "static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js", "static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js", "static/today-work.js", "static/pick-work.js", "static/batch-find-work.js", "static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js", diff --git a/tests/test_assign_and_start.py b/tests/test_assign_and_start.py index d3820dd..b789a1b 100644 --- a/tests/test_assign_and_start.py +++ b/tests/test_assign_and_start.py @@ -4,6 +4,7 @@ from pathlib import Path ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js" +FILED_CLAIM = Path(__file__).parents[1] / "frontend" / "filed-claim.js" QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.js" HTML = Path(__file__).parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" @@ -17,6 +18,47 @@ def run_node(script): ).stdout) +def test_filed_claim_actions_require_open_authored_unassigned_canonical_issue(): + script = f""" +const eligible=require({json.dumps(str(FILED_CLAIM))}); +const item={{is_filed:true,is_assigned:false,is_completed:false,state:'open'}}; +process.stdout.write(JSON.stringify({{ + eligible:eligible(item, {{state:'open',assignees:[]}}), + assigned:eligible(item, {{state:'open',assignees:['alex']}}), + closed:eligible(item, {{state:'closed',assignees:[]}}), + ordinary:eligible({{...item,is_filed:false}}, {{state:'open',assignees:[]}}), +}})); +""" + + assert run_node(script) == { + "eligible": True, + "assigned": False, + "closed": False, + "ordinary": False, + } + + +def test_filed_sheet_exposes_phone_safe_claim_actions_after_canonical_detail(): + html = HTML.read_text() + dashboard = DASHBOARD.read_text() + css = CSS.read_text() + worker = WORKER.read_text() + + assert 'id="filed-claim-actions"' in html + assert 'id="queue-filed-issue"' in html + assert 'id="start-filed-issue"' in html + assert "filedClaimEligible(item, detail)" in dashboard + assert "filedAssignAndStart.run(item, { destination:'queue' })" in dashboard + assert "filedAssignAndStart.run(item)" in dashboard + assert "Claim failed; this issue is still in Filed." in dashboard + assert ".filed-claim-actions" in css + action_rule = css.split(".filed-claim-actions", 1)[1].split("}", 1)[0] + assert "grid-template-columns:repeat(2,minmax(0,1fr))" in action_rule + assert "width:min(560px,100%)" in action_rule + assert "BASE + 'static/filed-claim.js'" in worker + assert '' in html + + def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue(): script = f""" const fs=require('fs'); diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index aea188c..d43ff02 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -883,6 +883,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell(): "/dashboard/static/create-issue-sheet.js", "/dashboard/static/create-and-start.js", "/dashboard/static/assign-and-start.js", + "/dashboard/static/filed-claim.js", "/dashboard/static/queue-today.js", "/dashboard/static/pull-sheet.js", "/dashboard/static/review-sheet.js", -- 2.43.0