feat: preview issue details before claiming (#179)
This commit is contained in:
parent
1947f878e9
commit
b4b13a7db1
|
|
@ -150,11 +150,14 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.find-work-sheet.open { display:flex; }
|
||||
.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
||||
.find-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.find-work-header button, .find-work-card button, .find-work-more { min-height:44px; }
|
||||
.find-work-header button, .find-work-card button, .find-work-card a, .find-work-more { min-height:44px; }
|
||||
.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 button { width:100%; font-weight:700; }
|
||||
@media(max-width:320px) { .find-work-panel { padding:12px; } .my-work-actions { width:100%; } .my-work-actions button { flex:1 1 100%; } }
|
||||
.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-detail a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||
@media(max-width:320px) { .find-work-panel { padding:12px; overflow-x:hidden; } .find-work-card { min-width:0; } .my-work-actions { width:100%; } .my-work-actions button { flex:1 1 100%; } }
|
||||
.create-issue-sheet { position:fixed; inset:0; z-index:57; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
||||
.create-issue-sheet.open { display:flex; }
|
||||
.create-issue-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
||||
|
|
@ -1139,12 +1142,31 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
|
||||
function renderAvailableIssues(items) {
|
||||
const list = qs('#find-work-list');
|
||||
list.innerHTML = items.length ? items.map((item, index) =>
|
||||
'<article class="find-work-card"><div class="small">' + escapeHtml(item.repository) + '#' +
|
||||
list.innerHTML = items.length ? items.map((item, index) => {
|
||||
const expanded = findWorkController.isPreviewed(item);
|
||||
const detailId = 'find-work-detail-' + index;
|
||||
const detail = '<div id="' + detailId + '" class="find-work-detail"' + (expanded ? '' : ' hidden') +
|
||||
'><p class="find-work-description">' + escapeHtml(item.body || 'No description provided.') + '</p>' +
|
||||
(item.url ? '<a href="' + escapeHtml(item.url) + '" target="_blank" rel="noopener noreferrer">Open in Gitea</a>' : '') +
|
||||
'</div>';
|
||||
return '<article class="find-work-card"><div class="small">' + escapeHtml(item.repository) + '#' +
|
||||
Number(item.number) + '</div><strong>' + escapeHtml(item.title || 'Untitled issue') + '</strong>' +
|
||||
'<div>' + (item.labels || []).map(label => '<span class="pill">' + escapeHtml(label) + '</span>').join(' ') +
|
||||
'</div><button type="button" data-claim-index="' + index + '">Assign to me</button></article>'
|
||||
).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
||||
'</div><button type="button" data-preview-index="' + index + '" aria-expanded="' + expanded +
|
||||
'" aria-controls="' + detailId + '">' + (expanded ? 'Hide details' : 'View details') + '</button>' +
|
||||
detail + '<button type="button" data-claim-index="' + index + '">Assign to me</button></article>';
|
||||
}).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
||||
list.querySelectorAll('[data-preview-index]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const item = findWorkController.items()[Number(button.dataset.previewIndex)];
|
||||
if (!item) return;
|
||||
const expanded = findWorkController.togglePreview(item);
|
||||
const detail = document.getElementById(button.getAttribute('aria-controls'));
|
||||
button.setAttribute('aria-expanded', String(expanded));
|
||||
button.textContent = expanded ? 'Hide details' : 'View details';
|
||||
if (detail) detail.hidden = !expanded;
|
||||
});
|
||||
});
|
||||
list.querySelectorAll('[data-claim-index]').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
const item = findWorkController.items()[Number(button.dataset.claimIndex)];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus }) {
|
|||
let pagination = { page: 1, total: 0, has_more: false };
|
||||
let loadRequest = null;
|
||||
let claimRequest = null;
|
||||
const previewed = new Set();
|
||||
|
||||
function itemKey(item) {
|
||||
return String(item?.repository || '') + '#' + String(item?.number || '');
|
||||
}
|
||||
|
||||
function apply(result, append) {
|
||||
const incoming = Array.isArray(result?.items) ? result.items : [];
|
||||
|
|
@ -15,6 +20,10 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus }) {
|
|||
} else {
|
||||
available = incoming.map(item => ({ ...item }));
|
||||
}
|
||||
const availableKeys = new Set(available.map(itemKey));
|
||||
previewed.forEach(key => {
|
||||
if (!availableKeys.has(key)) previewed.delete(key);
|
||||
});
|
||||
pagination = {
|
||||
page: Number(result?.page) || 1,
|
||||
total: Number(result?.total) || available.length,
|
||||
|
|
@ -49,6 +58,15 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus }) {
|
|||
items() {
|
||||
return available.slice();
|
||||
},
|
||||
togglePreview(item) {
|
||||
const key = itemKey(item);
|
||||
if (previewed.has(key)) previewed.delete(key);
|
||||
else previewed.add(key);
|
||||
return previewed.has(key);
|
||||
},
|
||||
isPreviewed(item) {
|
||||
return previewed.has(itemKey(item));
|
||||
},
|
||||
claim(item) {
|
||||
if (claimRequest) return claimRequest;
|
||||
const key = item.repository + '#' + item.number;
|
||||
|
|
@ -65,6 +83,7 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus }) {
|
|||
available = available.filter(candidate =>
|
||||
candidate.repository !== item.repository || candidate.number !== item.number
|
||||
);
|
||||
previewed.delete(itemKey(item));
|
||||
onItems(available.slice());
|
||||
onStatus('Assigned ' + key + ' to you.');
|
||||
return confirmed;
|
||||
|
|
|
|||
|
|
@ -317,6 +317,47 @@ controller.load().then(() => controller.loadMore()).then(() =>
|
|||
assert output["pages"][-1] == {"page": 2, "total": 2, "has_more": False}
|
||||
|
||||
|
||||
def test_find_work_preview_stays_with_issue_across_pagination_and_clears_when_claimed():
|
||||
script = f"""
|
||||
const createFindWork = require({json.dumps(str(PICK_WORK))});
|
||||
const states = [];
|
||||
const controller = createFindWork({{
|
||||
fetchJson: (url, options) => options?.method === 'PATCH'
|
||||
? Promise.resolve({{number:7,repository:'stackchain/api',assignees:['timmy']}})
|
||||
: Promise.resolve({{
|
||||
items:[
|
||||
{{id:17,number:7,title:'Preview me',repository:'stackchain/api',body:'Full scope'}},
|
||||
{{id:18,number:8,title:'Next',repository:'stackchain/web',body:''}}
|
||||
],page:2,total:2,has_more:false
|
||||
}}),
|
||||
onItems: items => states.push(items),
|
||||
onPagination: () => {{}},
|
||||
onStatus: () => {{}},
|
||||
}});
|
||||
controller.reset({{
|
||||
items:[{{id:17,number:7,title:'Preview me',repository:'stackchain/api',body:'Full scope'}}],
|
||||
page:1,total:2,has_more:true
|
||||
}});
|
||||
const target = controller.items()[0];
|
||||
const opened = controller.togglePreview(target);
|
||||
controller.loadMore().then(() => {{
|
||||
const afterPagination = controller.isPreviewed(controller.items()[0]);
|
||||
return controller.claim(controller.items()[0]).then(() => process.stdout.write(JSON.stringify({{
|
||||
opened, afterPagination, afterClaim:controller.isPreviewed(target), states
|
||||
}})));
|
||||
}});
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
|
||||
assert output["opened"] is True
|
||||
assert output["afterPagination"] is True
|
||||
assert output["afterClaim"] is False
|
||||
assert [item["number"] for item in output["states"][-1]] == [8]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_my_work_exposes_truthful_work_pagination_control():
|
||||
html = await dashboard()
|
||||
|
|
@ -342,6 +383,21 @@ async def test_mobile_find_work_sheet_is_accessible_touch_sized_and_subpath_safe
|
|||
assert '@media(max-width:320px)' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_find_work_cards_preview_escaped_context_without_extra_requests():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'data-preview-index=' in html
|
||||
assert 'aria-expanded="' in html
|
||||
assert 'aria-controls="' in html
|
||||
assert "const detailId = 'find-work-detail-' + index" in html
|
||||
assert 'class="find-work-detail"' in html
|
||||
assert "escapeHtml(item.body || 'No description provided.')" in html
|
||||
assert '>Open in Gitea</a>' in html
|
||||
assert '.find-work-card a, .find-work-more { min-height:44px;' in html
|
||||
assert '.find-work-detail { min-width:0;' in html
|
||||
|
||||
|
||||
def test_issue_capture_is_single_flight_and_keeps_draft_until_confirmed_success():
|
||||
script = f"""
|
||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user