feat: search the full Find Work catalog (Closes #673)
This commit is contained in:
1 parent
dcf26c896c
commit
77fab85a66
18 files changed
+201
-28
No files matched your search
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
Reference in new issue
Block a user