- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录 - 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目 - 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑 - 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
247 lines
8.9 KiB
TypeScript
247 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import type { DragEvent, MouseEvent } from "react";
|
|
import {
|
|
TreeShellHost,
|
|
type FileTreeShellDeleteSelectionPayload,
|
|
type FileTreeShellExternalDropPayload,
|
|
type FileTreeShellInternalDropPayload,
|
|
type TreeShellMutationPayload,
|
|
type TreeShellPickerCommand,
|
|
type TreeRendererFamily,
|
|
} from "@/components/sidebar/tree-shell-host";
|
|
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
|
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
|
import type { FileTreeRow } from "@/lib/file-tree/types";
|
|
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export type { TreeRendererFamily } from "@/components/sidebar/tree-shell-host";
|
|
|
|
type SidebarPageTreeSurfaceProps = {
|
|
mode: "page";
|
|
rendererFamily?: TreeRendererFamily;
|
|
workspaceId: string | null;
|
|
treeShellEnabled?: boolean;
|
|
rows?: PageTreeProjectionItem[];
|
|
treeShellRows?: PageTreeProjectionItem[];
|
|
expanded: Set<string>;
|
|
activeId: string;
|
|
focusedDocumentId?: string | null;
|
|
className?: string;
|
|
onToggleExpand: (id: string) => void;
|
|
onMove: (nodeId: string, parentId: string | null, index: number) => void;
|
|
onCreateChild: (parentId: string | null) => void;
|
|
onContextMenu: (event: MouseEvent, node: SidebarTreeNode) => void;
|
|
onNavigate?: (documentId: string) => void;
|
|
onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void;
|
|
onPageExpandChange?: (payload: { documentId: string | null; expanded: boolean }) => void;
|
|
onPageFocusChange?: (payload: { documentId: string | null }) => void;
|
|
onTreeMutation?: (payload: TreeShellMutationPayload) => void;
|
|
};
|
|
|
|
type SidebarFileTreeSurfaceProps = {
|
|
mode: "filetree";
|
|
rendererFamily?: TreeRendererFamily;
|
|
workspaceId: string | null;
|
|
treeShellEnabled?: boolean;
|
|
rows?: FileTreeRow[];
|
|
treeShellItems?: KernelFileTreeProjectionItem[];
|
|
activeId: string;
|
|
className?: string;
|
|
onRowClick: (row: FileTreeRow, event: MouseEvent) => void;
|
|
onRowDoubleClick: (row: FileTreeRow, event: MouseEvent) => void;
|
|
onRowContextMenu: (row: FileTreeRow, event: MouseEvent) => void;
|
|
onRowDragStart?: (row: FileTreeRow, event: DragEvent) => void;
|
|
onToggleExpand: (docId: string) => void;
|
|
onToggleAssetFolderExpand?: (assetId: string) => void;
|
|
onCreateChild: (parentId: string | null) => void;
|
|
onBlankMouseDown?: (event: MouseEvent) => void;
|
|
onDropFiles?: (payload: FileTreeShellExternalDropPayload) => void;
|
|
onInternalDrop?: (payload: FileTreeShellInternalDropPayload) => void;
|
|
onNavigate?: (documentId: string) => void;
|
|
onFileTreeContextMenu?: (payload: {
|
|
documentId: string | null;
|
|
assetId: string | null;
|
|
rowId: string | null;
|
|
rowKind: string | null;
|
|
x: number;
|
|
y: number;
|
|
}) => void;
|
|
onFileTreeSelectionChange?: (payload: {
|
|
selectedRowIds: string[];
|
|
anchorRowId: string | null;
|
|
focusedRowId: string | null;
|
|
}) => void;
|
|
onFileTreeDeleteSelection?: (payload: FileTreeShellDeleteSelectionPayload) => void;
|
|
onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void;
|
|
onTreeMutation?: (payload: TreeShellMutationPayload) => void;
|
|
};
|
|
|
|
export type SidebarTreeSurfaceProps =
|
|
| SidebarPageTreeSurfaceProps
|
|
| SidebarFileTreeSurfaceProps;
|
|
|
|
export type TreePickerSurfaceItem =
|
|
| { kind: "root"; id: null; title: string; subtitle?: string }
|
|
| { kind: "doc"; id: string; title: string; subtitle?: string; depth?: number };
|
|
|
|
type TreePickerSurfaceProps = {
|
|
rendererFamily?: TreeRendererFamily;
|
|
workspaceId: string | null;
|
|
treeShellEnabled?: boolean;
|
|
activeDocumentId?: string | null;
|
|
activePickerItemKey?: string | null;
|
|
allowRootPick?: boolean;
|
|
excludeIds?: string[];
|
|
pickerCommand?: TreeShellPickerCommand | null;
|
|
treeShellItems?: TreePickerSurfaceItem[];
|
|
items: TreePickerSurfaceItem[];
|
|
highlighted: number;
|
|
className?: string;
|
|
emptyText?: string;
|
|
onHighlight: (index: number) => void;
|
|
onPick: (targetId: string | null) => void;
|
|
onPickerFocusChange?: (payload: { itemKey: string | null; documentId: string | null }) => void;
|
|
};
|
|
|
|
export function SidebarTreeSurface(props: SidebarTreeSurfaceProps) {
|
|
const surfaceTestId =
|
|
props.mode === "page"
|
|
? "sidebar-page-tree-shell"
|
|
: "sidebar-file-tree-shell";
|
|
const rendererFamily = props.rendererFamily ?? "react";
|
|
const pageTreeFallback = (
|
|
<div
|
|
data-testid="page-tree-renderer-removed"
|
|
className="flex h-full items-center justify-center px-4 text-center text-sm text-gray-400"
|
|
>
|
|
页面树旧 React renderer 已退出;请使用 Rust tree shell 宿主。
|
|
</div>
|
|
);
|
|
const fileTreeFallback = (
|
|
<div
|
|
data-testid="file-tree-renderer-removed"
|
|
className="flex h-full items-center justify-center px-4 text-center text-sm text-gray-400"
|
|
>
|
|
文件树旧 React renderer 已退出;请使用 Rust tree shell 宿主。
|
|
</div>
|
|
);
|
|
const fallbackContent =
|
|
props.mode === "page" ? (
|
|
pageTreeFallback
|
|
) : (
|
|
fileTreeFallback
|
|
);
|
|
|
|
return (
|
|
<TreeShellHost
|
|
mode={props.mode}
|
|
surfaceTestId={surfaceTestId}
|
|
rendererFamily={rendererFamily}
|
|
treeShellEnabled={props.treeShellEnabled}
|
|
fallbackImplementation={
|
|
props.mode === "page"
|
|
? "page_tree_renderer_removed"
|
|
: "filetree_renderer_removed"
|
|
}
|
|
workspaceId={props.workspaceId}
|
|
activeDocumentId={props.activeId}
|
|
focusedDocumentId={props.mode === "page" ? (props.focusedDocumentId ?? null) : undefined}
|
|
pageTreeItems={props.mode === "page" ? props.treeShellRows : undefined}
|
|
inlineFileTreeItems={props.mode === "filetree" ? props.treeShellItems : undefined}
|
|
onNavigate={props.onNavigate}
|
|
onPageContextMenu={props.mode === "page" ? props.onPageContextMenu : undefined}
|
|
onPageExpandChange={props.mode === "page" ? props.onPageExpandChange : undefined}
|
|
onPageFocusChange={props.mode === "page" ? props.onPageFocusChange : undefined}
|
|
onFileTreeContextMenu={props.mode === "filetree" ? props.onFileTreeContextMenu : undefined}
|
|
onFileTreeSelectionChange={props.mode === "filetree" ? props.onFileTreeSelectionChange : undefined}
|
|
onFileTreeDeleteSelection={props.mode === "filetree" ? props.onFileTreeDeleteSelection : undefined}
|
|
onInternalDrop={props.mode === "filetree" ? props.onInternalDrop : undefined}
|
|
onDropFiles={props.mode === "filetree" ? props.onDropFiles : undefined}
|
|
onAssetOpen={props.mode === "filetree" ? props.onAssetOpen : undefined}
|
|
onTreeMutation={props.onTreeMutation}
|
|
className={cn(
|
|
"h-full w-full min-w-0 overflow-x-hidden rounded-md border border-[#eff2f6] bg-white",
|
|
props.className,
|
|
)}
|
|
>
|
|
{fallbackContent}
|
|
</TreeShellHost>
|
|
);
|
|
}
|
|
|
|
export function TreePickerSurface({
|
|
rendererFamily = "react",
|
|
workspaceId,
|
|
treeShellEnabled = true,
|
|
activeDocumentId = null,
|
|
activePickerItemKey = null,
|
|
allowRootPick = false,
|
|
excludeIds = [],
|
|
pickerCommand = null,
|
|
treeShellItems,
|
|
items,
|
|
highlighted,
|
|
className,
|
|
emptyText = "没有匹配结果",
|
|
onHighlight,
|
|
onPick,
|
|
onPickerFocusChange,
|
|
}: TreePickerSurfaceProps) {
|
|
const hasItems = items.length > 0;
|
|
const effectiveTreeShellItems =
|
|
rendererFamily === "rust_family" ? (treeShellItems ?? items) : treeShellItems;
|
|
|
|
return (
|
|
<TreeShellHost
|
|
mode="picker"
|
|
surfaceTestId="tree-picker-surface"
|
|
rendererFamily={rendererFamily}
|
|
treeShellEnabled={treeShellEnabled}
|
|
workspaceId={workspaceId}
|
|
activeDocumentId={activeDocumentId}
|
|
activePickerItemKey={activePickerItemKey}
|
|
allowRootPick={allowRootPick}
|
|
excludeIds={excludeIds}
|
|
pickerCommand={pickerCommand}
|
|
pickerItems={effectiveTreeShellItems}
|
|
onPick={onPick}
|
|
onPickerFocusChange={onPickerFocusChange}
|
|
className={cn(hasItems ? "py-2" : null, className)}
|
|
>
|
|
{!hasItems ? (
|
|
<div className="p-4 text-sm text-gray-400">{emptyText}</div>
|
|
) : (
|
|
items.map((item, index) => {
|
|
const isRoot = item.kind === "root";
|
|
return (
|
|
<button
|
|
key={isRoot ? "root" : item.id}
|
|
data-testid={isRoot ? "tree-picker-root" : "tree-picker-row"}
|
|
data-node-id={isRoot ? "" : item.id}
|
|
type="button"
|
|
className={cn(
|
|
"w-full px-4 py-3 text-left transition-colors hover:bg-[#eef2ff]",
|
|
index === highlighted && "bg-[#e3ecff]",
|
|
)}
|
|
onMouseEnter={() => onHighlight(index)}
|
|
onClick={() => onPick(item.id)}
|
|
>
|
|
<div
|
|
className="text-sm font-medium text-gray-900"
|
|
style={!isRoot ? { paddingLeft: 12 * (item.depth ?? 0) } : undefined}
|
|
>
|
|
{item.title}
|
|
</div>
|
|
{item.subtitle ? (
|
|
<div className="mt-1 text-xs text-gray-500">{item.subtitle}</div>
|
|
) : null}
|
|
</button>
|
|
);
|
|
})
|
|
)}
|
|
</TreeShellHost>
|
|
);
|
|
}
|