Merge remote-tracking branch 'gitea/develop' into feat/page-templates
# Conflicts: # apps/server/src/integrations/throttle/throttle.module.ts # apps/server/src/integrations/throttle/throttler-names.ts
This commit is contained in:
40
apps/client/src/features/page/tree/utils/utils.test.ts
Normal file
40
apps/client/src/features/page/tree/utils/utils.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildTree } from "./utils";
|
||||
import type { IPage } from "@/features/page/types/page.types.ts";
|
||||
|
||||
function page(id: string, position: string): IPage {
|
||||
return {
|
||||
id,
|
||||
slugId: `slug-${id}`,
|
||||
title: id.toUpperCase(),
|
||||
icon: "",
|
||||
position,
|
||||
hasChildren: false,
|
||||
spaceId: "space-1",
|
||||
parentPageId: null as unknown as string,
|
||||
} as IPage;
|
||||
}
|
||||
|
||||
describe("buildTree", () => {
|
||||
it("builds one node per unique page", () => {
|
||||
const tree = buildTree([page("a", "a1"), page("b", "a2")]);
|
||||
expect(tree.map((n) => n.id)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("dedups a duplicate id so the tree has no duplicate node", () => {
|
||||
// A realtime cache write could append a page twice; buildTree must not emit
|
||||
// two references to the same node (which would crash the sidebar render with
|
||||
// a duplicate React key).
|
||||
const tree = buildTree([
|
||||
page("a", "a1"),
|
||||
page("b", "a2"),
|
||||
page("a", "a1"), // duplicate id
|
||||
]);
|
||||
|
||||
expect(tree).toHaveLength(2);
|
||||
expect(tree.map((n) => n.id).sort()).toEqual(["a", "b"]);
|
||||
// No id appears more than once.
|
||||
const ids = tree.map((n) => n.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,14 @@ export function buildTree(pages: IPage[]): SpaceTreeNode[] {
|
||||
};
|
||||
});
|
||||
|
||||
// Defense-in-depth: a duplicate id in `pages` would push two references to the
|
||||
// same node, producing a duplicate React key that crashes the sidebar render.
|
||||
// Track ids we've already pushed and skip repeats so a stray duplicate from a
|
||||
// realtime cache write can never break the tree.
|
||||
const seen = new Set<string>();
|
||||
pages.forEach((page) => {
|
||||
if (seen.has(page.id)) return;
|
||||
seen.add(page.id);
|
||||
tree.push(pageMap[page.id]);
|
||||
});
|
||||
|
||||
@@ -217,3 +224,33 @@ export function mergeRootTrees(
|
||||
|
||||
return sortPositionKeys(merged);
|
||||
}
|
||||
|
||||
// Collect every node id in the tree (roots, branches, leaves). Used by
|
||||
// collapseAll to clear the open-state map for all current-space nodes.
|
||||
export function collectAllIds(nodes: SpaceTreeNode[]): string[] {
|
||||
const ids: string[] = [];
|
||||
const walk = (list: SpaceTreeNode[]) => {
|
||||
for (const n of list) {
|
||||
ids.push(n.id);
|
||||
if (n.children?.length) walk(n.children);
|
||||
}
|
||||
};
|
||||
walk(nodes);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Collect ids of branch nodes (nodes that have children). Used by expandAll to
|
||||
// open every branch in the open-state map; leaves need no entry.
|
||||
export function collectBranchIds(nodes: SpaceTreeNode[]): string[] {
|
||||
const ids: string[] = [];
|
||||
const walk = (list: SpaceTreeNode[]) => {
|
||||
for (const n of list) {
|
||||
if (n.children?.length) {
|
||||
ids.push(n.id);
|
||||
walk(n.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(nodes);
|
||||
return ids;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user