Compare commits
No commits in common. "c31129f1cc7116bc436ee26122e7b010244967d1" and "985472eb065876eb371c8c070e863818f3650447" have entirely different histories.
c31129f1cc
...
985472eb06
|
|
@ -1,29 +0,0 @@
|
||||||
function createAssignAndStart({ available, claim, start, recover, announce }) {
|
|
||||||
let request = null;
|
|
||||||
|
|
||||||
function run(item) {
|
|
||||||
if (request) return request;
|
|
||||||
if (!available()) {
|
|
||||||
announce('Today is full—remove an item before assigning this issue.');
|
|
||||||
return Promise.resolve('full');
|
|
||||||
}
|
|
||||||
request = Promise.resolve()
|
|
||||||
.then(() => claim(item))
|
|
||||||
.then(confirmed => {
|
|
||||||
const outcome = start(confirmed);
|
|
||||||
if (outcome === 'started') {
|
|
||||||
announce('Assigned, added to Today, and ready to work.');
|
|
||||||
return outcome;
|
|
||||||
}
|
|
||||||
announce('Assigned to you, but Today could not start. The issue is open so you can recover.');
|
|
||||||
recover(confirmed);
|
|
||||||
return 'recovery';
|
|
||||||
})
|
|
||||||
.finally(() => { request = null; });
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { run };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = createAssignAndStart;
|
|
||||||
|
|
@ -263,7 +263,6 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.find-work-list { display:grid; gap:10px; }
|
.find-work-list { display:grid; gap:10px; }
|
||||||
.find-work-card { display:grid; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; overflow-wrap:anywhere; }
|
.find-work-card { display:grid; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; overflow-wrap:anywhere; }
|
||||||
.find-work-card button { width:100%; font-weight:700; }
|
.find-work-card button { width:100%; font-weight:700; }
|
||||||
.find-work-claim-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
|
||||||
.find-work-detail { min-width:0; display:grid; gap:10px; padding:10px; border-radius:10px; background:#0b1526; }
|
.find-work-detail { min-width:0; display:grid; gap:10px; padding:10px; border-radius:10px; background:#0b1526; }
|
||||||
.find-work-description { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
.find-work-description { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||||
.find-work-detail a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
.find-work-detail a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||||
|
|
|
||||||
|
|
@ -757,38 +757,6 @@
|
||||||
announce: message => { qs('#my-work-action-status').textContent = message; },
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
});
|
});
|
||||||
|
|
||||||
function acceptClaimedIssue(confirmed) {
|
|
||||||
lastContextSnapshot = lastContextSnapshot || { user: {}, repos: [], issues: [], pull_requests: [] };
|
|
||||||
lastContextSnapshot.issues = [confirmed].concat((lastContextSnapshot.issues || []).filter(candidate =>
|
|
||||||
candidate.repository !== confirmed.repository || candidate.number !== confirmed.number
|
|
||||||
));
|
|
||||||
lastMyWork = buildMyWork(lastContextSnapshot);
|
|
||||||
return lastMyWork.find(work =>
|
|
||||||
work.kind === 'issue' && work.repository === confirmed.repository && work.number === confirmed.number
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignAndStart = createAssignAndStart({
|
|
||||||
available: createAndStart.available,
|
|
||||||
claim: item => findWorkController.claim(item),
|
|
||||||
start: confirmed => {
|
|
||||||
const claimed = acceptClaimedIssue(confirmed);
|
|
||||||
taskOverlayHistory.leave();
|
|
||||||
refreshMyWorkView();
|
|
||||||
return createAndStart.complete(claimed);
|
|
||||||
},
|
|
||||||
recover: confirmed => {
|
|
||||||
const claimed = acceptClaimedIssue(confirmed);
|
|
||||||
taskOverlayHistory.leave();
|
|
||||||
refreshMyWorkView();
|
|
||||||
openRoutedWork(claimed, qs('#find-work'));
|
|
||||||
},
|
|
||||||
announce: message => {
|
|
||||||
qs('#find-work-status').textContent = message;
|
|
||||||
qs('#my-work-action-status').textContent = message;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let planTodayTrigger = null;
|
let planTodayTrigger = null;
|
||||||
function planTodayItemMarkup(item, selected, index = -1) {
|
function planTodayItemMarkup(item, selected, index = -1) {
|
||||||
const id = todayWork.identity(item);
|
const id = todayWork.identity(item);
|
||||||
|
|
@ -1901,9 +1869,7 @@
|
||||||
'<div>' + (item.labels || []).map(label => '<span class="pill">' + escapeHtml(label) + '</span>').join(' ') +
|
'<div>' + (item.labels || []).map(label => '<span class="pill">' + escapeHtml(label) + '</span>').join(' ') +
|
||||||
'</div><button type="button" data-preview-index="' + index + '" aria-expanded="' + expanded +
|
'</div><button type="button" data-preview-index="' + index + '" aria-expanded="' + expanded +
|
||||||
'" aria-controls="' + detailId + '">' + (expanded ? 'Hide details' : 'View details') + '</button>' +
|
'" aria-controls="' + detailId + '">' + (expanded ? 'Hide details' : 'View details') + '</button>' +
|
||||||
detail + '<div class="find-work-claim-actions"><button type="button" data-claim-index="' + index +
|
detail + '<button type="button" data-claim-index="' + index + '">Assign to me</button></article>';
|
||||||
'">Assign to me</button><button type="button" data-claim-start-index="' + index +
|
|
||||||
'">Assign & start</button></div></article>';
|
|
||||||
}).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
}).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
||||||
list.querySelectorAll('[data-preview-index]').forEach(button => {
|
list.querySelectorAll('[data-preview-index]').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
|
|
@ -1923,7 +1889,12 @@
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
try {
|
try {
|
||||||
const confirmed = await findWorkController.claim(item);
|
const confirmed = await findWorkController.claim(item);
|
||||||
const claimed = acceptClaimedIssue(confirmed);
|
lastContextSnapshot = lastContextSnapshot || { user: {}, repos: [], issues: [], pull_requests: [] };
|
||||||
|
lastContextSnapshot.issues = [confirmed].concat(lastContextSnapshot.issues || []);
|
||||||
|
lastMyWork = buildMyWork(lastContextSnapshot);
|
||||||
|
const claimed = lastMyWork.find(work =>
|
||||||
|
work.kind === 'issue' && work.repository === confirmed.repository && work.number === confirmed.number
|
||||||
|
);
|
||||||
taskOverlayHistory.leave();
|
taskOverlayHistory.leave();
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
qs('#my-work-action-status').textContent = confirmed.repository + '#' + confirmed.number + ' assigned to you.';
|
qs('#my-work-action-status').textContent = confirmed.repository + '#' + confirmed.number + ' assigned to you.';
|
||||||
|
|
@ -1935,22 +1906,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
list.querySelectorAll('[data-claim-start-index]').forEach(button => {
|
|
||||||
button.addEventListener('click', async () => {
|
|
||||||
const item = findWorkController.items()[Number(button.dataset.claimStartIndex)];
|
|
||||||
if (!item) return;
|
|
||||||
const claimButtons = button.closest('.find-work-card')
|
|
||||||
.querySelectorAll('[data-claim-index], [data-claim-start-index]');
|
|
||||||
claimButtons.forEach(action => { action.disabled = true; });
|
|
||||||
try {
|
|
||||||
await assignAndStart.run(item);
|
|
||||||
} catch (error) {
|
|
||||||
qs('#find-work-status').textContent = error.message + ' Nothing was added to Today; retry assignment.';
|
|
||||||
claimButtons.forEach(action => { action.disabled = false; });
|
|
||||||
button.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openFindWorkSheet(navigate = true) {
|
async function openFindWorkSheet(navigate = true) {
|
||||||
|
|
|
||||||
|
|
@ -606,7 +606,6 @@
|
||||||
<script src="static/issue-sheet.js"></script>
|
<script src="static/issue-sheet.js"></script>
|
||||||
<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/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>
|
||||||
<script src="static/work-route.js"></script>
|
<script src="static/work-route.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ const SHELL = [
|
||||||
BASE + 'static/issue-sheet.js',
|
BASE + 'static/issue-sheet.js',
|
||||||
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/pull-sheet.js',
|
BASE + 'static/pull-sheet.js',
|
||||||
BASE + 'static/review-sheet.js',
|
BASE + 'static/review-sheet.js',
|
||||||
BASE + 'static/work-route.js',
|
BASE + 'static/work-route.js',
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
|
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
|
||||||
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
|
||||||
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
|
||||||
|
|
||||||
|
|
||||||
def run_node(script):
|
|
||||||
return json.loads(subprocess.run(
|
|
||||||
["node", "-e", script], check=True, capture_output=True, text=True
|
|
||||||
).stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue():
|
|
||||||
script = f"""
|
|
||||||
const fs=require('fs');
|
|
||||||
if (!fs.existsSync({json.dumps(str(ASSIGN_AND_START))})) {{
|
|
||||||
process.stdout.write(JSON.stringify({{available:false}}));
|
|
||||||
}} else {{
|
|
||||||
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
|
||||||
const calls=[];
|
|
||||||
let release;
|
|
||||||
const claimResult=new Promise(resolve=>{{release=resolve;}});
|
|
||||||
const flow=createAssignAndStart({{
|
|
||||||
available:()=>true,
|
|
||||||
claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
|
|
||||||
start:item=>{{calls.push('start:'+item.number);return 'started';}},
|
|
||||||
recover:item=>calls.push('recover:'+item.number),
|
|
||||||
announce:message=>calls.push('announce:'+message),
|
|
||||||
}});
|
|
||||||
const issue={{repository:'stackchain/dashboard',number:411}};
|
|
||||||
const first=flow.run(issue);
|
|
||||||
const second=flow.run(issue);
|
|
||||||
release({{...issue,assignees:['timmy']}});
|
|
||||||
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{available:true,calls,results,same:first===second}})));
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"available": True,
|
|
||||||
"calls": [
|
|
||||||
"claim:411",
|
|
||||||
"start:411",
|
|
||||||
"announce:Assigned, added to Today, and ready to work.",
|
|
||||||
],
|
|
||||||
"results": ["started", "started"],
|
|
||||||
"same": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_full_today_blocks_before_assignment_and_explains_recovery():
|
|
||||||
script = f"""
|
|
||||||
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
|
||||||
const calls=[];
|
|
||||||
const flow=createAssignAndStart({{
|
|
||||||
available:()=>false,
|
|
||||||
claim:()=>{{calls.push('claim');return Promise.resolve({{}});}},
|
|
||||||
start:()=>calls.push('start'),
|
|
||||||
recover:()=>calls.push('recover'),
|
|
||||||
announce:message=>calls.push('announce:'+message),
|
|
||||||
}});
|
|
||||||
flow.run({{repository:'stackchain/dashboard',number:411}}).then(result=>
|
|
||||||
process.stdout.write(JSON.stringify({{result,calls}}))
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"result": "full",
|
|
||||||
"calls": ["announce:Today is full—remove an item before assigning this issue."],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_confirmed_assignment_recovers_in_issue_detail_when_today_start_fails():
|
|
||||||
script = f"""
|
|
||||||
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
|
||||||
const calls=[];
|
|
||||||
const issue={{repository:'stackchain/dashboard',number:411,assignees:['timmy']}};
|
|
||||||
const flow=createAssignAndStart({{
|
|
||||||
available:()=>true,
|
|
||||||
claim:()=>Promise.resolve(issue),
|
|
||||||
start:()=> 'sync-unavailable',
|
|
||||||
recover:item=>calls.push('recover:'+item.number),
|
|
||||||
announce:message=>calls.push('announce:'+message),
|
|
||||||
}});
|
|
||||||
flow.run(issue).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"result": "recovery",
|
|
||||||
"calls": [
|
|
||||||
"announce:Assigned to you, but Today could not start. The issue is open so you can recover.",
|
|
||||||
"recover:411",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_failed_assignment_does_not_start_today_and_can_be_retried():
|
|
||||||
script = f"""
|
|
||||||
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
|
||||||
const calls=[];let attempt=0;
|
|
||||||
const issue={{repository:'stackchain/dashboard',number:411}};
|
|
||||||
const flow=createAssignAndStart({{
|
|
||||||
available:()=>true,
|
|
||||||
claim:()=>{{attempt+=1;calls.push('claim:'+attempt);return attempt===1 ? Promise.reject(new Error('conflict')) : Promise.resolve(issue);}},
|
|
||||||
start:()=>{{calls.push('start');return 'started';}},
|
|
||||||
recover:()=>calls.push('recover'),
|
|
||||||
announce:message=>calls.push('announce:'+message),
|
|
||||||
}});
|
|
||||||
flow.run(issue).catch(error=>{{
|
|
||||||
calls.push('error:'+error.message);
|
|
||||||
return flow.run(issue);
|
|
||||||
}}).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"result": "started",
|
|
||||||
"calls": [
|
|
||||||
"claim:1",
|
|
||||||
"error:conflict",
|
|
||||||
"claim:2",
|
|
||||||
"start",
|
|
||||||
"announce:Assigned, added to Today, and ready to work.",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
|
|
||||||
dashboard = DASHBOARD.read_text()
|
|
||||||
css = CSS.read_text()
|
|
||||||
worker = WORKER.read_text()
|
|
||||||
|
|
||||||
assert "data-claim-start-index" in dashboard
|
|
||||||
assert ">Assign & start</button>" in dashboard
|
|
||||||
assert "const assignAndStart = createAssignAndStart({" in dashboard
|
|
||||||
assert "assignAndStart.run(item)" in dashboard
|
|
||||||
assert "createAndStart.available" in dashboard
|
|
||||||
assert "createAndStart.complete" in dashboard
|
|
||||||
assert ".find-work-claim-actions" in css
|
|
||||||
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
|
|
||||||
assert "BASE + 'static/assign-and-start.js'" in worker
|
|
||||||
|
|
@ -377,7 +377,6 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/issue-sheet.js",
|
"/dashboard/static/issue-sheet.js",
|
||||||
"/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/pull-sheet.js",
|
"/dashboard/static/pull-sheet.js",
|
||||||
"/dashboard/static/review-sheet.js",
|
"/dashboard/static/review-sheet.js",
|
||||||
"/dashboard/static/work-route.js",
|
"/dashboard/static/work-route.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user