diff --git a/frontend/index.html b/frontend/index.html
index 2534446..2e45fb1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -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) =>
- '' + escapeHtml(item.repository) + '#' +
+ list.innerHTML = items.length ? items.map((item, index) => {
+ const expanded = findWorkController.isPreviewed(item);
+ const detailId = 'find-work-detail-' + index;
+ const detail = '
' + escapeHtml(item.body || 'No description provided.') + '
' +
+ (item.url ? '
Open in Gitea' : '') +
+ '
';
+ return '
' + escapeHtml(item.repository) + '#' +
Number(item.number) + '
' + escapeHtml(item.title || 'Untitled issue') + '' +
'' + (item.labels || []).map(label => '' + escapeHtml(label) + '').join(' ') +
- '
'
- ).join('') : '
No unassigned issues are available on this page.
';
+ '
' +
+ detail + '';
+ }).join('') : '
No unassigned issues are available on this page.
';
+ 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)];
diff --git a/frontend/pick-work.js b/frontend/pick-work.js
index 2231f0a..f7be839 100644
--- a/frontend/pick-work.js
+++ b/frontend/pick-work.js
@@ -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;
diff --git a/tests/test_my_work.py b/tests/test_my_work.py
index 59faeda..c0596f4 100644
--- a/tests/test_my_work.py
+++ b/tests/test_my_work.py
@@ -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' 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))});