You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

166 lines
5.4 KiB

"use client";
import { useCallback, useMemo, type Key } from "react";
import { Spin, Tree } from "antd";
import type { TreeProps } from "antd";
import { FolderOutlined } from "@ant-design/icons";
import { useI18n } from "@/i18n";
import { getUserFileFolderKey, type UserFileItem } from "@/utils/userFileLibrary";
interface AssetLibraryFolderTreeProps {
folders: UserFileItem[];
childrenByFolderId: Record<number, UserFileItem[]>;
loadingFolderIds: Set<number>;
loadedFolderIds: Set<number>;
expandedKeys: Key[];
selectedFolderId: number | null;
disabledFolderIds?: Set<number>;
onExpandedKeysChange: (keys: Key[]) => void;
onSelectFolder: (folderId: number) => void;
onLoadFolderChildren: (folderId: number) => void | Promise<void>;
}
type FolderTreeNode = {
key: string;
title: string;
item?: UserFileItem;
isLeaf: boolean;
depth: number;
isEmpty?: boolean;
children?: FolderTreeNode[];
};
const folderTreeClassName = [
"!bg-transparent !text-[var(--text-secondary)]",
"[&_.ant-tree-list-holder-inner]:gap-y-1",
"[&_.ant-tree-treenode]:!w-full [&_.ant-tree-treenode]:!items-center [&_.ant-tree-treenode]:!p-0",
"[&_.ant-tree-indent-unit]:!w-5",
"[&_.ant-tree-switcher]:!hidden",
"[&_.ant-tree-node-content-wrapper]:!m-0 [&_.ant-tree-node-content-wrapper]:!min-w-0 [&_.ant-tree-node-content-wrapper]:!flex-1 [&_.ant-tree-node-content-wrapper]:!bg-transparent [&_.ant-tree-node-content-wrapper]:!p-0 [&_.ant-tree-node-content-wrapper]:!text-inherit",
"[&_.ant-tree-node-content-wrapper:hover]:!bg-transparent [&_.ant-tree-node-content-wrapper.ant-tree-node-selected]:!bg-transparent",
"[&_.ant-tree-title]:!block [&_.ant-tree-title]:!w-full",
].join(" ");
export function AssetLibraryFolderTree({
folders,
childrenByFolderId,
loadingFolderIds,
loadedFolderIds,
expandedKeys,
selectedFolderId,
disabledFolderIds,
onExpandedKeysChange,
onSelectFolder,
onLoadFolderChildren,
}: AssetLibraryFolderTreeProps) {
const { t } = useI18n();
const treeData = useMemo<FolderTreeNode[]>(() => {
const buildNodes = (items: UserFileItem[], depth: number): FolderTreeNode[] => (
items
.filter((item) => item.nodeType === "folder")
.map((folder) => {
const folderKey = getUserFileFolderKey(folder.id);
const rawChildren = childrenByFolderId[folder.id] || [];
const childFolders = buildNodes(rawChildren, depth + 1);
const isLoadedEmpty = loadedFolderIds.has(folder.id)
&& !loadingFolderIds.has(folder.id)
&& childFolders.length === 0;
return {
key: folderKey,
title: folder.name,
item: folder,
isLeaf: false,
depth,
children: isLoadedEmpty
? [{
key: `${folderKey}-empty`,
title: "",
isLeaf: true,
depth: depth + 1,
isEmpty: true,
}]
: childFolders,
};
})
);
return buildNodes(folders, 0);
}, [childrenByFolderId, folders, loadedFolderIds, loadingFolderIds]);
const handleExpand = useCallback<NonNullable<TreeProps<FolderTreeNode>["onExpand"]>>((keys, info) => {
onExpandedKeysChange(keys);
if (info.expanded && info.node.item) {
void onLoadFolderChildren(info.node.item.id);
}
}, [onExpandedKeysChange, onLoadFolderChildren]);
const titleRender = useCallback((node: FolderTreeNode) => {
if (node.isEmpty) {
return (
<div className="flex h-8 items-center px-3 text-xs text-[var(--text-disabled)]">
{t("assetLibrary.folderEmpty")}
</div>
);
}
const item = node.item;
if (!item) return null;
const isSelected = selectedFolderId === item.id;
const isLoading = loadingFolderIds.has(item.id);
const isExpanded = expandedKeys.includes(node.key);
const isDisabled = disabledFolderIds?.has(item.id) ?? false;
return (
<button
type="button"
disabled={isDisabled}
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
isSelected
? "bg-[var(--surface-active)] text-[var(--text-primary)]"
: "text-[var(--text-secondary)] hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]"
}`}
onClick={() => {
onSelectFolder(item.id);
onExpandedKeysChange(
isExpanded
? expandedKeys.filter((key) => key !== node.key)
: [...expandedKeys, node.key]
);
if (!isExpanded) {
void onLoadFolderChildren(item.id);
}
}}
>
<FolderOutlined className="shrink-0 text-[15px] text-[var(--text-muted)]" />
<span className="min-w-0 flex-1 truncate">{item.name}</span>
{isLoading && <Spin size="small" />}
</button>
);
}, [
expandedKeys,
disabledFolderIds,
loadingFolderIds,
onExpandedKeysChange,
onLoadFolderChildren,
onSelectFolder,
selectedFolderId,
t,
]);
return (
<Tree<FolderTreeNode>
blockNode
showIcon={false}
showLine={false}
className={folderTreeClassName}
treeData={treeData}
expandedKeys={expandedKeys}
selectedKeys={selectedFolderId === null ? [] : [getUserFileFolderKey(selectedFolderId)]}
onExpand={handleExpand}
titleRender={titleRender}
/>
);
}