Merge pull request 'Claim and start unowned work from mobile Filed' (#894) from timmy/893-claim-start-filed into main
All checks were successful
CI / lint (push) Successful in 1m43s
CI / build-release (push) Successful in 5s
CI / browser-journey (push) Successful in 53s
CI / release-candidate (push) Successful in 6s

This commit is contained in:
timmy 2026-08-15 13:03:51 +00:00
commit 498d7e772a
8 changed files with 117 additions and 1 deletions

View File

@ -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; }

View File

@ -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);
});

9
frontend/filed-claim.js Normal file
View File

@ -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;

View File

@ -698,6 +698,10 @@
<div id="issue-handoff-status" class="small" aria-live="assertive">Load teammates to transfer ownership.</div>
</div>
</details>
<section class="filed-claim-actions" id="filed-claim-actions" aria-label="Take ownership of filed issue" hidden>
<button id="queue-filed-issue" type="button">Queue Today</button>
<button id="start-filed-issue" type="button">Claim &amp; start</button>
</section>
<div class="issue-sheet-actions">
<button class="share-work-route" type="button">Share</button>
<button id="release-issue" type="button">Release assignment</button>
@ -1307,6 +1311,7 @@
<script src="static/create-issue-sheet.js"></script>
<script src="static/create-and-start.js"></script>
<script src="static/assign-and-start.js"></script>
<script src="static/filed-claim.js"></script>
<script src="static/queue-today.js"></script>
<script src="static/pull-sheet.js"></script>
<script src="static/review-sheet.js"></script>

View File

@ -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',

View File

@ -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",

View File

@ -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 '<script src="static/filed-claim.js"></script>' in html
def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue():
script = f"""
const fs=require('fs');

View File

@ -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",