diff --git a/packages/trilium-core/src/routes/api/search.spec.ts b/packages/trilium-core/src/routes/api/search.spec.ts index 4a1822b3ba..195b14eedb 100644 --- a/packages/trilium-core/src/routes/api/search.spec.ts +++ b/packages/trilium-core/src/routes/api/search.spec.ts @@ -15,6 +15,13 @@ async function createSearchNote(searchString: string): Promise { 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)", () => { let createdNoteId: string; @@ -95,6 +102,37 @@ describe("Search API (core)", () => { 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 () => { const { noteId } = await createTextNote(api, { title: "A template note" }); await api.post(`/api/notes/${noteId}/attributes`, { diff --git a/packages/trilium-core/src/routes/api/search.ts b/packages/trilium-core/src/routes/api/search.ts index b8b45f3a96..66cbe7f2bb 100644 --- a/packages/trilium-core/src/routes/api/search.ts +++ b/packages/trilium-core/src/routes/api/search.ts @@ -116,10 +116,11 @@ function quickSearch(req: Request<{ searchString: string }>) { const searchContext = new SearchContext({ fastSearch: false, includeArchivedNotes: false, - includeHiddenNotes: true, fuzzyAttributeSearch: 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); diff --git a/packages/trilium-core/src/services/search/search_context.ts b/packages/trilium-core/src/services/search/search_context.ts index f8aa027605..45964b5906 100644 --- a/packages/trilium-core/src/services/search/search_context.ts +++ b/packages/trilium-core/src/services/search/search_context.ts @@ -51,8 +51,6 @@ class SearchContext { this.ancestorNoteId = params.ancestorNoteId; 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(); } diff --git a/packages/trilium-core/src/test/api_tester.ts b/packages/trilium-core/src/test/api_tester.ts index 04556221c3..ebec3f0557 100644 --- a/packages/trilium-core/src/test/api_tester.ts +++ b/packages/trilium-core/src/test/api_tester.ts @@ -215,14 +215,19 @@ export class CoreApiTester { private registerAll() { const apiRoute = (method: HttpMethod, path: string, handler: Handler) => this.add(method, path, async (req) => { - const result = await getContext().init(() => - getSql().transactional(() => handler(req))); + const result = await getContext().init(() => { + seedContext(req); + return getSql().transactional(() => handler(req)); + }); return formatApiResult(result); }); const asyncApiRoute = (method: HttpMethod, path: string, handler: Handler) => 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); }); @@ -238,8 +243,14 @@ export class CoreApiTester { const mockRes = createMockResponse(); const invoke = () => handler(req, mockRes); const result = transactional - ? await getContext().init(() => getSql().transactional(invoke)) - : await getContext().init(async () => await invoke()); + ? await getContext().init(() => { + seedContext(req); + return getSql().transactional(invoke); + }) + : await getContext().init(async () => { + seedContext(req); + return await invoke(); + }); if (mockRes.used) { 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"); +}