4-10 树域 Rust 家族化

This commit is contained in:
lix-2026
2026-04-24 06:10:18 +08:00
parent 41e958769e
commit 94631f3636
49 changed files with 5751 additions and 557 deletions
@@ -117,6 +117,172 @@ describe("buildVisibleRows", () => {
expect(rows.filter((r) => r.rowId === "doc:a").length).toBe(1);
expect(new Set(rows.map((r) => r.rowId)).size).toBe(rows.length);
});
it("优先消费 Rust file_tree projection 并保留 index 与资源文件夹语义", () => {
const rows = buildVisibleRows({
fileTreeItems: [
{
rowId: "doc:page_root",
rowKind: "document",
nodeId: "page_root",
parentNodeId: null,
nodeType: "page",
projectionKind: "file_tree",
title: "根页面",
depth: 0,
position: 0,
childCount: 4,
expandable: true,
expandedByDefault: true,
capabilities: ["expand", "open", "select"],
resourceMeta: {
resourceKind: "document",
documentId: "page_root",
workspaceId: "ws_1",
iconHint: "page",
},
iconHint: "page",
},
{
rowId: "index:page_root",
rowKind: "index",
nodeId: "index:page_root",
parentNodeId: "page_root",
nodeType: "index",
projectionKind: "file_tree",
title: "index.md",
depth: 1,
position: 0,
childCount: 0,
expandable: false,
expandedByDefault: false,
capabilities: ["open", "select"],
resourceMeta: {
resourceKind: "index",
documentId: "page_root",
workspaceId: "ws_1",
iconHint: "index",
},
iconHint: "index",
},
{
rowId: "asset-folder:mind_1",
rowKind: "asset_folder",
nodeId: "asset-folder:mind_1",
parentNodeId: "page_root",
nodeType: "mindmap",
projectionKind: "file_tree",
title: "头脑风暴",
depth: 1,
position: 1,
childCount: 1,
expandable: true,
expandedByDefault: false,
capabilities: ["expand", "open-asset", "select"],
resourceMeta: {
resourceKind: "mindmap",
documentId: "page_root",
assetId: "mind_1",
workspaceId: "ws_1",
assetKind: "mindmap",
iconHint: "mindmap",
},
iconHint: "mindmap",
},
{
rowId: "asset:asset_child_1",
rowKind: "asset",
nodeId: "asset:asset_child_1",
parentNodeId: "asset-folder:mind_1",
nodeType: "asset",
projectionKind: "file_tree",
title: "节点图片.png",
depth: 2,
position: 0,
childCount: 0,
expandable: false,
expandedByDefault: false,
capabilities: ["open-asset", "select"],
resourceMeta: {
resourceKind: "asset",
documentId: "page_root",
assetId: "asset_child_1",
workspaceId: "ws_1",
assetKind: "image",
iconHint: "image",
},
iconHint: "image",
},
{
rowId: "asset:table_1",
rowKind: "asset",
nodeId: "asset:table_1",
parentNodeId: "page_root",
nodeType: "table",
projectionKind: "file_tree",
title: "预算.luckysheet",
depth: 1,
position: 2,
childCount: 0,
expandable: false,
expandedByDefault: false,
capabilities: ["open-asset", "select"],
resourceMeta: {
resourceKind: "table",
documentId: "page_root",
assetId: "table_1",
workspaceId: "ws_1",
assetKind: "table",
iconHint: "table",
},
iconHint: "table",
},
],
expanded: new Set(["page_root"]),
expandedAssetFolderIds: new Set(["mind_1"]),
});
expect(rows.map((row) => `${row.kind}:${row.depth}:${row.rowId}`)).toEqual([
"doc:0:doc:page_root",
"index:1:index:page_root",
"asset-folder:1:asset-folder:mind_1",
"asset:2:asset:asset_child_1",
"asset:1:asset:table_1",
]);
expect(rows[0]).toMatchObject({
kind: "doc",
docId: "page_root",
hasChildren: true,
isExpanded: true,
});
expect(rows[2]).toMatchObject({
kind: "asset-folder",
docId: "page_root",
asset: {
id: "mind_1",
asset_type: "mindmap",
file_name: "头脑风暴",
},
hasChildren: true,
isExpanded: true,
});
expect(rows[3]).toMatchObject({
kind: "asset",
asset: {
id: "asset_child_1",
asset_type: "image",
file_name: "节点图片.png",
},
});
expect(rows[4]).toMatchObject({
kind: "asset",
asset: {
id: "table_1",
asset_type: "luckysheet",
file_name: "预算.luckysheet",
},
});
});
});
describe("parseFileTreeRowId", () => {
+105 -4
View File
@@ -1,28 +1,129 @@
"use client";
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
import {
getDocIdFromFileTreeItem,
resolveFileTreeRowAsset,
resolveFileTreeRowNode,
} from "@/lib/kernel-file-tree";
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";
function buildRowsFromKernelFileTreeProjection(input: {
fileTreeItems: KernelFileTreeProjectionItem[];
expanded: Set<string>;
expandedAssetFolderIds?: Set<string>;
nodeById?: Map<string, SidebarTreeNode>;
assetById?: Map<string, MediaAsset>;
}): FileTreeRow[] {
const rows: FileTreeRow[] = [];
const seen = new Set<string>();
input.fileTreeItems.forEach((item) => {
if (!item?.rowId || seen.has(item.rowId)) {
return;
}
seen.add(item.rowId);
const docId = getDocIdFromFileTreeItem(item);
switch (item.rowKind) {
case "document": {
const node = resolveFileTreeRowNode(item, input.nodeById);
rows.push({
kind: "doc",
rowId: makeDocRowId(docId),
depth: item.depth,
docId,
parentDocId: item.parentNodeId,
node,
hasChildren: item.childCount > 0,
isExpanded: input.expanded.has(docId),
});
return;
}
case "index": {
const node = resolveFileTreeRowNode(item, input.nodeById);
rows.push({
kind: "index",
rowId: makeIndexRowId(docId),
depth: item.depth,
docId,
parentDocId: docId,
node,
});
return;
}
case "asset_folder": {
const asset = resolveFileTreeRowAsset(item, input.assetById);
rows.push({
kind: "asset-folder",
rowId: makeAssetFolderRowId(asset.id),
depth: item.depth,
docId,
parentDocId: docId,
asset,
hasChildren: item.childCount > 0,
isExpanded: input.expandedAssetFolderIds?.has(asset.id) ?? false,
});
return;
}
case "asset": {
const asset = resolveFileTreeRowAsset(item, input.assetById);
rows.push({
kind: "asset",
rowId: makeAssetRowId(asset.id),
depth: item.depth,
docId,
parentDocId: docId,
asset,
});
return;
}
}
});
return rows;
}
export function buildVisibleRows({
fileTreeItems,
pageRows,
expanded,
assetsByDoc,
assetChildrenByAssetId,
expandedAssetFolderIds,
nodeById,
assetById,
}: {
fileTreeItems?: KernelFileTreeProjectionItem[];
// 只消费统一 page_tree projection;结构真相不再由文件树自行定义。
pageRows: PageTreeProjectionItem[];
pageRows?: PageTreeProjectionItem[];
expanded: Set<string>;
assetsByDoc: Record<string, MediaAsset[]>;
assetsByDoc?: Record<string, MediaAsset[]>;
assetChildrenByAssetId?: Record<string, MediaAsset[]>;
expandedAssetFolderIds?: Set<string>;
nodeById?: Map<string, SidebarTreeNode>;
assetById?: Map<string, MediaAsset>;
}): FileTreeRow[] {
if (fileTreeItems && fileTreeItems.length > 0) {
return buildRowsFromKernelFileTreeProjection({
fileTreeItems,
expanded,
expandedAssetFolderIds,
nodeById,
assetById,
});
}
const safePageRows = pageRows ?? [];
const safeAssetsByDoc = assetsByDoc ?? {};
const rows: FileTreeRow[] = [];
pageRows.forEach((item) => {
const assets = assetsByDoc[item.nodeId] ?? [];
safePageRows.forEach((item) => {
const assets = safeAssetsByDoc[item.nodeId] ?? [];
const hasChildren = item.childCount > 0 || assets.length > 0;
const isExpanded = expanded.has(item.nodeId);
rows.push({