feat: claim unowned work from Filed (Closes #893)
This commit is contained in:
parent
5105fb40d3
commit
c594cc20fc
|
|
@ -539,6 +539,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
#issue-sheet.read-only .detail-defer,
|
#issue-sheet.read-only .detail-defer,
|
||||||
#issue-sheet.read-only #issue-blockers,
|
#issue-sheet.read-only #issue-blockers,
|
||||||
#issue-sheet.read-only #retry-issue-comment-actions { display:none; }
|
#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 { 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 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-sheet-actions a { border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||||
|
|
|
||||||
|
|
@ -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({
|
const batchFindWork = createBatchFindWork({
|
||||||
capacity: () => Math.max(0, todayWork.limit - todayWork.read().length),
|
capacity: () => Math.max(0, todayWork.limit - todayWork.read().length),
|
||||||
owner:()=>planningOwnerLogin,timeBudget:()=>{
|
owner:()=>planningOwnerLogin,timeBudget:()=>{
|
||||||
|
|
@ -3373,6 +3402,9 @@
|
||||||
candidate.repository === item.repository && candidate.number === item.number
|
candidate.repository === item.repository && candidate.number === item.number
|
||||||
);
|
);
|
||||||
qs('#completed-filed-actions').hidden = !item.is_completed;
|
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 ?
|
qs('#completed-filed-progress').textContent = item.is_completed ?
|
||||||
'Completed Filed issue ' + (completedPosition + 1) + ' of ' + completedItems.length : '';
|
'Completed Filed issue ' + (completedPosition + 1) + ' of ' + completedItems.length : '';
|
||||||
qs('#issue-sheet-body').textContent = '';
|
qs('#issue-sheet-body').textContent = '';
|
||||||
|
|
@ -3432,6 +3464,10 @@
|
||||||
).join(' ');
|
).join(' ');
|
||||||
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
||||||
'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported';
|
'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);
|
if (readOnly) paintIssueConversation(issueConversation.snapshot(), null);
|
||||||
else renderIssueConversation(issueConversation.snapshot());
|
else renderIssueConversation(issueConversation.snapshot());
|
||||||
qs('#open-issue-gitea').href = detail.url || item.url || '#';
|
qs('#open-issue-gitea').href = detail.url || item.url || '#';
|
||||||
|
|
@ -5481,6 +5517,25 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
qs('#close-issue-sheet').addEventListener('click', closeIssueSheet);
|
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', () => {
|
qs('#retry-issue-load').addEventListener('click', () => {
|
||||||
if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger);
|
if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
9
frontend/filed-claim.js
Normal file
9
frontend/filed-claim.js
Normal 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;
|
||||||
|
|
@ -698,6 +698,10 @@
|
||||||
<div id="issue-handoff-status" class="small" aria-live="assertive">Load teammates to transfer ownership.</div>
|
<div id="issue-handoff-status" class="small" aria-live="assertive">Load teammates to transfer ownership.</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</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 & start</button>
|
||||||
|
</section>
|
||||||
<div class="issue-sheet-actions">
|
<div class="issue-sheet-actions">
|
||||||
<button class="share-work-route" type="button">Share</button>
|
<button class="share-work-route" type="button">Share</button>
|
||||||
<button id="release-issue" type="button">Release assignment</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-issue-sheet.js"></script>
|
||||||
<script src="static/create-and-start.js"></script>
|
<script src="static/create-and-start.js"></script>
|
||||||
<script src="static/assign-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/queue-today.js"></script>
|
||||||
<script src="static/pull-sheet.js"></script>
|
<script src="static/pull-sheet.js"></script>
|
||||||
<script src="static/review-sheet.js"></script>
|
<script src="static/review-sheet.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ const SHELL = [
|
||||||
BASE + 'static/create-issue-sheet.js',
|
BASE + 'static/create-issue-sheet.js',
|
||||||
BASE + 'static/create-and-start.js',
|
BASE + 'static/create-and-start.js',
|
||||||
BASE + 'static/assign-and-start.js',
|
BASE + 'static/assign-and-start.js',
|
||||||
|
BASE + 'static/filed-claim.js',
|
||||||
BASE + 'static/queue-today.js',
|
BASE + 'static/queue-today.js',
|
||||||
BASE + 'static/pull-sheet.js',
|
BASE + 'static/pull-sheet.js',
|
||||||
BASE + 'static/review-sheet.js',
|
BASE + 'static/review-sheet.js',
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ FEATURE_SOURCES = {
|
||||||
"today-timer": (
|
"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-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/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/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/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",
|
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
|
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"
|
QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.js"
|
||||||
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
|
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
|
|
@ -17,6 +18,47 @@ def run_node(script):
|
||||||
).stdout)
|
).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():
|
def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue():
|
||||||
script = f"""
|
script = f"""
|
||||||
const fs=require('fs');
|
const fs=require('fs');
|
||||||
|
|
|
||||||
|
|
@ -883,6 +883,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/create-issue-sheet.js",
|
"/dashboard/static/create-issue-sheet.js",
|
||||||
"/dashboard/static/create-and-start.js",
|
"/dashboard/static/create-and-start.js",
|
||||||
"/dashboard/static/assign-and-start.js",
|
"/dashboard/static/assign-and-start.js",
|
||||||
|
"/dashboard/static/filed-claim.js",
|
||||||
"/dashboard/static/queue-today.js",
|
"/dashboard/static/queue-today.js",
|
||||||
"/dashboard/static/pull-sheet.js",
|
"/dashboard/static/pull-sheet.js",
|
||||||
"/dashboard/static/review-sheet.js",
|
"/dashboard/static/review-sheet.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user