diff --git a/apps/client/src/entities/fnote.spec.ts b/apps/client/src/entities/fnote.spec.ts index f988446c0e..2ddf841444 100644 --- a/apps/client/src/entities/fnote.spec.ts +++ b/apps/client/src/entities/fnote.spec.ts @@ -714,6 +714,33 @@ describe("FNote paths & hierarchy", () => { expect(leaf.getBestNotePathString()).toContain("sharedLeaf"); }); + it("keeps a bookmark clone out of the best path while a hidden note is active", () => { + // Bookmarking clones the note under `_lbBookmarks`, so its children gain a second path + // through `_hidden`. A search activates a search note that lives under `_hidden` as well, + // and that shared prefix must not promote the bookmark path over the real one. + const root = froca.notes["root"] ?? buildNote({ id: "root", title: "root" }); + const hidden = froca.notes["_hidden"] ?? buildNote({ id: "_hidden", title: "Hidden" }); + wireChild(root, hidden); + + let launchBarParent = hidden; + for (const id of ["_lbRoot", "_lbVisibleLaunchers", "_lbBookmarks"]) { + const launcher = buildNote({ id, title: id }); + wireChild(launchBarParent, launcher); + launchBarParent = launcher; + } + + const inbox = buildNote({ id: "bmInbox", title: "Inbox" }); + wireChild(root, inbox); + wireChild(launchBarParent, inbox); + + const leaf = buildNote({ id: "bmLeaf", title: "ThisIsTest" }); + wireChild(inbox, leaf); + + expect(leaf.getAllNotePaths()).toHaveLength(2); + expect(leaf.getBestNotePath("root", "root/_hidden/_search/202609/searchNoteId")) + .toEqual(["root", "bmInbox", "bmLeaf"]); + }); + it("getSortedNotePathRecords orders by archived / hidden / search / length without an active path", () => { const leaf = froca.notes["sharedLeaf"]; expect(leaf).toBeDefined(); @@ -1269,6 +1296,14 @@ function makeBlob(content: string | undefined): FBlob { }); } +/** Wires one froca note as a child of another, the way a branch does in the application. */ +function wireChild(parent: FNote, child: FNote) { + const branchId = `br-${parent.noteId}-${child.noteId}`; + registerBranch(branchId, child.noteId, parent.noteId, 0); + parent.addChild(child.noteId, branchId, false); + child.addParent(parent.noteId, branchId, false); +} + function registerBranch(branchId: string, noteId: string, parentNoteId: string, notePosition: number | undefined) { froca.branches[branchId] = new FBranch(froca, { branchId, diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 90bb7296f9..1d447d5fa2 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -453,16 +453,6 @@ export default class FNote { }; notePaths.sort((a, b) => { - if (activeNotePath) { - const activeSegments = activeNotePath.split('/'); - const aOverlap = prefixMatchLength(a.notePath, activeSegments); - const bOverlap = prefixMatchLength(b.notePath, activeSegments); - // Paths with more matching prefix segments are prioritized - // when the match count is equal, other criteria are used for sorting - if (bOverlap !== aOverlap) { - return bOverlap - aOverlap; - } - } if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { return a.isInHoistedSubTree ? -1 : 1; } else if (a.isArchived !== b.isArchived) { @@ -474,6 +464,19 @@ export default class FNote { return a.isSearch ? 1 : -1; } /* v8 ignore stop */ + + if (activeNotePath) { + // Among otherwise equal paths, the one sharing the longest prefix with the active + // note wins, so opening a clone keeps the user where they came from. The checks + // above outrank it: an active `_hidden` note must not promote a bookmark clone. + const activeSegments = activeNotePath.split("/"); + const aOverlap = prefixMatchLength(a.notePath, activeSegments); + const bOverlap = prefixMatchLength(b.notePath, activeSegments); + if (bOverlap !== aOverlap) { + return bOverlap - aOverlap; + } + } + return a.notePath.length - b.notePath.length; });