diff --git a/apps/web/src/lib/utils.test.mjs b/apps/web/src/lib/utils.test.mjs new file mode 100644 index 00000000..bea81c7e --- /dev/null +++ b/apps/web/src/lib/utils.test.mjs @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test"; +import { buildNotebookTree } from "./utils.ts"; + +const notebook = (id, parentId, memoCount, lastMemoUpdatedAt = null) => ({ + id, + parentId, + name: id, + slug: null, + icon: null, + color: null, + sortOrder: 0, + memoCount, + lastMemoUpdatedAt, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", +}); + +describe("buildNotebookTree", () => { + test("rolls descendant memo counts and update times into every ancestor", () => { + const [root] = buildNotebookTree([ + notebook("root", null, 2, "2026-01-02T00:00:00.000Z"), + notebook("child", "root", 3, "2026-01-03T00:00:00.000Z"), + notebook("grandchild", "child", 5, "2026-01-04T00:00:00.000Z"), + ]); + + expect(root.memoCount).toBe(10); + expect(root.lastMemoUpdatedAt).toBe("2026-01-04T00:00:00.000Z"); + expect(root.children[0].memoCount).toBe(8); + expect(root.children[0].lastMemoUpdatedAt).toBe("2026-01-04T00:00:00.000Z"); + expect(root.children[0].children[0].memoCount).toBe(5); + }); + + test("sorts siblings after descendant counts have been rolled up", () => { + const roots = buildNotebookTree( + [ + notebook("larger-subtree", null, 0), + notebook("child", "larger-subtree", 4), + notebook("smaller-subtree", null, 3), + ], + (first, second) => second.memoCount - first.memoCount, + ); + + expect(roots.map((node) => node.id)).toEqual(["larger-subtree", "smaller-subtree"]); + expect(roots.map((node) => node.memoCount)).toEqual([4, 3]); + }); + + test("does not mutate the notebook records returned by the repository", () => { + const root = notebook("root", null, 1); + const child = notebook("child", "root", 2); + + buildNotebookTree([root, child]); + + expect(root.memoCount).toBe(1); + expect(root).not.toHaveProperty("children"); + }); +}); diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index a3ef6ba2..c5eb8d9d 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -29,12 +29,23 @@ export const buildNotebookTree = (notebooks: Notebook[], compareNodes: NotebookN } } - const sortNodes = (items: NotebookNode[]) => { + const rollUpAndSortNodes = (items: NotebookNode[]) => { + for (const item of items) { + rollUpAndSortNodes(item.children); + + for (const child of item.children) { + item.memoCount += child.memoCount; + + if (child.lastMemoUpdatedAt && (!item.lastMemoUpdatedAt || child.lastMemoUpdatedAt > item.lastMemoUpdatedAt)) { + item.lastMemoUpdatedAt = child.lastMemoUpdatedAt; + } + } + } + items.sort(compareNodes); - items.forEach((item) => sortNodes(item.children)); }; - sortNodes(roots); + rollUpAndSortNodes(roots); return roots; };