151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
"use client";
|
|||
|
|
|
||
|
|
import type { DragEvent, MouseEvent } from "react";
|
||
|
|
import { FileTree } from "@/components/sidebar/file-tree";
|
||
|
|
import { PrivateTree } from "@/components/sidebar/private-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";
|
||
|
|
|
||
|
|
type SidebarPageTreeSurfaceProps = {
|
||
|
|
mode: "page";
|
||
|
|
rows: PageTreeProjectionItem[];
|
||
|
|
expanded: Set<string>;
|
||
|
|
activeId: string;
|
||
|
|
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;
|
||
|
|
};
|
||
|
|
|
||
|
|
type SidebarFileTreeSurfaceProps = {
|
||
|
|
mode: "filetree";
|
||
|
|
rows: FileTreeRow[];
|
||
|
|
activeId: string;
|
||
|
|
selectedRowIds: Set<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?: (docId: string, files: FileList, targetRow?: FileTreeRow) => void;
|
||
|
|
onInternalDrop?: (args: { targetRow: FileTreeRow; rowIds: string[]; copy: boolean }) => 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 = {
|
||
|
|
items: TreePickerSurfaceItem[];
|
||
|
|
highlighted: number;
|
||
|
|
className?: string;
|
||
|
|
emptyText?: string;
|
||
|
|
onHighlight: (index: number) => void;
|
||
|
|
onPick: (targetId: string | null) => void;
|
||
|
|
};
|
||
|
|
|
||
|
|
export function SidebarTreeSurface(props: SidebarTreeSurfaceProps) {
|
||
|
|
const surfaceTestId =
|
||
|
|
props.mode === "page"
|
||
|
|
? "sidebar-page-tree-shell"
|
||
|
|
: "sidebar-file-tree-shell";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
data-testid={surfaceTestId}
|
||
|
|
data-shell-mode={props.mode}
|
||
|
|
className={cn(
|
||
|
|
"h-full w-full min-w-0 overflow-x-hidden rounded-md border border-[#eff2f6] bg-white",
|
||
|
|
props.className,
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{props.mode === "page" ? (
|
||
|
|
<PrivateTree
|
||
|
|
rows={props.rows}
|
||
|
|
expanded={props.expanded}
|
||
|
|
activeId={props.activeId}
|
||
|
|
onToggleExpand={props.onToggleExpand}
|
||
|
|
onMove={props.onMove}
|
||
|
|
onCreateChild={props.onCreateChild}
|
||
|
|
onContextMenu={props.onContextMenu}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<FileTree
|
||
|
|
rows={props.rows}
|
||
|
|
activeId={props.activeId}
|
||
|
|
selectedRowIds={props.selectedRowIds}
|
||
|
|
onRowClick={props.onRowClick}
|
||
|
|
onRowDoubleClick={props.onRowDoubleClick}
|
||
|
|
onRowContextMenu={props.onRowContextMenu}
|
||
|
|
onRowDragStart={props.onRowDragStart}
|
||
|
|
onToggleExpand={props.onToggleExpand}
|
||
|
|
onToggleAssetFolderExpand={props.onToggleAssetFolderExpand}
|
||
|
|
onCreateChild={props.onCreateChild}
|
||
|
|
onBlankMouseDown={props.onBlankMouseDown}
|
||
|
|
onDropFiles={props.onDropFiles}
|
||
|
|
onInternalDrop={props.onInternalDrop}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TreePickerSurface({
|
||
|
|
items,
|
||
|
|
highlighted,
|
||
|
|
className,
|
||
|
|
emptyText = "没有匹配结果",
|
||
|
|
onHighlight,
|
||
|
|
onPick,
|
||
|
|
}: TreePickerSurfaceProps) {
|
||
|
|
if (items.length === 0) {
|
||
|
|
return <div className="p-4 text-sm text-gray-400">{emptyText}</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
data-testid="tree-picker-surface"
|
||
|
|
className={cn("py-2", className)}
|
||
|
|
>
|
||
|
|
{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>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|