feat: search the full Find Work catalog (Closes #673)
CI / lint (pull_request) Successful in 1m30s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-12 19:24:55 +00:00
1 parent dcf26c896c
commit 77fab85a66
18 files changed
+201 -28

No files matched your search

+3
View File
@@ -455,6 +455,9 @@ textarea { resize: vertical; min-height: 120px; }
.find-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.find-work-header-actions { display:flex; gap:8px; }
.find-work-header button, .find-work-card button, .find-work-card a, .find-work-more { min-height:44px; }
.find-work-search { position:sticky; top:0; z-index:2; display:grid; gap:6px; padding:8px 0; background:#0b1526; }
.find-work-search > div { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; }
.find-work-search input, .find-work-search button { 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.selected { border-color:#60a5fa; box-shadow:0 0 0 2px rgba(96,165,250,.25); }
+32
View File
@@ -593,6 +593,14 @@
renderAvailableIssues(findWorkController.items());
},
});
let findWorkSearchTimer = null;
function updateFindWorkMatchStatus() {
const query = findWorkController.query();
qs('#find-work-match-status').textContent = query ?
availablePagination.total + ' match' + (availablePagination.total === 1 ? '' : 'es') +
' for “' + query + '”.' : '';
}
function setStatus(msg) { qs('#status').textContent = msg || 'Live'; }
function setClock() { qs('#clock').textContent = fmt(new Date()); }
@@ -4222,6 +4230,30 @@
qs('#find-work').addEventListener('click', openFindWorkSheet);
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
qs('#select-find-work').addEventListener('click', () => findWorkController.startSelection());
qs('#find-work-search-form').addEventListener('submit', event => event.preventDefault());
qs('#find-work-search').addEventListener('input', event => {
const value = event.currentTarget.value;
qs('#clear-find-work-search').hidden = !value;
clearTimeout(findWorkSearchTimer);
findWorkSearchTimer = setTimeout(async () => {
qs('#find-work-status').textContent = value.trim() ?
'Searching available issues…' : 'Loading available issues…';
try {
await findWorkController.search(value);
updateFindWorkMatchStatus();
qs('#find-work-status').textContent = availablePagination.total ?
'Available work loaded.' : 'No matching unassigned issues.';
} catch (error) {
qs('#find-work-status').textContent = error.message + ' Retry search.';
}
}, 250);
});
qs('#clear-find-work-search').addEventListener('click', () => {
const input = qs('#find-work-search');
input.value = '';
input.dispatchEvent(new Event('input', { bubbles:true }));
input.focus();
});
qs('#cancel-find-work-selection').addEventListener('click', () => findWorkController.cancelSelection());
qs('#claim-selected-work').addEventListener('click', async event => {
event.currentTarget.disabled = true;
+6
View File
@@ -535,6 +535,12 @@
<div class="find-work-header-actions"><button id="select-find-work" type="button">Select work</button><button id="close-find-work" type="button">Close</button></div>
</div>
<p class="small">Claim an available issue and continue it in My Work.</p>
<form class="find-work-search" id="find-work-search-form" role="search">
<label for="find-work-search">Search repository, #number, or title</label>
<div><input id="find-work-search" type="search" maxlength="100" autocomplete="off" />
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
<span id="find-work-match-status" class="small" aria-live="polite"></span>
</form>
<div id="find-work-status" class="small" aria-live="assertive">Open Find Work to load available issues.</div>
<div class="find-work-list" id="find-work-list"></div>
<button class="find-work-more" id="load-more-available" type="button" hidden>Load more available issues</button>
+18 -5
View File
@@ -2,6 +2,8 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio
let available = [];
let pagination = { page: 1, total: 0, has_more: false };
let loadRequest = null;
let loadGeneration = 0;
let query = '';
let claimRequest = null;
const previewed = new Set();
let selecting = false;
@@ -41,11 +43,14 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio
onPagination({ ...pagination });
}
function loadPage(page, append) {
if (loadRequest) return loadRequest;
loadRequest = fetchJson('api/v1/available-issues?page=' + page, {
function loadPage(page, append, requestedQuery = query) {
if (loadRequest && requestedQuery === query && append) return loadRequest;
const generation = ++loadGeneration;
const queryPart = requestedQuery ? '&q=' + encodeURIComponent(requestedQuery) : '';
const request = fetchJson('api/v1/available-issues?page=' + page + queryPart, {
headers: { Accept: 'application/json' },
}).then(result => {
if (generation !== loadGeneration) return result;
apply(result, append);
if (result?.refresh_failed === true) {
onStatus('Showing saved available work. Catalog refresh failed; retrying shortly.');
@@ -53,8 +58,9 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio
onStatus('Showing saved available work while the catalog refreshes…');
}
return result;
}).finally(() => { loadRequest = null; });
return loadRequest;
}).finally(() => { if (loadRequest === request) loadRequest = null; });
loadRequest = request;
return request;
}
return {
@@ -68,6 +74,13 @@ function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelectio
if (!pagination.has_more) return Promise.resolve(false);
return loadPage(pagination.page + 1, true);
},
search(value) {
query = String(value || '').trim();
return loadPage(1, false, query);
},
query() {
return query;
},
items() {
return available.slice();
},
+1 -1
View File
@@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v97';
const CACHE = 'stackchain-dashboard-shell-v98';
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;