Scope mobile Search by repository
This commit is contained in:
1 parent
7636e30482
commit
ec86bf55a8
15 files changed
+234
-25
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user