Merge pull request 'Assign and start work from global search' (#414) from timmy/413-search-assign-and-start into main
This commit is contained in:
commit
4b8d0c4504
|
|
@ -1,21 +1,24 @@
|
||||||
function createAssignAndStart({ available, claim, start, recover, announce }) {
|
function createAssignAndStart({ available, claim, start, recover, announce }) {
|
||||||
let request = null;
|
let request = null;
|
||||||
|
|
||||||
function run(item) {
|
function run(item, { alreadyOwned = false } = {}) {
|
||||||
if (request) return request;
|
if (request) return request;
|
||||||
if (!available()) {
|
if (!available()) {
|
||||||
announce('Today is full—remove an item before assigning this issue.');
|
announce('Today is full—remove an item before assigning this issue.');
|
||||||
return Promise.resolve('full');
|
return Promise.resolve('full');
|
||||||
}
|
}
|
||||||
request = Promise.resolve()
|
request = Promise.resolve()
|
||||||
.then(() => claim(item))
|
.then(() => alreadyOwned ? item : claim(item))
|
||||||
.then(confirmed => {
|
.then(confirmed => {
|
||||||
const outcome = start(confirmed);
|
const outcome = start(confirmed);
|
||||||
if (outcome === 'started') {
|
if (outcome === 'started') {
|
||||||
announce('Assigned, added to Today, and ready to work.');
|
announce(alreadyOwned ? 'Added to Today and ready to work.' :
|
||||||
|
'Assigned, added to Today, and ready to work.');
|
||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
announce('Assigned to you, but Today could not start. The issue is open so you can recover.');
|
announce(alreadyOwned ?
|
||||||
|
'Today could not start. The issue is open so you can recover.' :
|
||||||
|
'Assigned to you, but Today could not start. The issue is open so you can recover.');
|
||||||
recover(confirmed);
|
recover(confirmed);
|
||||||
return 'recovery';
|
return 'recovery';
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.search-preview-header button, .search-preview-actions button, .search-preview-actions a { min-height:44px; }
|
.search-preview-header button, .search-preview-actions button, .search-preview-actions a { min-height:44px; }
|
||||||
.search-preview-body { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
.search-preview-body { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||||
.search-preview-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
|
.search-preview-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
|
||||||
|
.search-preview-primary-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
||||||
.search-preview-actions a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
.search-preview-actions a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||||
.markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere; white-space:normal; }
|
.markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere; white-space:normal; }
|
||||||
.markdown-content > :first-child { margin-top:0; }
|
.markdown-content > :first-child { margin-top:0; }
|
||||||
|
|
|
||||||
|
|
@ -2644,6 +2644,7 @@
|
||||||
const sheet = qs('#search-preview');
|
const sheet = qs('#search-preview');
|
||||||
const status = qs('#search-preview-status');
|
const status = qs('#search-preview-status');
|
||||||
const claimButton = qs('#claim-search-result');
|
const claimButton = qs('#claim-search-result');
|
||||||
|
const startButton = qs('#start-search-result');
|
||||||
if (state.status === 'closed') {
|
if (state.status === 'closed') {
|
||||||
sheet.classList.remove('open');
|
sheet.classList.remove('open');
|
||||||
return;
|
return;
|
||||||
|
|
@ -2651,6 +2652,8 @@
|
||||||
sheet.classList.add('open');
|
sheet.classList.add('open');
|
||||||
claimButton.hidden = true;
|
claimButton.hidden = true;
|
||||||
claimButton.disabled = false;
|
claimButton.disabled = false;
|
||||||
|
startButton.hidden = true;
|
||||||
|
startButton.disabled = false;
|
||||||
if (state.status === 'loading') {
|
if (state.status === 'loading') {
|
||||||
searchPreviewDetail = null;
|
searchPreviewDetail = null;
|
||||||
qs('#search-preview-key').textContent = state.item.repository + ' #' + state.item.number;
|
qs('#search-preview-key').textContent = state.item.repository + ' #' + state.item.number;
|
||||||
|
|
@ -2680,6 +2683,10 @@
|
||||||
claimButton.hidden = !(detail.claimable || (detail.assigned_to_me && detail.kind === 'issue'));
|
claimButton.hidden = !(detail.claimable || (detail.assigned_to_me && detail.kind === 'issue'));
|
||||||
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
|
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
|
||||||
claimButton.disabled = state.status === 'claiming';
|
claimButton.disabled = state.status === 'claiming';
|
||||||
|
startButton.hidden = !(detail.kind === 'issue' && detail.state === 'open' &&
|
||||||
|
(detail.claimable || detail.assigned_to_me));
|
||||||
|
startButton.textContent = detail.assigned_to_me ? 'Start in Today' : 'Assign & start';
|
||||||
|
startButton.disabled = state.status === 'claiming';
|
||||||
status.textContent = state.status === 'claiming' ? 'Assigning this issue to you…' :
|
status.textContent = state.status === 'claiming' ? 'Assigning this issue to you…' :
|
||||||
(state.status === 'claimed' ? 'Assignment confirmed. Opening My Work…' :
|
(state.status === 'claimed' ? 'Assignment confirmed. Opening My Work…' :
|
||||||
(detail.claimable ? 'This issue is open and unassigned.' :
|
(detail.claimable ? 'This issue is open and unassigned.' :
|
||||||
|
|
@ -2694,6 +2701,26 @@
|
||||||
),
|
),
|
||||||
onState: renderSearchPreview,
|
onState: renderSearchPreview,
|
||||||
});
|
});
|
||||||
|
const searchAssignAndStart = createAssignAndStart({
|
||||||
|
available: createAndStart.available,
|
||||||
|
claim: detail => searchPreview.claim(detail),
|
||||||
|
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('#open-palette'));
|
||||||
|
},
|
||||||
|
announce: message => {
|
||||||
|
qs('#search-preview-status').textContent = message;
|
||||||
|
qs('#my-work-action-status').textContent = message;
|
||||||
|
},
|
||||||
|
});
|
||||||
const mobileSearchViewport = createMobileSearchViewport({
|
const mobileSearchViewport = createMobileSearchViewport({
|
||||||
palette: qs('#cmd-palette'),
|
palette: qs('#cmd-palette'),
|
||||||
results: qs('#cmd-results'),
|
results: qs('#cmd-results'),
|
||||||
|
|
@ -2902,6 +2929,16 @@
|
||||||
qs('#search-preview-status').textContent = error.message + ' Retry assignment.';
|
qs('#search-preview-status').textContent = error.message + ' Retry assignment.';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
qs('#start-search-result').addEventListener('click', async () => {
|
||||||
|
const detail = searchPreviewDetail;
|
||||||
|
if (!detail || detail.kind !== 'issue' || detail.state !== 'open' ||
|
||||||
|
(!detail.claimable && !detail.assigned_to_me)) return;
|
||||||
|
try {
|
||||||
|
await searchAssignAndStart.run(detail, { alreadyOwned: detail.assigned_to_me });
|
||||||
|
} catch (error) {
|
||||||
|
qs('#search-preview-status').textContent = error.message + ' Retry assignment and start.';
|
||||||
|
}
|
||||||
|
});
|
||||||
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
|
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
|
||||||
qs('#find-work').addEventListener('click', openFindWorkSheet);
|
qs('#find-work').addEventListener('click', openFindWorkSheet);
|
||||||
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
|
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,10 @@
|
||||||
<div id="search-preview-meta" class="row"></div>
|
<div id="search-preview-meta" class="row"></div>
|
||||||
<div id="search-preview-body" class="search-preview-body markdown-content"></div>
|
<div id="search-preview-body" class="search-preview-body markdown-content"></div>
|
||||||
<div class="search-preview-actions">
|
<div class="search-preview-actions">
|
||||||
<button id="claim-search-result" type="button" hidden>Assign to me</button>
|
<div class="search-preview-primary-actions">
|
||||||
|
<button id="claim-search-result" type="button" hidden>Assign to me</button>
|
||||||
|
<button id="start-search-result" type="button" hidden>Assign & start</button>
|
||||||
|
</div>
|
||||||
<a id="open-search-result-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-search-result-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,32 @@ flow.run(issue).catch(error=>{{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_already_owned_search_result_starts_without_claiming():
|
||||||
|
script = f"""
|
||||||
|
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
||||||
|
const calls=[];
|
||||||
|
const issue={{repository:'stackchain/dashboard',number:413,assigned_to_me:true}};
|
||||||
|
const flow=createAssignAndStart({{
|
||||||
|
available:()=>true,
|
||||||
|
claim:()=>{{calls.push('claim');return Promise.resolve(issue);}},
|
||||||
|
start:item=>{{calls.push('start:'+item.number);return 'started';}},
|
||||||
|
recover:item=>calls.push('recover:'+item.number),
|
||||||
|
announce:message=>calls.push('announce:'+message),
|
||||||
|
}});
|
||||||
|
flow.run(issue, {{alreadyOwned:true}}).then(result=>
|
||||||
|
process.stdout.write(JSON.stringify({{result,calls}}))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"result": "started",
|
||||||
|
"calls": [
|
||||||
|
"start:413",
|
||||||
|
"announce:Added to Today and ready to work.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
|
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
|
||||||
dashboard = DASHBOARD.read_text()
|
dashboard = DASHBOARD.read_text()
|
||||||
css = CSS.read_text()
|
css = CSS.read_text()
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,18 @@ def test_assigned_issue_preview_hands_off_to_existing_my_work_sheet():
|
||||||
assert "openPreviewIssueInMyWork" in html
|
assert "openPreviewIssueInMyWork" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_preview_offers_assign_and_start_for_eligible_issues():
|
||||||
|
html = dashboard_bundle_text()
|
||||||
|
css = (FRONTEND / "dashboard.css").read_text()
|
||||||
|
|
||||||
|
assert 'id="start-search-result"' in html
|
||||||
|
assert "startButton.textContent = detail.assigned_to_me ? 'Start in Today' : 'Assign & start'" in html
|
||||||
|
assert "const searchAssignAndStart = createAssignAndStart({" in html
|
||||||
|
assert "searchAssignAndStart.run(detail, { alreadyOwned: detail.assigned_to_me })" in html
|
||||||
|
assert ".search-preview-primary-actions" in css
|
||||||
|
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_tracks_keyboard_geometry_without_leaking_listeners():
|
def test_mobile_search_viewport_tracks_keyboard_geometry_without_leaking_listeners():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createMobileSearchViewport = require({json.dumps(str(MOBILE_SEARCH_VIEWPORT))});
|
const createMobileSearchViewport = require({json.dumps(str(MOBILE_SEARCH_VIEWPORT))});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user