@ant-design/x
Version:
Craft AI-driven interfaces effortlessly
264 lines (243 loc) • 9.07 kB
JavaScript
import { useControlledState } from '@rc-component/util';
import { Flex, Splitter } from 'antd';
import { clsx } from 'clsx';
import React, { useCallback, useEffect, useState } from 'react';
import useProxyImperativeHandle from "../_util/hooks/use-proxy-imperative-handle";
import useXComponentConfig from "../_util/hooks/use-x-component-config";
import { useLocale } from "../locale";
import enUS from "../locale/en_US";
import { useXProviderContext } from "../x-provider";
import DirectoryTree from "./DirectoryTree";
import FilePreview from "./FilePreview";
import useStyle from "./style";
// File content service interface
// Folder properties
// Ref interface type
const ForwardFolder = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls: customizePrefixCls,
className,
classNames,
styles,
style,
treeData,
directoryIcons,
previewRender,
directoryTitle,
previewTitle,
selectable = true,
defaultSelectedFile,
defaultExpandAll = true,
selectedFile,
onSelectedFileChange,
directoryTreeWith = 278,
emptyRender,
defaultExpandedPaths,
expandedPaths,
onExpandedPathsChange,
onFileClick,
onFolderClick
} = props;
// ============================= Refs =============================
const containerRef = React.useRef(null);
useProxyImperativeHandle(ref, () => {
return {
nativeElement: containerRef.current
};
});
// ============================ State ============================
// Find node and validate path
const findNodeAndValidate = useCallback((path, validateAsFile = false) => {
if (!path) return {
node: undefined,
isValid: false
};
const segments = Array.isArray(path) ? path.filter(Boolean) : path.split('/').filter(Boolean);
if (segments.length === 0) return {
node: undefined,
isValid: false
};
const findNode = (nodes, index = 0) => {
if (index >= segments.length) return undefined;
const currentSegment = segments[index];
for (const node of nodes) {
if (node.path === currentSegment) {
return index === segments.length - 1 ? node : node.children ? findNode(node.children, index + 1) : undefined;
}
}
return undefined;
};
const node = findNode(treeData);
const isValid = validateAsFile ? !!node && (!node?.children || node.children.length === 0) : !!node;
return {
node,
isValid
};
}, [treeData]);
const [validSelectedFile, setValidSelectedFile] = useState(false);
const isValidSelectedFile = filePath => !!(filePath && filePath.length > 0 && findNodeAndValidate(filePath, true).isValid);
const [expandedPathsState, setExpandedPaths] = useControlledState(defaultExpandedPaths, expandedPaths);
const [selectedFileState, setSelectedFileState] = useControlledState(isValidSelectedFile(defaultSelectedFile || []) ? defaultSelectedFile || [] : [], selectedFile);
useEffect(() => {
const isValid = isValidSelectedFile(selectedFile || defaultSelectedFile || []);
setValidSelectedFile(isValid);
}, [selectedFile, treeData, defaultSelectedFile]);
const [fileContent, setFileContent] = useState('');
const [loadingContent, setLoadingContent] = useState(false);
// ============================ Prefix ============================
const {
getPrefixCls,
direction
} = useXProviderContext();
const prefixCls = getPrefixCls('folder', customizePrefixCls);
const [hashId, cssVarCls] = useStyle(prefixCls);
const contextConfig = useXComponentConfig('folder');
const [locale] = useLocale('Folder', enUS.Folder);
// ============================ Style ============================
const mergedCls = clsx(prefixCls, contextConfig.className, className, classNames?.root, hashId, cssVarCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-selectable`]: selectable
});
// ============================ Event Handlers ============================
const handleSelect = (_keys, info) => {
const keys = _keys;
const nodes = Array.isArray(info.selectedNodes) ? info.selectedNodes : [info.selectedNodes];
// Check if a folder was clicked
const isFolder = nodes.some(node => !node.isLeaf);
if (isFolder) {
// Click folder: don't update selectedFileState, only trigger folder click event
if (nodes.length === 1) {
const node = nodes[0];
onFolderClick?.(node.path);
}
return;
}
// Convert full path to array format
const pathArray = keys[0]?.split('/').filter(Boolean) || [];
// Avoid empty or invalid paths
if (pathArray.length === 0) return;
// Get selected file name and content (single file selection)
const selectedNode = nodes[0];
const fileName = selectedNode?.title;
const fileContent = selectedNode?.content;
// Trigger selection change callback (main interaction method)
onSelectedFileChange?.({
path: pathArray,
title: fileName,
content: fileContent
});
// // Update internal state in uncontrolled mode
const isControlled = selectedFile !== undefined;
if (!isControlled) {
setValidSelectedFile(true);
setSelectedFileState(pathArray);
}
// Handle single file click event
if (nodes.length === 1) {
const node = nodes[0];
onFileClick?.(node.path, node.content);
}
};
const handleExpand = keys => {
const newPaths = keys;
setExpandedPaths(newPaths);
onExpandedPathsChange?.(newPaths);
};
// ============================ Effects ============================
useEffect(() => {
const loadFileContent = async () => {
if (!validSelectedFile || selectedFileState.length === 0) {
setFileContent('');
setLoadingContent(false);
return;
}
const filePath = selectedFileState.join('/');
// First check if the node already has content
const segments = filePath.split('/').filter(segment => segment !== '');
const {
node
} = findNodeAndValidate(segments);
// If file content service is available, use it to load content
if (props.fileContentService) {
setLoadingContent(true);
try {
const content = await props.fileContentService.loadFileContent(filePath);
setFileContent(content);
} catch (error) {
setFileContent(`// ${locale?.loadError}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setLoadingContent(false);
}
} else if (node?.content) {
// If node already has content, use it directly
setFileContent(node.content);
setLoadingContent(false);
return;
} else {
// No file content service, show prompt message
setFileContent(`// ${locale.noService}`);
setLoadingContent(false);
}
};
loadFileContent();
}, [validSelectedFile, selectedFileState, treeData, props.fileContentService, findNodeAndValidate]);
// ============================ Style ============================
const mergedStyle = {
...contextConfig.style,
...styles?.root,
...style
};
return /*#__PURE__*/React.createElement("div", {
ref: containerRef,
className: mergedCls,
style: mergedStyle
}, /*#__PURE__*/React.createElement(Flex, {
className: `${prefixCls}-container`
}, /*#__PURE__*/React.createElement(Splitter, null, /*#__PURE__*/React.createElement(Splitter.Panel, {
defaultSize: directoryTreeWith
}, /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-directory-tree`, classNames?.directoryTree),
style: {
...contextConfig.styles?.directoryTree,
...styles?.directoryTree
}
}, /*#__PURE__*/React.createElement(DirectoryTree, {
directoryIcons: directoryIcons,
prefixCls: customizePrefixCls,
treeData: treeData,
selectedKeys: selectable && selectedFileState && validSelectedFile ? [selectedFileState.join('/')] : [],
classNames: classNames,
styles: styles,
expandedKeys: expandedPathsState,
onSelect: handleSelect,
onExpand: handleExpand,
defaultExpandAll: defaultExpandAll,
directoryTitle: directoryTitle
}))), /*#__PURE__*/React.createElement(Splitter.Panel, null, /*#__PURE__*/React.createElement(FilePreview, {
emptyRender: emptyRender,
prefixCls: customizePrefixCls,
classNames: classNames,
styles: styles,
selectedFile: validSelectedFile ? selectedFileState : [],
fileContent: fileContent,
loading: loadingContent,
previewTitle: previewTitle,
previewRender: previewRender,
getFileNode: path => {
if (!path || path.length === 0) return undefined;
const {
node
} = findNodeAndValidate(path);
return node ? {
title: node.title,
path: node.path,
content: node.content
} : undefined;
}
})))));
});
const Folder = ForwardFolder;
if (process.env.NODE_ENV !== 'production') {
Folder.displayName = 'Folder';
}
export default Folder;