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
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}
|
|
/>
|
|
);
|
|
}
|
|
|