feat: scope mobile search by type and status (Closes #793)
CI / lint (pull_request) Successful in 1m45s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 02:34:44 +00:00
1 parent 3c65bc1b31
commit bd054998b2
14 files changed
+315 -37

No files matched your search

+18 -5
View File
@@ -22,7 +22,8 @@
let timer = null;
let generation = 0;
let activeController = null;
let state = { status: 'idle', query: '', items: [], more: false, next: 1 };
let scope = { kind:'all', state:'all' };
let state = { status: 'idle', query: '', items: [], more: false, next: 1, scope };
function publish(next) {
state = next;
@@ -33,7 +34,8 @@
const requestController = new AbortController();
activeController = requestController;
try {
const result = await search(query, requestController.signal, page);
const requestScope = { ...scope };
const result = await search(query, requestController.signal, page, requestScope);
const partial = result.partial === true;
const incoming = result.items || result;
const combined = append ? state.items.concat(incoming) : incoming;
@@ -44,12 +46,13 @@
status: 'ready', query, items, partial,
more: !!result.has_more,
next: result.next_page,
scope:requestScope,
});
} catch (error) {
if (error && error.name === 'AbortError') return;
if (current === generation) publish(append
? { ...state, status: 'ready' }
: { status: 'error', query, items: [], error });
: { status: 'error', query, items: [], error, scope:{ ...scope } });
} finally {
if (activeController === requestController) activeController = null;
}
@@ -64,12 +67,22 @@
if (activeController !== null) activeController.abort();
activeController = null;
if (query.length < 2) {
publish({ status: 'idle', query, items: [], more: false, next: 1 });
publish({ status: 'idle', query, items: [], more: false, next: 1, scope:{ ...scope } });
return;
}
publish({ status: 'loading', query, items: [], more: false, next: 1 });
publish({ status: 'loading', query, items: [], more: false, next: 1, scope:{ ...scope } });
timer = setTimeout(() => requestPage(query, 1, current, false), delay);
},
setScope(value) {
const next = {
kind:['all', 'issue', 'pull'].includes(value?.kind) ? value.kind : 'all',
state:['all', 'open', 'closed'].includes(value?.state) ? value.state : 'all',
};
if (next.kind === scope.kind && next.state === scope.state) return;
const query = state.query;
scope = next;
this.setQuery(query);
},
loadMore() {
if (state.status !== 'ready' || !state.more || activeController) return;
const current = generation;
+6
View File
@@ -114,6 +114,10 @@ textarea { resize: vertical; min-height: 120px; }
#cmd-palette { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: min(900px, 94vw); background: rgba(11,21,38,.96); border: 1px solid #2a496e; border-radius: 12px; box-shadow: 0 20px 70px rgba(0,0,0,.55); padding: 10px; z-index: 30; display: none; backdrop-filter: blur(12px); }
#cmd-palette.open { display: block; }
.cmd-palette-header { display:none; align-items:center; justify-content:space-between; gap:10px; }
.cmd-search-scope { display:grid; grid-template-columns:auto minmax(120px,1fr) auto minmax(120px,1fr); gap:6px 10px; align-items:center; margin:8px 0 0; padding:8px; border:1px solid #1f3a5f; border-radius:8px; }
.cmd-search-scope legend { padding:0 4px; color:#93a4b8; font-size:12px; }
.cmd-search-scope label { font-size:12px; color:#cbd5e1; }
.cmd-search-scope select { min-width:0; padding:7px; border-radius:8px; border:1px solid #31577f; background:#0b1526; color:#e5e7eb; }
#cmd-results { margin-top:8px; max-height:min(65vh,520px); overflow-y:auto; }
.cmd-item { padding: 10px; min-height:44px; cursor: pointer; border-radius: 10px; color:#e5e7eb; display:flex; gap:10px; align-items:center; justify-content:space-between; }
.cmd-item:hover, .cmd-item.selected { background: #10233a; outline:1px solid #31577f; }
@@ -685,6 +689,8 @@ textarea { resize: vertical; min-height: 120px; }
.cmd-palette-header { display:flex; flex:0 0 auto; min-height:44px; }
#close-command-palette, #cmd-load-more { min-height:44px; }
#cmd-input { flex:0 0 auto; min-height:44px; }
.cmd-search-scope { grid-template-columns:auto minmax(0,1fr); }
#cmd-search-kind, #cmd-search-state { min-height:44px; width:100%; }
#cmd-results { flex:1; min-height:0; overflow-y:auto; max-height:none; overscroll-behavior:contain; padding-bottom:env(safe-area-inset-bottom); }
.create-issue-panel { width:100%; border-left:0; padding:14px; }
.pull-sheet-panel { width:100%; border-left:0; padding:14px; }
+30 -6
View File
@@ -4285,8 +4285,9 @@
let commandSearchState = { status:'idle', query:'', items:[] };
let commandItems = [];
let commandSelection = -1;
async function searchGlobalWork(query, signal, page = 1) {
const response = await fetch('api/v1/search?q=' + encodeURIComponent(query) + '&limit=10&page=' + page, {
async function searchGlobalWork(query, signal, page = 1, scope = {kind:'all', state:'all'}) {
const response = await fetch('api/v1/search?q=' + encodeURIComponent(query) + '&limit=10&page=' + page +
'&kind=' + encodeURIComponent(scope.kind) + '&state=' + encodeURIComponent(scope.state), {
headers: { Accept:'application/json' },
signal,
});
@@ -4301,6 +4302,14 @@
renderCommands(state.query);
},
});
function currentSearchScope() {
return { kind:qs('#cmd-search-kind').value, state:qs('#cmd-search-state').value };
}
function applySearchScope(scope = {kind:'all', state:'all'}) {
qs('#cmd-search-kind').value = scope.kind;
qs('#cmd-search-state').value = scope.state;
commandSearch.setScope(scope);
}
function safeSearchUrl(value) {
try {
const url = new URL(value);
@@ -4393,8 +4402,11 @@
onState: renderSearchPreview,
});
function canonicalSearchPreviewUrl() {
const { query, preview } = taskOverlayHistory.currentState();
const params = new URLSearchParams({ search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number });
const { query, preview, scope = currentSearchScope() } = taskOverlayHistory.currentState();
const params = new URLSearchParams({
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
search_kind:scope.kind, search_state:scope.state,
});
return new URL('?' + params, window.location.origin + window.location.pathname).href;
}
function createSearchStart(claim) {
@@ -4458,7 +4470,9 @@
mobileSearchViewport.rememberScroll();
searchPreviewReturnKind = 'search';
searchPreview.open(item.result).catch(() => {});
taskOverlayHistory.open('search-preview', { query:qs('#cmd-input').value, preview:item.result });
taskOverlayHistory.open('search-preview', {
query:qs('#cmd-input').value, scope:currentSearchScope(), preview:item.result,
});
}
qs('#cmd-palette').classList.remove('open');
qs('#cmd-input').setAttribute('aria-expanded', 'false');
@@ -4491,7 +4505,7 @@
}
function openCommandPalette(navigate = true) {
if (navigate) {
taskOverlayHistory.open('search');
taskOverlayHistory.open('search', { scope:currentSearchScope() });
return;
}
qs('#cmd-palette').classList.add('open');
@@ -4541,6 +4555,7 @@
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
if (kind === 'find' && previous !== 'find') openFindWorkSheet(false);
if (kind === 'search' && previous !== 'search-preview') {
if (detail?.scope) applySearchScope(detail.scope);
if (detail?.query !== undefined) {
qs('#cmd-input').value = detail.query;
commandSearch.setQuery(detail.query);
@@ -4548,6 +4563,7 @@
openCommandPalette(false);
}
if (kind === 'search-preview' && detail?.preview && previous !== 'search') {
if (detail?.scope) applySearchScope(detail.scope);
if (detail?.query !== undefined) qs('#cmd-input').value = detail.query;
searchPreviewReturnKind = 'search';
searchPreview.open(detail.preview).catch(() => taskOverlayHistory.close());
@@ -4560,6 +4576,14 @@
qs('#open-palette').addEventListener('click', openCommandPalette);
qs('#close-command-palette').addEventListener('click', () => taskOverlayHistory.close());
qs('#cmd-load-more').addEventListener('click', () => commandSearch.loadMore());
function changeSearchScope() {
const scope = currentSearchScope();
commandSelection = -1;
taskOverlayHistory.update({ scope });
commandSearch.setScope(scope);
}
qs('#cmd-search-kind').addEventListener('change', changeSearchScope);
qs('#cmd-search-state').addEventListener('change', changeSearchScope);
qs('#cmd-input').addEventListener('input', (e) => {
commandSelection = -1;
taskOverlayHistory.update({ query:e.target.value });
+15
View File
@@ -429,6 +429,21 @@
<button id="close-command-palette" type="button">Close</button>
</div>
<input id="cmd-input" type="text" role="combobox" aria-autocomplete="list" aria-controls="cmd-results" aria-expanded="false" placeholder="Search commands, issues, and pull requests..." />
<fieldset class="cmd-search-scope">
<legend>Filter search results</legend>
<label for="cmd-search-kind">Type</label>
<select id="cmd-search-kind">
<option value="all">All work</option>
<option value="issue">Issues</option>
<option value="pull">Pull requests</option>
</select>
<label for="cmd-search-state">Status</label>
<select id="cmd-search-state">
<option value="all">Any status</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
</fieldset>
<div id="cmd-results" class="stack" role="listbox" aria-label="Commands and work search results"></div>
<button id="cmd-load-more" hidden>More results</button>
</div>
+29 -2
View File
@@ -14,6 +14,13 @@
return query.length <= maxQueryLength ? query : '';
}
function cleanScope(value) {
return {
kind:['all', 'issue', 'pull'].includes(value?.kind) ? value.kind : 'all',
state:['all', 'open', 'closed'].includes(value?.state) ? value.state : 'all',
};
}
function cleanPreview(value) {
if (!value || !['issue', 'pull'].includes(value.kind)) return null;
const repository = typeof value.repository === 'string' ? value.repository : '';
@@ -41,11 +48,12 @@
const kind = allowed.has(state?.taskOverlay) ? state.taskOverlay : null;
if (!searchKinds.has(kind)) return { kind };
const query = cleanQuery(state.searchQuery);
const scope = state.searchScope === undefined ? null : cleanScope(state.searchScope);
const preview = kind === 'search-preview' ? cleanPreview(state.searchPreview) : null;
if (kind === 'search-preview' && state.searchPreview !== undefined && !preview) {
return { kind:'search', ...(query ? { query } : {}) };
}
return { kind, ...(query ? { query } : {}), ...(preview ? { preview } : {}) };
return { kind, ...(query ? { query } : {}), ...(scope ? { scope } : {}), ...(preview ? { preview } : {}) };
}
function urlDetail() {
@@ -56,9 +64,14 @@
const query = cleanQuery(rawQuery);
if (rawQuery.trim() && !query) return { kind:null };
const preview = parsePreview(params.get('preview'));
const hasScope = params.has('search_kind') || params.has('search_state');
const scope = hasScope ? cleanScope({
kind:params.get('search_kind'), state:params.get('search_state'),
}) : null;
return {
kind:preview ? 'search-preview' : 'search',
...(query ? { query } : {}),
...(scope ? { scope } : {}),
...(preview ? { preview } : {}),
};
}
@@ -67,8 +80,10 @@
const state = { ...(base || {}), taskOverlay:detail.kind };
delete state.searchQuery;
delete state.searchPreview;
delete state.searchScope;
if (searchKinds.has(detail.kind)) {
if (detail.query) state.searchQuery = detail.query;
if (detail.scope) state.searchScope = cleanScope(detail.scope);
if (detail.kind === 'search-preview' && detail.preview) state.searchPreview = detail.preview;
}
return state;
@@ -79,8 +94,14 @@
const params = new URLSearchParams(location.search || '');
params.delete('search');
params.delete('preview');
params.delete('search_kind');
params.delete('search_state');
if (searchKinds.has(detail.kind)) {
params.set('search', detail.query || '');
if (detail.scope) {
params.set('search_kind', detail.scope.kind);
params.set('search_state', detail.scope.state);
}
if (detail.kind === 'search-preview' && detail.preview) params.set('preview', previewToken(detail.preview));
}
const query = params.toString();
@@ -115,7 +136,11 @@
},
open(kind, detail = {}) {
if (!allowed.has(kind)) return false;
const next = stateDetail(toState({ kind, query:cleanQuery(detail.query), preview:cleanPreview(detail.preview) }));
const next = stateDetail(toState({
kind, query:cleanQuery(detail.query),
scope:detail.scope === undefined ? undefined : cleanScope(detail.scope),
preview:cleanPreview(detail.preview),
}));
if (active.kind === next.kind && JSON.stringify(active) === JSON.stringify(next)) return true;
const previous = active.kind;
history.pushState(toState(next), '', urlFor(next));
@@ -128,6 +153,7 @@
const next = stateDetail(toState({
kind:active.kind,
query:detail.query === undefined ? active.query : cleanQuery(detail.query),
scope:detail.scope === undefined ? active.scope : cleanScope(detail.scope),
preview:detail.preview === undefined ? active.preview : cleanPreview(detail.preview),
}));
history.replaceState(toState(next), '', urlFor(next));
@@ -146,6 +172,7 @@
delete state.taskOverlay;
delete state.searchQuery;
delete state.searchPreview;
delete state.searchScope;
active = { kind:null };
history.replaceState(state, '', urlFor(active));
onChange(null, previous, active);