feat(kernel): finish phase3 bridge cutover and phase4 tree projection
- add generic mnote-web query transport and bridge routes for workspace/request/trace - route sidebar compat traffic through mnote-web and tighten fixture fallback to dev/test - unify sidebar/page tree/file tree/picker consumers on page_tree projection - sync phase3/phase4 checklist, breakdown docs, and harness progress state
This commit is contained in:
@@ -15,6 +15,8 @@ export interface DocumentNode extends DocumentRecord {
|
||||
children: DocumentNode[];
|
||||
}
|
||||
|
||||
// 过渡兼容 helper:保留给旧单测和非树域场景使用。
|
||||
// Sidebar / 页面树 / 文件树 / move-embed picker 主路径已统一改读 projection family。
|
||||
export function buildDocumentTree(records: DocumentRecord[]): DocumentNode[] {
|
||||
// 防御性处理:当上游数据意外包含重复 id 时,避免生成重复节点导致渲染 key 冲突。
|
||||
// 以“最后一次出现”为准(与原先 nodeMap.set 的覆盖行为保持一致)。
|
||||
@@ -33,7 +35,6 @@ export function buildDocumentTree(records: DocumentRecord[]): DocumentNode[] {
|
||||
uniqueRecords.reverse();
|
||||
|
||||
if (duplicatedIds.size > 0 && process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[buildDocumentTree] 检测到重复文档 id(已自动去重):${Array.from(duplicatedIds).slice(0, 10).join(", ")}${duplicatedIds.size > 10 ? "…" : ""}`,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildPageTreeProjectionItems } from "@/lib/tree-projection";
|
||||
import { buildVisibleRows } from "./rows";
|
||||
import { parseFileTreeRowId } from "./types";
|
||||
|
||||
@@ -33,7 +34,7 @@ describe("buildVisibleRows", () => {
|
||||
};
|
||||
|
||||
const rows = buildVisibleRows({
|
||||
nodes: [a],
|
||||
pageRows: buildPageTreeProjectionItems([a]),
|
||||
expanded: new Set(["a"]),
|
||||
assetsByDoc: {
|
||||
a: [
|
||||
@@ -108,7 +109,7 @@ describe("buildVisibleRows", () => {
|
||||
};
|
||||
|
||||
const rows = buildVisibleRows({
|
||||
nodes: [a, a],
|
||||
pageRows: buildPageTreeProjectionItems([a, a]),
|
||||
expanded: new Set(["a"]),
|
||||
assetsByDoc: {},
|
||||
});
|
||||
|
||||
@@ -1,55 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import type { FileTreeRow } from "./types";
|
||||
import { makeAssetFolderRowId, makeAssetRowId, makeDocRowId, makeIndexRowId } from "./types";
|
||||
|
||||
export function buildVisibleRows({
|
||||
nodes,
|
||||
pageRows,
|
||||
expanded,
|
||||
assetsByDoc,
|
||||
assetChildrenByAssetId,
|
||||
expandedAssetFolderIds,
|
||||
}: {
|
||||
nodes: SidebarTreeNode[];
|
||||
// 只消费统一 page_tree projection;结构真相不再由文件树自行定义。
|
||||
pageRows: PageTreeProjectionItem[];
|
||||
expanded: Set<string>;
|
||||
assetsByDoc: Record<string, MediaAsset[]>;
|
||||
assetChildrenByAssetId?: Record<string, MediaAsset[]>;
|
||||
expandedAssetFolderIds?: Set<string>;
|
||||
}): FileTreeRow[] {
|
||||
const rows: FileTreeRow[] = [];
|
||||
const visitedDocIds = new Set<string>();
|
||||
|
||||
const walk = (node: SidebarTreeNode, depth: number) => {
|
||||
// 防御性处理:上游数据异常时(例如同一 docId 在树中重复出现),避免生成重复 rowId 导致 React key 冲突。
|
||||
// 同时也能避免潜在的“循环引用/重复引用”导致的递归问题。
|
||||
if (visitedDocIds.has(node.id)) return;
|
||||
visitedDocIds.add(node.id);
|
||||
|
||||
const assets = assetsByDoc[node.id] ?? [];
|
||||
const hasChildren = node.children.length > 0 || assets.length > 0;
|
||||
const isExpanded = expanded.has(node.id);
|
||||
pageRows.forEach((item) => {
|
||||
const assets = assetsByDoc[item.nodeId] ?? [];
|
||||
const hasChildren = item.childCount > 0 || assets.length > 0;
|
||||
const isExpanded = expanded.has(item.nodeId);
|
||||
rows.push({
|
||||
kind: "doc",
|
||||
rowId: makeDocRowId(node.id),
|
||||
depth,
|
||||
docId: node.id,
|
||||
parentDocId: node.parent_id,
|
||||
node,
|
||||
rowId: makeDocRowId(item.nodeId),
|
||||
depth: item.depth,
|
||||
docId: item.nodeId,
|
||||
parentDocId: item.parentNodeId,
|
||||
node: item.node,
|
||||
hasChildren,
|
||||
isExpanded,
|
||||
});
|
||||
|
||||
if (!isExpanded) return;
|
||||
if (!isExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
kind: "index",
|
||||
rowId: makeIndexRowId(node.id),
|
||||
depth: depth + 1,
|
||||
docId: node.id,
|
||||
parentDocId: node.id,
|
||||
node,
|
||||
rowId: makeIndexRowId(item.nodeId),
|
||||
depth: item.depth + 1,
|
||||
docId: item.nodeId,
|
||||
parentDocId: item.nodeId,
|
||||
node: item.node,
|
||||
});
|
||||
|
||||
assets.forEach((asset) => {
|
||||
@@ -60,9 +57,9 @@ export function buildVisibleRows({
|
||||
rows.push({
|
||||
kind: "asset-folder",
|
||||
rowId: makeAssetFolderRowId(asset.id),
|
||||
depth: depth + 1,
|
||||
docId: node.id,
|
||||
parentDocId: node.id,
|
||||
depth: item.depth + 1,
|
||||
docId: item.nodeId,
|
||||
parentDocId: item.nodeId,
|
||||
asset,
|
||||
hasChildren,
|
||||
isExpanded,
|
||||
@@ -72,9 +69,9 @@ export function buildVisibleRows({
|
||||
rows.push({
|
||||
kind: "asset",
|
||||
rowId: makeAssetRowId(child.id),
|
||||
depth: depth + 2,
|
||||
docId: node.id,
|
||||
parentDocId: node.id,
|
||||
depth: item.depth + 2,
|
||||
docId: item.nodeId,
|
||||
parentDocId: item.nodeId,
|
||||
asset: child,
|
||||
});
|
||||
});
|
||||
@@ -85,16 +82,12 @@ export function buildVisibleRows({
|
||||
rows.push({
|
||||
kind: "asset",
|
||||
rowId: makeAssetRowId(asset.id),
|
||||
depth: depth + 1,
|
||||
docId: node.id,
|
||||
parentDocId: node.id,
|
||||
depth: item.depth + 1,
|
||||
docId: item.nodeId,
|
||||
parentDocId: item.nodeId,
|
||||
asset,
|
||||
});
|
||||
});
|
||||
|
||||
node.children.forEach((child) => walk(child, depth + 1));
|
||||
};
|
||||
|
||||
nodes.forEach((node) => walk(node, 0));
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { headers } from "next/headers";
|
||||
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
|
||||
import { isDevAuthEnabled } from "@/lib/auth/devUser";
|
||||
import type { SidebarDatasetListQueryResult } from "@/lib/sidebar-data";
|
||||
|
||||
type MnoteWebSidebarCompatResponse = {
|
||||
ok?: boolean;
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
workspaceId?: string;
|
||||
result?: SidebarDatasetListQueryResult;
|
||||
};
|
||||
|
||||
const MNOTE_WEB_BASE_URL = (
|
||||
process.env.MNOTE_WEB_BASE_URL ??
|
||||
process.env.NEXT_PUBLIC_MNOTE_WEB_BASE_URL ??
|
||||
""
|
||||
).trim();
|
||||
|
||||
function copyHeaderIfPresent(target: Headers, source: Headers, name: string) {
|
||||
const value = source.get(name);
|
||||
if (value) {
|
||||
target.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildForwardHeaders(request?: Request): Promise<Headers> {
|
||||
const source = request?.headers ?? new Headers(await headers());
|
||||
const forwarded = new Headers();
|
||||
|
||||
copyHeaderIfPresent(forwarded, source, "cookie");
|
||||
copyHeaderIfPresent(forwarded, source, "authorization");
|
||||
copyHeaderIfPresent(forwarded, source, "x-request-id");
|
||||
copyHeaderIfPresent(forwarded, source, "x-trace-id");
|
||||
copyHeaderIfPresent(forwarded, source, "x-session-id");
|
||||
copyHeaderIfPresent(forwarded, source, "x-mnote-workspace-id");
|
||||
copyHeaderIfPresent(forwarded, source, "x-mnote-source-channel");
|
||||
copyHeaderIfPresent(forwarded, source, "x-mnote-source-client");
|
||||
copyHeaderIfPresent(forwarded, source, "user-agent");
|
||||
|
||||
if (!forwarded.has("authorization") && !isDevAuthEnabled()) {
|
||||
const token = await convexAuthNextjsToken();
|
||||
if (token?.trim()) {
|
||||
forwarded.set("authorization", `Bearer ${token.trim()}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!forwarded.has("x-mnote-source-channel")) {
|
||||
forwarded.set("x-mnote-source-channel", request ? "next_route" : "next_server_component");
|
||||
}
|
||||
if (!forwarded.has("x-mnote-source-client")) {
|
||||
forwarded.set("x-mnote-source-client", "wolai-frontend");
|
||||
}
|
||||
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
export function getMnoteWebBaseUrl(): string | null {
|
||||
return MNOTE_WEB_BASE_URL || null;
|
||||
}
|
||||
|
||||
export async function fetchSidebarDatasetFromMnoteWeb(input: {
|
||||
workspaceId: string;
|
||||
request?: Request;
|
||||
}): Promise<{
|
||||
dataset: SidebarDatasetListQueryResult;
|
||||
meta: {
|
||||
requestId: string | null;
|
||||
traceId: string | null;
|
||||
workspaceId: string;
|
||||
};
|
||||
}> {
|
||||
const baseUrl = getMnoteWebBaseUrl();
|
||||
if (!baseUrl) {
|
||||
throw new Error("未配置 MNOTE_WEB_BASE_URL");
|
||||
}
|
||||
|
||||
const url = new URL("/api/compat/next/sidebar", baseUrl);
|
||||
url.searchParams.set("workspaceId", input.workspaceId);
|
||||
|
||||
const forwardedHeaders = await buildForwardHeaders(input.request);
|
||||
forwardedHeaders.set("x-mnote-workspace-id", input.workspaceId);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: forwardedHeaders,
|
||||
cache: "no-store",
|
||||
});
|
||||
const payload = (await response
|
||||
.json()
|
||||
.catch(() => null)) as MnoteWebSidebarCompatResponse | null;
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
payload && typeof (payload as Record<string, unknown>).message === "string"
|
||||
? String((payload as Record<string, unknown>).message)
|
||||
: "mnote-web 侧边栏兼容接口请求失败";
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (!payload?.result) {
|
||||
throw new Error("mnote-web /api/compat/next/sidebar 未返回 result");
|
||||
}
|
||||
|
||||
return {
|
||||
dataset: payload.result,
|
||||
meta: {
|
||||
requestId: payload.requestId ?? null,
|
||||
traceId: payload.traceId ?? null,
|
||||
workspaceId: payload.workspaceId?.trim() || input.workspaceId,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
type SidebarDatasetListQueryResult,
|
||||
} from "@/lib/sidebar-data";
|
||||
import type { WorkspaceSummary } from "@/lib/workspaces";
|
||||
import {
|
||||
fetchSidebarDatasetFromMnoteWeb,
|
||||
getMnoteWebBaseUrl,
|
||||
} from "@/lib/server/mnote-web";
|
||||
|
||||
type LoadSidebarDataFromConvexInput = {
|
||||
client: ConvexHttpClient;
|
||||
@@ -57,9 +61,15 @@ export async function loadSidebarDataFromConvex(
|
||||
};
|
||||
}
|
||||
|
||||
const sidebarDataset = (await input.client.query(sidebarDatasetListQuery, {
|
||||
workspaceId: targetWorkspaceId,
|
||||
})) as SidebarDatasetListQueryResult;
|
||||
const sidebarDataset = getMnoteWebBaseUrl()
|
||||
? (
|
||||
await fetchSidebarDatasetFromMnoteWeb({
|
||||
workspaceId: targetWorkspaceId,
|
||||
})
|
||||
).dataset
|
||||
: ((await input.client.query(sidebarDatasetListQuery, {
|
||||
workspaceId: targetWorkspaceId,
|
||||
})) as SidebarDatasetListQueryResult);
|
||||
const normalizedDocuments = (sidebarDataset.documents ?? []) as DocumentRecord[];
|
||||
|
||||
return {
|
||||
|
||||
@@ -51,7 +51,7 @@ export type SidebarDatasetListQueryResult = {
|
||||
active_workspace_id: string;
|
||||
workspaces: WorkspaceSummary[];
|
||||
documents: DocumentRecord[];
|
||||
kernel_sidebar_projection?: KernelSidebarProjection | null;
|
||||
kernel_sidebar_projection: KernelSidebarProjection;
|
||||
trashed_documents: SidebarInitialData["trashedDocuments"];
|
||||
media_assets: MediaAsset[];
|
||||
trashed_media_assets: MediaAsset[];
|
||||
@@ -241,16 +241,14 @@ export function buildSidebarDatasetListQueryResult(
|
||||
export function mapSidebarDatasetListQueryResultToInitialData(
|
||||
result: SidebarDatasetListQueryResult,
|
||||
): SidebarInitialData {
|
||||
const kernelSidebarProjection =
|
||||
result.kernel_sidebar_projection ?? buildKernelSidebarProjection(result.documents);
|
||||
return {
|
||||
activeWorkspaceId: result.active_workspace_id,
|
||||
workspaces: [...result.workspaces],
|
||||
documents: [...result.documents],
|
||||
kernelSidebarProjection,
|
||||
kernelSidebarProjection: result.kernel_sidebar_projection,
|
||||
kernelSidebarTree: buildSidebarTreeFromKernelProjection({
|
||||
records: result.documents,
|
||||
projection: kernelSidebarProjection,
|
||||
projection: result.kernel_sidebar_projection,
|
||||
}),
|
||||
trashedDocuments: [...result.trashed_documents],
|
||||
trashedMediaAssets: [...result.trashed_media_assets],
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPageTreeProjectionItems,
|
||||
buildPickerTreeItems,
|
||||
filterVisiblePageTreeProjectionItems,
|
||||
} from "./tree-projection";
|
||||
|
||||
describe("buildPageTreeProjectionItems", () => {
|
||||
it("按当前树结构重建 parent/depth,而不是信任陈旧节点字段", () => {
|
||||
const rows = buildPageTreeProjectionItems([
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "root",
|
||||
workspace_id: "w",
|
||||
title: "Root",
|
||||
parent_id: null,
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
kernel: {
|
||||
nodeType: "page",
|
||||
depth: 0,
|
||||
position: 0,
|
||||
childCount: 1,
|
||||
expandedByDefault: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "child",
|
||||
workspace_id: "w",
|
||||
title: "Child",
|
||||
parent_id: null,
|
||||
sort_order: 9,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
kernel: {
|
||||
nodeType: "page",
|
||||
depth: 0,
|
||||
position: 9,
|
||||
childCount: 0,
|
||||
expandedByDefault: true,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(rows.map((item) => ({
|
||||
nodeId: item.nodeId,
|
||||
parentNodeId: item.parentNodeId,
|
||||
depth: item.depth,
|
||||
}))).toEqual([
|
||||
{ nodeId: "root", parentNodeId: null, depth: 0 },
|
||||
{ nodeId: "child", parentNodeId: "root", depth: 1 },
|
||||
]);
|
||||
expect(rows[1]?.node.parent_id).toBe("root");
|
||||
expect(rows[1]?.node.kernel?.depth).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterVisiblePageTreeProjectionItems", () => {
|
||||
it("只返回当前展开路径上的可见页面", () => {
|
||||
const items = buildPageTreeProjectionItems([
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "root",
|
||||
workspace_id: "w",
|
||||
title: "Root",
|
||||
parent_id: null,
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
children: [
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "child",
|
||||
workspace_id: "w",
|
||||
title: "Child",
|
||||
parent_id: "root",
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
children: [
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "leaf",
|
||||
workspace_id: "w",
|
||||
title: "Leaf",
|
||||
parent_id: "child",
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(filterVisiblePageTreeProjectionItems(items, new Set()).map((item) => item.nodeId)).toEqual(["root"]);
|
||||
expect(filterVisiblePageTreeProjectionItems(items, new Set(["root"])).map((item) => item.nodeId)).toEqual([
|
||||
"root",
|
||||
"child",
|
||||
]);
|
||||
expect(
|
||||
filterVisiblePageTreeProjectionItems(items, new Set(["root", "child"])).map(
|
||||
(item) => item.nodeId,
|
||||
),
|
||||
).toEqual(["root", "child", "leaf"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPickerTreeItems", () => {
|
||||
it("复用 page_tree projection 生成 picker 列表", () => {
|
||||
const items = buildPageTreeProjectionItems([
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "a",
|
||||
workspace_id: "w",
|
||||
title: "A",
|
||||
parent_id: null,
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
children: [
|
||||
{
|
||||
access_scope: "private",
|
||||
id: "b",
|
||||
workspace_id: "w",
|
||||
title: null,
|
||||
parent_id: "a",
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "",
|
||||
updated_at: null,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(buildPickerTreeItems(items, ["a"])).toEqual([
|
||||
{ id: "b", title: "无标题", depth: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
|
||||
export type TreeProjectionKind = "page_tree";
|
||||
|
||||
export type TreeProjectionNodeType = "page";
|
||||
|
||||
export type TreeProjectionCapability =
|
||||
| "expand"
|
||||
| "open"
|
||||
| "drag"
|
||||
| "drop"
|
||||
| "select"
|
||||
| "create-child";
|
||||
|
||||
export type TreeProjectionResourceMeta = {
|
||||
resourceKind: "document";
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export type PageTreeProjectionItem = {
|
||||
rowId: `page:${string}`;
|
||||
nodeId: string;
|
||||
parentNodeId: string | null;
|
||||
nodeType: TreeProjectionNodeType;
|
||||
projectionKind: TreeProjectionKind;
|
||||
depth: number;
|
||||
position: number | null;
|
||||
title: string;
|
||||
childCount: number;
|
||||
capabilities: TreeProjectionCapability[];
|
||||
resourceMeta: TreeProjectionResourceMeta;
|
||||
node: SidebarTreeNode;
|
||||
};
|
||||
|
||||
export type PickerTreeItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
function buildPageCapabilities(childCount: number): TreeProjectionCapability[] {
|
||||
const capabilities: TreeProjectionCapability[] = [
|
||||
"open",
|
||||
"drag",
|
||||
"drop",
|
||||
"select",
|
||||
"create-child",
|
||||
];
|
||||
if (childCount > 0) {
|
||||
capabilities.unshift("expand");
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
export function buildPageTreeProjectionItems(
|
||||
nodes: SidebarTreeNode[],
|
||||
): PageTreeProjectionItem[] {
|
||||
const items: PageTreeProjectionItem[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const walk = (
|
||||
entries: SidebarTreeNode[],
|
||||
depth: number,
|
||||
parentNodeId: string | null,
|
||||
) => {
|
||||
entries.forEach((node, index) => {
|
||||
if (visited.has(node.id)) {
|
||||
return;
|
||||
}
|
||||
visited.add(node.id);
|
||||
|
||||
const childCount = node.children.length;
|
||||
items.push({
|
||||
rowId: `page:${node.id}`,
|
||||
nodeId: node.id,
|
||||
parentNodeId,
|
||||
nodeType: "page",
|
||||
projectionKind: "page_tree",
|
||||
depth,
|
||||
position: node.kernel?.position ?? node.sort_order ?? index,
|
||||
title: node.title ?? "无标题",
|
||||
childCount,
|
||||
capabilities: buildPageCapabilities(childCount),
|
||||
resourceMeta: {
|
||||
resourceKind: "document",
|
||||
documentId: node.id,
|
||||
},
|
||||
node: {
|
||||
...node,
|
||||
parent_id: parentNodeId,
|
||||
kernel: node.kernel
|
||||
? {
|
||||
...node.kernel,
|
||||
depth,
|
||||
position: node.kernel.position ?? node.sort_order ?? index,
|
||||
childCount,
|
||||
}
|
||||
: {
|
||||
nodeType: "page",
|
||||
depth,
|
||||
position: node.sort_order ?? index,
|
||||
childCount,
|
||||
expandedByDefault: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (childCount > 0) {
|
||||
walk(node.children, depth + 1, node.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(nodes, 0, null);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function filterVisiblePageTreeProjectionItems(
|
||||
items: PageTreeProjectionItem[],
|
||||
expanded: Set<string>,
|
||||
): PageTreeProjectionItem[] {
|
||||
const visible: PageTreeProjectionItem[] = [];
|
||||
const visibleIds = new Set<string>();
|
||||
|
||||
items.forEach((item) => {
|
||||
if (!item.parentNodeId) {
|
||||
visible.push(item);
|
||||
visibleIds.add(item.nodeId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visibleIds.has(item.parentNodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expanded.has(item.parentNodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visible.push(item);
|
||||
visibleIds.add(item.nodeId);
|
||||
});
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
export function buildPickerTreeItems(
|
||||
items: PageTreeProjectionItem[],
|
||||
excludeIds: Iterable<string> = [],
|
||||
): PickerTreeItem[] {
|
||||
const excluded = new Set(excludeIds);
|
||||
return items
|
||||
.filter((item) => !excluded.has(item.nodeId))
|
||||
.map((item) => ({
|
||||
id: item.nodeId,
|
||||
title: item.title,
|
||||
depth: item.depth,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user