mirror of
https://github.com/zadam/trilium.git
synced 2026-09-12 19:50:20 +05:00
fix(search): confine quick search to the hoisted subtree (closes #10803)
Quick search widened its scope to the whole tree whenever the hoisted note lived inside _hidden, so browsing the in-app help returned the user's own notes instead of help pages. It also passed includeHiddenNotes, which made an unhoisted quick search return notes that have no visible path at all. Neither setting was chosen for quick search. Both arrived together in8e227a6146, a commit about highlighting exact matches, which copied the search context of searchNotesForAutocomplete verbatim into the quickSearch route. The widening belongs to autocomplete alone. It was added in6a9ac6f90afor the launcher work: while hoisted into the hidden configuration area, the note-link autocomplete still has to reach normal notes so a launcher can point at one. It first sat in the SearchContext constructor, where it applied to every search that passed no explicit ancestor, and88bc7402a2deliberately narrowed it to the one function that needs it, introducing includeHiddenNotes and the IsHiddenExp filter at the same time. That commit's subject states the intent this one restores: hidden notes should not appear in the global search unless hoisted into it. Quick search now scopes to the hoisted note with no special case, so a hoist into the hidden subtree confines it there like any other hoist. The two settings have to move together: getAncestorExp only applies the hidden filter when the ancestor is root, so dropping includeHiddenNotes while keeping the widening would leave a hoist into help searching everything except help. Hoisting to _hidden itself now yields nothing, because getSubtree defaults to includeHidden: false and stops at that node. Hidden descendants, help and the launcher bar included, traverse normally. Letting AncestorExp traverse hidden notes would fix that, but the expression also backs saved searches and ETAPI queries, so it is left for a separate change. CoreApiTester never seeded the execution context from request headers, so no spec could describe a hoisted request; the route read "root" whatever the spec did. It now seeds hoistedNoteId, componentId and localNowDateTime from the trilium-* headers, matching route_api.ts on Express and browser_routes.ts in standalone. The new specs create their hidden note under _lbBookmarks because the API refuses children directly under _hidden and _help*. The comment in SearchContext explaining the widening has documented a condition that no longer exists since88bc7402a2and is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73edb95583
commit
7edff12d9b
@ -15,6 +15,13 @@ async function createSearchNote(searchString: string): Promise<string> {
|
|||||||
return created.body.noteId;
|
return created.body.noteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Runs a quick search as a client hoisted into `hoistedNoteId`, or unhoisted when it is omitted. */
|
||||||
|
function quickSearch(searchString: string, hoistedNoteId?: string) {
|
||||||
|
return api.get<{ searchResultNoteIds: string[] }>(`/api/quick-search/${searchString}`, {
|
||||||
|
headers: hoistedNoteId ? { "trilium-hoisted-note-id": hoistedNoteId } : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("Search API (core)", () => {
|
describe("Search API (core)", () => {
|
||||||
let createdNoteId: string;
|
let createdNoteId: string;
|
||||||
|
|
||||||
@ -95,6 +102,37 @@ describe("Search API (core)", () => {
|
|||||||
expect(res.body.highlightedTokens).not.toContain(pattern);
|
expect(res.body.highlightedTokens).not.toContain(pattern);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("confines a quick search to the hoisted subtree, hidden subtree included", async () => {
|
||||||
|
const token = "ZzHoistedQuickSearchQwerty";
|
||||||
|
const workspace = await createTextNote(api, { title: "Workspace" });
|
||||||
|
const inside = await createTextNote(api, { parentNoteId: workspace.noteId, title: `${token} inside` });
|
||||||
|
const outside = await createTextNote(api, { title: `${token} outside` });
|
||||||
|
const hidden = await createTextNote(api, { parentNoteId: "_lbBookmarks", title: `${token} hidden` });
|
||||||
|
|
||||||
|
const inWorkspace = await quickSearch(token, workspace.noteId);
|
||||||
|
expect(inWorkspace.status).toBe(200);
|
||||||
|
expect(inWorkspace.body.searchResultNoteIds).toContain(inside.noteId);
|
||||||
|
expect(inWorkspace.body.searchResultNoteIds).not.toContain(outside.noteId);
|
||||||
|
|
||||||
|
// A hoist into the hidden subtree scopes the search to that subtree like any other hoist,
|
||||||
|
// so browsing the in-app help does not surface the user's own notes.
|
||||||
|
const inHidden = await quickSearch(token, "_lbBookmarks");
|
||||||
|
expect(inHidden.status).toBe(200);
|
||||||
|
expect(inHidden.body.searchResultNoteIds).toContain(hidden.noteId);
|
||||||
|
expect(inHidden.body.searchResultNoteIds).not.toContain(outside.noteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits hidden notes from an unhoisted quick search", async () => {
|
||||||
|
const token = "ZzUnhoistedQuickSearchQwerty";
|
||||||
|
const visible = await createTextNote(api, { title: `${token} visible` });
|
||||||
|
const hidden = await createTextNote(api, { parentNoteId: "_lbBookmarks", title: `${token} hidden` });
|
||||||
|
|
||||||
|
const res = await quickSearch(token);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.searchResultNoteIds).toContain(visible.noteId);
|
||||||
|
expect(res.body.searchResultNoteIds).not.toContain(hidden.noteId);
|
||||||
|
});
|
||||||
|
|
||||||
it("lists template note ids including a freshly-labelled template", async () => {
|
it("lists template note ids including a freshly-labelled template", async () => {
|
||||||
const { noteId } = await createTextNote(api, { title: "A template note" });
|
const { noteId } = await createTextNote(api, { title: "A template note" });
|
||||||
await api.post(`/api/notes/${noteId}/attributes`, {
|
await api.post(`/api/notes/${noteId}/attributes`, {
|
||||||
|
|||||||
@ -116,10 +116,11 @@ function quickSearch(req: Request<{ searchString: string }>) {
|
|||||||
const searchContext = new SearchContext({
|
const searchContext = new SearchContext({
|
||||||
fastSearch: false,
|
fastSearch: false,
|
||||||
includeArchivedNotes: false,
|
includeArchivedNotes: false,
|
||||||
includeHiddenNotes: true,
|
|
||||||
fuzzyAttributeSearch: true,
|
fuzzyAttributeSearch: true,
|
||||||
ignoreInternalAttributes: true,
|
ignoreInternalAttributes: true,
|
||||||
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree() ? "root" : hoistedNoteService.getHoistedNoteId()
|
// Quick search covers the subtree the user is looking at, so a hoist into the hidden
|
||||||
|
// subtree scopes it there too. Only link autocomplete widens to root, for link targets.
|
||||||
|
ancestorNoteId: hoistedNoteService.getHoistedNoteId()
|
||||||
});
|
});
|
||||||
|
|
||||||
const trimmed = searchService.findResultsWithQuery(searchString, searchContext).slice(0, 200);
|
const trimmed = searchService.findResultsWithQuery(searchString, searchContext).slice(0, 200);
|
||||||
|
|||||||
@ -51,8 +51,6 @@ class SearchContext {
|
|||||||
this.ancestorNoteId = params.ancestorNoteId;
|
this.ancestorNoteId = params.ancestorNoteId;
|
||||||
|
|
||||||
if (!this.ancestorNoteId && !this.ignoreHoistedNote) {
|
if (!this.ancestorNoteId && !this.ignoreHoistedNote) {
|
||||||
// hoisting in hidden subtree should not limit autocomplete
|
|
||||||
// since we want to link (create relations) to the normal non-hidden notes
|
|
||||||
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -215,14 +215,19 @@ export class CoreApiTester {
|
|||||||
private registerAll() {
|
private registerAll() {
|
||||||
const apiRoute = (method: HttpMethod, path: string, handler: Handler) =>
|
const apiRoute = (method: HttpMethod, path: string, handler: Handler) =>
|
||||||
this.add(method, path, async (req) => {
|
this.add(method, path, async (req) => {
|
||||||
const result = await getContext().init(() =>
|
const result = await getContext().init(() => {
|
||||||
getSql().transactional(() => handler(req)));
|
seedContext(req);
|
||||||
|
return getSql().transactional(() => handler(req));
|
||||||
|
});
|
||||||
return formatApiResult(result);
|
return formatApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
const asyncApiRoute = (method: HttpMethod, path: string, handler: Handler) =>
|
const asyncApiRoute = (method: HttpMethod, path: string, handler: Handler) =>
|
||||||
this.add(method, path, async (req) => {
|
this.add(method, path, async (req) => {
|
||||||
const result = await getContext().init(async () => await handler(req));
|
const result = await getContext().init(async () => {
|
||||||
|
seedContext(req);
|
||||||
|
return await handler(req);
|
||||||
|
});
|
||||||
return formatApiResult(result);
|
return formatApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,8 +243,14 @@ export class CoreApiTester {
|
|||||||
const mockRes = createMockResponse();
|
const mockRes = createMockResponse();
|
||||||
const invoke = () => handler(req, mockRes);
|
const invoke = () => handler(req, mockRes);
|
||||||
const result = transactional
|
const result = transactional
|
||||||
? await getContext().init(() => getSql().transactional(invoke))
|
? await getContext().init(() => {
|
||||||
: await getContext().init(async () => await invoke());
|
seedContext(req);
|
||||||
|
return getSql().transactional(invoke);
|
||||||
|
})
|
||||||
|
: await getContext().init(async () => {
|
||||||
|
seedContext(req);
|
||||||
|
return await invoke();
|
||||||
|
});
|
||||||
|
|
||||||
if (mockRes.used) {
|
if (mockRes.used) {
|
||||||
return mockRes.snapshot();
|
return mockRes.snapshot();
|
||||||
@ -363,3 +374,15 @@ export class CoreApiTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds the execution context from the `trilium-*` request headers, the way both real adapters do
|
||||||
|
* (`route_api.ts` on Express, `browser_routes.ts` in standalone). Routes that read the hoisted note
|
||||||
|
* through `cls.getHoistedNoteId()` need this to see anything other than "root".
|
||||||
|
*/
|
||||||
|
function seedContext(req: ApiRequest) {
|
||||||
|
const ctx = getContext();
|
||||||
|
ctx.set("componentId", req.get("trilium-component-id"));
|
||||||
|
ctx.set("localNowDateTime", req.get("trilium-local-now-datetime"));
|
||||||
|
ctx.set("hoistedNoteId", req.get("trilium-hoisted-note-id") || "root");
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user