Fix global search result navigation
This commit is contained in:
parent
b28039cf0d
commit
578ce2f2a5
|
|
@ -46,6 +46,47 @@ test.describe('UX follow-up fixes', () => {
|
|||
await expect(page.locator('.files-item')).toContainText('Overview.md');
|
||||
});
|
||||
|
||||
test('global search opens indexed browser inbox results', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
||||
'captures:workspace:Project': [{
|
||||
captureId: 'capture-search-1',
|
||||
capturedAt: '2026-06-30T08:00:00.000Z',
|
||||
kind: 'page',
|
||||
url: 'https://example.com/research',
|
||||
title: 'Research Search Result',
|
||||
domain: 'example.com',
|
||||
text: 'Searchable captured browser text',
|
||||
workspaceRootPath: 'Project',
|
||||
browserName: 'Firefox',
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
const search = page.locator('[data-global-search-input]');
|
||||
await search.fill('Research Search Result');
|
||||
const result = page.locator('[data-global-search-result-type="Browser Inbox"]').filter({ hasText: 'Research Search Result' });
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
await result.click();
|
||||
|
||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.browser-inbox-detail-title')).toHaveText('Research Search Result');
|
||||
});
|
||||
|
||||
test('workspace Search input keeps focus while typing', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await page.getByRole('tab', { name: 'Search' }).click();
|
||||
const searchInput = page.locator('[data-search-input="query"]');
|
||||
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await searchInput.click();
|
||||
await page.keyboard.press('p');
|
||||
await expect(searchInput).toBeFocused();
|
||||
await page.keyboard.press('r');
|
||||
await expect(searchInput).toBeFocused();
|
||||
await expect(searchInput).toHaveValue('pr');
|
||||
});
|
||||
|
||||
test('plugin settings modal gives complex panels enough space', async ({ page }) => {
|
||||
await openPluginManager(page);
|
||||
await page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).getByRole('button', { name: 'Settings' }).click();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
let focused = false;
|
||||
let loading = true;
|
||||
let searchTimer = null;
|
||||
let buildSeq = 0;
|
||||
|
||||
$: scheduleSearch(query);
|
||||
|
||||
|
|
@ -114,7 +115,14 @@
|
|||
return String(text || '').slice(0, 900);
|
||||
}
|
||||
|
||||
async function indexPluginSettings(pluginId, label, rank) {
|
||||
function pluginToolKind(pluginId, label) {
|
||||
if (pluginId === 'verstak.browser-inbox') return 'browser-inbox';
|
||||
if (pluginId === 'verstak.activity') return 'activity';
|
||||
if (pluginId === 'verstak.journal') return 'journal';
|
||||
return String(label || pluginId || '').toLowerCase();
|
||||
}
|
||||
|
||||
async function indexPluginSettings(pluginId, label, rank, view, nodes) {
|
||||
const settings = await resultOrEmpty(App.ReadPluginSettings(pluginId), {});
|
||||
const items = [];
|
||||
Object.keys(settings || {}).forEach(key => {
|
||||
|
|
@ -123,12 +131,19 @@
|
|||
rows.forEach(row => {
|
||||
if (!row || typeof row !== 'object') return;
|
||||
const title = row.title || row.summary || row.url || row.captureId || row.activityId || row.entryId || label;
|
||||
const workspaceName = row.workspaceRootPath || row.workspaceName || '';
|
||||
items.push({
|
||||
type: label,
|
||||
title,
|
||||
subtitle: row.url || row.summary || row.workspaceRootPath || key,
|
||||
keywords: JSON.stringify(row),
|
||||
rank,
|
||||
action: workspaceName ? 'workspace-tool' : (view ? 'view' : ''),
|
||||
viewId: view?.id || '',
|
||||
pluginId,
|
||||
workspaceName,
|
||||
toolKind: pluginToolKind(pluginId, label),
|
||||
nodes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -136,6 +151,7 @@
|
|||
}
|
||||
|
||||
async function buildIndex() {
|
||||
const seq = ++buildSeq;
|
||||
loading = true;
|
||||
const next = [];
|
||||
|
||||
|
|
@ -155,6 +171,10 @@
|
|||
});
|
||||
|
||||
const contributions = await resultOrEmpty(App.GetContributions(), {});
|
||||
const viewByPluginId = new Map();
|
||||
(contributions.views || []).forEach(view => {
|
||||
if (view.pluginId && !viewByPluginId.has(view.pluginId)) viewByPluginId.set(view.pluginId, view);
|
||||
});
|
||||
(contributions.sidebarItems || []).forEach(item => {
|
||||
next.push({
|
||||
type: 'Tool',
|
||||
|
|
@ -185,16 +205,22 @@
|
|||
}
|
||||
|
||||
const pluginItems = await Promise.all([
|
||||
indexPluginSettings('verstak.journal', 'Journal', 50),
|
||||
indexPluginSettings('verstak.browser-inbox', 'Browser Inbox', 55),
|
||||
indexPluginSettings('verstak.activity', 'Activity', 60),
|
||||
indexPluginSettings('verstak.journal', 'Journal', 50, viewByPluginId.get('verstak.journal'), nodes),
|
||||
indexPluginSettings('verstak.browser-inbox', 'Browser Inbox', 55, viewByPluginId.get('verstak.browser-inbox'), nodes),
|
||||
indexPluginSettings('verstak.activity', 'Activity', 60, viewByPluginId.get('verstak.activity'), nodes),
|
||||
]);
|
||||
|
||||
if (seq !== buildSeq) return;
|
||||
index = next.concat(pluginItems.flat());
|
||||
loading = false;
|
||||
runSearch(query);
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
focused = true;
|
||||
buildIndex();
|
||||
}
|
||||
|
||||
async function openResult(item) {
|
||||
query = '';
|
||||
results = [];
|
||||
|
|
@ -210,6 +236,18 @@
|
|||
}));
|
||||
return;
|
||||
}
|
||||
if (item.action === 'workspace-tool') {
|
||||
const workspaceName = item.workspaceName || '';
|
||||
if (workspaceName) {
|
||||
window.dispatchEvent(new CustomEvent('verstak:workspace-selected', {
|
||||
detail: { workspaceName, nodes: item.nodes || [] }
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', {
|
||||
detail: { kind: item.toolKind || item.type || '' }
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (item.action === 'file-folder') {
|
||||
const parts = String(item.path || '').split('/').filter(Boolean);
|
||||
const workspaceName = parts[0] || '';
|
||||
|
|
@ -252,7 +290,7 @@
|
|||
<Icon name="search" size={14} class="global-search-icon" />
|
||||
<input
|
||||
bind:value={query}
|
||||
on:focus={() => focused = true}
|
||||
on:focus={handleFocus}
|
||||
on:blur={() => setTimeout(() => focused = false, 120)}
|
||||
type="search"
|
||||
placeholder={loading ? 'Индексируем...' : 'Поиск'}
|
||||
|
|
|
|||
|
|
@ -220,8 +220,11 @@
|
|||
icon: 'search',
|
||||
provides: ['verstak/search/v1', 'search.provider'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register'],
|
||||
permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register', 'events.subscribe', 'commands.register'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
workspaceItems: [{ id: 'verstak.search.workspace', title: 'Search', icon: 'search', component: 'SearchView' }],
|
||||
commands: [{ id: 'verstak.search.searchVaultText', title: 'Search Vault Text', handler: 'verstak.search.searchVaultText' }],
|
||||
searchProviders: [{ id: 'verstak.search.vault-text', label: 'Vault Text Search', handler: 'verstak.search.searchVaultText' }]
|
||||
}
|
||||
},
|
||||
|
|
@ -1422,6 +1425,102 @@
|
|||
}.toString() + ')();';
|
||||
}
|
||||
|
||||
function searchPluginBundle() {
|
||||
return '(' + function () {
|
||||
function el(tag, attrs, children) {
|
||||
var node = document.createElement(tag);
|
||||
attrs = attrs || {};
|
||||
Object.keys(attrs).forEach(function (key) {
|
||||
if (attrs[key] == null) return;
|
||||
if (key === 'className') node.className = attrs[key];
|
||||
else if (key.indexOf('on') === 0) node.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
|
||||
else if (key === 'textContent') node.textContent = attrs[key];
|
||||
else node.setAttribute(key, attrs[key]);
|
||||
});
|
||||
(children || []).forEach(function (child) {
|
||||
if (child == null) return;
|
||||
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
||||
});
|
||||
return node;
|
||||
}
|
||||
function clean(path) { return String(path || '').split('/').filter(Boolean).join('/'); }
|
||||
function SearchView(containerEl, props, api) {
|
||||
if (!document.getElementById('mock-search-style')) {
|
||||
var style = document.createElement('style');
|
||||
style.id = 'mock-search-style';
|
||||
style.textContent = '.search-root{height:100%;min-height:0;display:flex;flex-direction:column;background:#0d0d1a;color:#e0e0e0}.search-toolbar{display:flex;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid #16213e;background:#12122a}.search-input{flex:1;min-width:180px;font-size:.86rem;padding:.42rem .55rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}.search-input:focus{border-color:#4ecca3}.search-btn{font-size:.8rem;padding:.42rem .7rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ddd}.search-scope,.search-status{font-size:.78rem;color:#8b8ba8}.search-status{padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}.search-results{flex:1;min-height:0;overflow:auto}.search-empty{padding:2rem;color:#666;text-align:center}.search-result{padding:.7rem .85rem;border-bottom:1px solid rgba(22,33,62,.55)}.search-path{color:#4ecca3}.search-snippet{margin-top:.25rem;color:#cfcfe0;font-size:.8rem}';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.className = 'search-root';
|
||||
containerEl.setAttribute('data-plugin-id', 'verstak.search');
|
||||
var rootPath = clean(props && (props.workspaceRootPath || props.workspaceName));
|
||||
var query = '';
|
||||
var timer = null;
|
||||
var results = [];
|
||||
var input = el('input', { className: 'search-input', type: 'search', placeholder: 'Search files, folders, text', 'data-search-input': 'query' });
|
||||
var button = el('button', { className: 'search-btn', 'data-search-action': 'run', textContent: 'Search' });
|
||||
var status = el('div', { className: 'search-status', textContent: 'Enter at least 2 characters.' });
|
||||
var list = el('div', { className: 'search-results' });
|
||||
containerEl.appendChild(el('div', { className: 'search-toolbar' }, [
|
||||
input,
|
||||
button,
|
||||
el('span', { className: 'search-scope', title: rootPath || 'Vault' }, [rootPath || 'Vault'])
|
||||
]));
|
||||
containerEl.appendChild(status);
|
||||
containerEl.appendChild(list);
|
||||
function render() {
|
||||
list.innerHTML = '';
|
||||
if (!results.length) {
|
||||
list.appendChild(el('div', { className: 'search-empty' }, [query.length < 2 ? 'Enter at least 2 characters.' : 'No results']));
|
||||
return;
|
||||
}
|
||||
results.forEach(function (item) {
|
||||
list.appendChild(el('div', { className: 'search-result' }, [
|
||||
el('div', { className: 'search-path', textContent: item.relativePath }),
|
||||
el('div', { className: 'search-snippet', textContent: item.name })
|
||||
]));
|
||||
});
|
||||
}
|
||||
async function run() {
|
||||
query = input.value.trim();
|
||||
if (query.length < 2) {
|
||||
results = [];
|
||||
status.textContent = 'Enter at least 2 characters.';
|
||||
render();
|
||||
return;
|
||||
}
|
||||
var entries = await api.files.list(rootPath);
|
||||
var needle = query.toLowerCase();
|
||||
results = (Array.isArray(entries) ? entries : []).filter(function (item) {
|
||||
return String(item.name || item.relativePath || '').toLowerCase().indexOf(needle) !== -1;
|
||||
});
|
||||
status.textContent = results.length + ' result' + (results.length === 1 ? '' : 's');
|
||||
render();
|
||||
}
|
||||
function schedule() {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(run, 100);
|
||||
}
|
||||
input.addEventListener('input', schedule);
|
||||
button.addEventListener('click', run);
|
||||
render();
|
||||
containerEl.__searchMockCleanup = function () { if (timer) clearTimeout(timer); };
|
||||
}
|
||||
window.VerstakPluginRegister('verstak.search', {
|
||||
components: {
|
||||
SearchView: {
|
||||
mount: SearchView,
|
||||
unmount: function (containerEl) {
|
||||
if (containerEl.__searchMockCleanup) containerEl.__searchMockCleanup();
|
||||
containerEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}.toString() + ')();';
|
||||
}
|
||||
|
||||
function platformTestBundle() {
|
||||
return [
|
||||
"(function(){",
|
||||
|
|
@ -1665,6 +1764,9 @@
|
|||
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(browserInboxBundle());
|
||||
}
|
||||
if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(searchPluginBundle());
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
GetPluginCapability: function (pluginId, capId) {
|
||||
|
|
@ -2226,8 +2328,11 @@
|
|||
icon: 'search',
|
||||
provides: ['verstak/search/v1', 'search.provider'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register'],
|
||||
permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register', 'events.subscribe', 'commands.register'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
workspaceItems: [{ id: 'verstak.search.workspace', title: 'Search', icon: 'search', component: 'SearchView' }],
|
||||
commands: [{ id: 'verstak.search.searchVaultText', title: 'Search Vault Text', handler: 'verstak.search.searchVaultText' }],
|
||||
searchProviders: [{ id: 'verstak.search.vault-text', label: 'Vault Text Search', handler: 'verstak.search.searchVaultText' }]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue