Scope mobile Search by repository
CI / lint (pull_request) Successful in 1m37s
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-14 03:11:40 +00:00
1 parent 7636e30482
commit ec86bf55a8
15 files changed
+234 -25

No files matched your search

+6 -1
View File
@@ -74,11 +74,16 @@
timer = setTimeout(() => requestPage(query, 1, current, false), delay);
},
setScope(value) {
const repository = typeof value?.repository === 'string' &&
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repository.trim())
? value.repository.trim() : '';
const next = {
kind:['all', 'issue', 'pull'].includes(value?.kind) ? value.kind : 'all',
state:['all', 'open', 'closed'].includes(value?.state) ? value.state : 'all',
...(repository ? { repository } : {}),
};
if (next.kind === scope.kind && next.state === scope.state) return;
if (next.kind === scope.kind && next.state === scope.state &&
(next.repository || '') === (scope.repository || '')) return;
const query = state.query;
scope = next;
this.setQuery(query);
+3
View File
@@ -118,6 +118,8 @@ textarea { resize: vertical; min-height: 120px; }
.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-search-scope input { min-width:0; padding:7px; border-radius:8px; border:1px solid #31577f; background:#0b1526; color:#e5e7eb; }
.cmd-repository-status { grid-column:2 / -1; min-height:16px; color:#93a4b8; font-size:12px; }
#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; }
@@ -691,6 +693,7 @@ textarea { resize: vertical; min-height: 120px; }
#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-search-repository { 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; }
+52 -2
View File
@@ -4287,7 +4287,8 @@
let commandSelection = -1;
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), {
'&kind=' + encodeURIComponent(scope.kind) + '&state=' + encodeURIComponent(scope.state) +
(scope.repository ? '&repository=' + encodeURIComponent(scope.repository) : ''), {
headers: { Accept:'application/json' },
signal,
});
@@ -4303,11 +4304,13 @@
},
});
function currentSearchScope() {
return { kind:qs('#cmd-search-kind').value, state:qs('#cmd-search-state').value };
return { kind:qs('#cmd-search-kind').value, state:qs('#cmd-search-state').value,
repository:qs('#cmd-search-repository').value.trim() };
}
function applySearchScope(scope = {kind:'all', state:'all'}) {
qs('#cmd-search-kind').value = scope.kind;
qs('#cmd-search-state').value = scope.state;
qs('#cmd-search-repository').value = scope.repository || '';
commandSearch.setScope(scope);
}
function safeSearchUrl(value) {
@@ -4407,6 +4410,7 @@
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
search_kind:scope.kind, search_state:scope.state,
});
if (scope.repository) params.set('search_repository', scope.repository);
return new URL('?' + params, window.location.origin + window.location.pathname).href;
}
function createSearchStart(claim) {
@@ -4584,6 +4588,52 @@
}
qs('#cmd-search-kind').addEventListener('change', changeSearchScope);
qs('#cmd-search-state').addEventListener('change', changeSearchScope);
let repositoryLookupController = null;
let repositoryLookupTimer = null;
async function suggestSearchRepositories() {
const input = qs('#cmd-search-repository');
const query = input.value.trim();
const status = qs('#cmd-repository-status');
if (repositoryLookupTimer !== null) clearTimeout(repositoryLookupTimer);
if (repositoryLookupController) repositoryLookupController.abort();
repositoryLookupController = null;
if (!query) {
qs('#cmd-search-repositories').innerHTML = '';
status.textContent = 'Searching all accessible repositories.';
changeSearchScope();
return;
}
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(query)) changeSearchScope();
if (query.length < 2) {
status.textContent = 'Enter at least 2 characters to find a repository.';
return;
}
status.textContent = 'Finding accessible repositories…';
repositoryLookupTimer = setTimeout(async () => {
const controller = new AbortController();
repositoryLookupController = controller;
try {
const response = await fetch('api/v1/repositories/search?q=' + encodeURIComponent(query) + '&limit=20', {
headers:{ Accept:'application/json' }, signal:controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || 'Repository lookup unavailable.');
if (repositoryLookupController !== controller || input.value.trim() !== query) return;
const items = Array.isArray(payload.items) ? payload.items : [];
qs('#cmd-search-repositories').innerHTML = items.map(item =>
'<option value="' + escapeHtml(item.full_name) + '"></option>'
).join('');
status.textContent = items.length ? 'Choose an accessible repository.' : 'No accessible repositories found.';
} catch (error) {
if (error?.name !== 'AbortError') status.textContent = 'Repository lookup unavailable; global Search still works.';
} finally {
if (repositoryLookupController === controller) repositoryLookupController = null;
}
}, 200);
}
qs('#cmd-search-repository').addEventListener('input', suggestSearchRepositories);
qs('#cmd-search-repository').addEventListener('change', changeSearchScope);
qs('#cmd-search-repository').addEventListener('search', suggestSearchRepositories);
qs('#cmd-input').addEventListener('input', (e) => {
commandSelection = -1;
taskOverlayHistory.update({ query:e.target.value });
+4
View File
@@ -443,6 +443,10 @@
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
<label for="cmd-search-repository">Repository</label>
<input id="cmd-search-repository" type="search" list="cmd-search-repositories" autocomplete="off" placeholder="All repositories" aria-describedby="cmd-repository-status" />
<datalist id="cmd-search-repositories"></datalist>
<span id="cmd-repository-status" class="cmd-repository-status" aria-live="polite"></span>
</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>
+11 -2
View File
@@ -4,7 +4,8 @@
}(typeof self !== 'undefined' ? self : this, function createLoginController(options) {
function validShareContinuation(value) {
if (typeof value !== 'string' || !value.startsWith('./?') || value.includes('#')) return './';
const limits = { title: 200, text: 8000, url: 2048, launch: 8, shared: 5, search: 200, preview: 200 };
const limits = { title: 200, text: 8000, url: 2048, launch: 8, shared: 5, search: 200,
preview: 200, search_kind: 5, search_state: 6, search_repository: 161 };
const params = new URLSearchParams(value.slice(3));
const entries = Array.from(params.entries());
if (!entries.length) return './';
@@ -18,9 +19,17 @@
const search = params.get('search');
const preview = params.get('preview');
if (search !== null || preview !== null) {
if (entries.some(([name]) => !['search', 'preview'].includes(name))) return './';
if (entries.some(([name]) => ![
'search', 'preview', 'search_kind', 'search_state', 'search_repository'
].includes(name))) return './';
if (search === null || preview === null) return './';
if (!/^(issue|pull):[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+:[1-9]\d*$/.test(preview)) return './';
const kind = params.get('search_kind');
const state = params.get('search_state');
const repository = params.get('search_repository');
if (kind !== null && !['all', 'issue', 'pull'].includes(kind)) return './';
if (state !== null && !['all', 'open', 'closed'].includes(state)) return './';
if (repository !== null && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return './';
return value;
}
const launch = params.get('launch');
+8 -1
View File
@@ -15,9 +15,13 @@
}
function cleanScope(value) {
const repository = typeof value?.repository === 'string' &&
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repository.trim())
? value.repository.trim() : '';
return {
kind:['all', 'issue', 'pull'].includes(value?.kind) ? value.kind : 'all',
state:['all', 'open', 'closed'].includes(value?.state) ? value.state : 'all',
repository,
};
}
@@ -64,9 +68,10 @@
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 hasScope = params.has('search_kind') || params.has('search_state') || params.has('search_repository');
const scope = hasScope ? cleanScope({
kind:params.get('search_kind'), state:params.get('search_state'),
repository:params.get('search_repository'),
}) : null;
return {
kind:preview ? 'search-preview' : 'search',
@@ -96,11 +101,13 @@
params.delete('preview');
params.delete('search_kind');
params.delete('search_state');
params.delete('search_repository');
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.scope.repository) params.set('search_repository', detail.scope.repository);
}
if (detail.kind === 'search-preview' && detail.preview) params.set('preview', previewToken(detail.preview));
}