import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { Tree, Empty, Spin, Typography, Select, Space, Breadcrumb, Input, theme } from 'antd'; import { FileOutlined, FileTextOutlined, CodeOutlined, FileImageOutlined, FileMarkdownOutlined, Html5Outlined, } from '@ant-design/icons'; import { useApp } from '@nocobase/client-v2'; import { useGitManager } from '../context/GitManagerContext'; import { RunReviewButton } from './RunReviewButton'; import { useT } from '../locale'; const { Text } = Typography; const { Search } = Input; const { useToken } = theme; const FILE_ICONS: Record = { ts: , tsx: , js: , jsx: , json: , md: , html: , css: , py: , png: , jpg: , svg: , }; function getFileIcon(name: string, type: string) { if (type === 'tree') return undefined; // use default folder icons const ext = name.split('.').pop()?.toLowerCase() || ''; return FILE_ICONS[ext] || ; } function getLanguage(filename: string): string { const ext = filename.split('.').pop()?.toLowerCase() || ''; const map: Record = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', json: 'json', md: 'markdown', html: 'html', css: 'css', scss: 'scss', py: 'python', sh: 'bash', yml: 'yaml', yaml: 'yaml', sql: 'sql', xml: 'xml', java: 'java', go: 'go', rs: 'rust', rb: 'ruby', }; return map[ext] || 'text'; } interface TreeNode { key: string; title: React.ReactNode; icon: React.ReactNode; isLeaf: boolean; children?: TreeNode[]; filePath: string; fileType: string; } export const FileExplorer: React.FC = () => { const t = useT(); const { token } = useToken(); const api = useApp().apiClient; const { selectedRepo, branches: branchList, currentBranch, refreshBranches } = useGitManager(); const [treeData, setTreeData] = useState([]); const [loading, setLoading] = useState(false); const [fileContent, setFileContent] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [selectedFolder, setSelectedFolder] = useState(null); const [currentRef, setCurrentRef] = useState('HEAD'); const [searchText, setSearchText] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [contentLoading, setContentLoading] = useState(false); const loadTree = useCallback( async (treePath = '', ref = currentRef) => { if (!selectedRepo) return []; try { const { data } = await api.request({ url: 'gitManager:fileTree', params: { repositoryId: selectedRepo.id, ref, treePath }, }); const responseData = data?.data || data || []; const list = Array.isArray(responseData) ? responseData : Array.isArray(responseData?.data) ? responseData.data : []; return list.map((item: any) => ({ key: item.path, title: ( {item.name} {item.type === 'blob' && item.size > 0 && ( {item.size > 1024 ? `${(item.size / 1024).toFixed(1)}KB` : `${item.size}B`} )} ), icon: getFileIcon(item.name, item.type), isLeaf: item.type === 'blob', filePath: item.path, fileType: item.type, })); } catch (error) { console.warn('Failed to load file tree:', error); return []; } }, [api, selectedRepo, currentRef], ); const loadRoot = useCallback(async () => { setLoading(true); try { const nodes = await loadTree('', currentRef); setTreeData(nodes); } finally { setLoading(false); } }, [loadTree, currentRef]); useEffect(() => { if (selectedRepo?.status === 'connected') { refreshBranches(); setFileContent(null); setSelectedFile(null); setSelectedFolder(null); } else { setTreeData([]); } }, [selectedRepo]); // Sync currentRef when branch data arrives useEffect(() => { if (currentBranch && currentRef === 'HEAD') { setCurrentRef(currentBranch); } }, [currentBranch]); useEffect(() => { if (selectedRepo?.status === 'connected') { loadRoot(); } }, [currentRef, selectedRepo]); const onLoadData = async (node: any) => { if (node.children) return; const children = await loadTree(node.filePath, currentRef); setTreeData((prev) => updateTreeData(prev, node.key, children)); }; const onSelect = async (_: any, info: any) => { const node = info.node; if (!node.isLeaf) { setSelectedFolder((prev) => (prev === node.filePath ? null : node.filePath)); return; } setSelectedFolder(null); setContentLoading(true); setSelectedFile(node.filePath); try { const { data } = await api.request({ url: 'gitManager:fileContent', params: { repositoryId: selectedRepo.id, ref: currentRef, filePath: node.filePath }, }); const responseData = data?.data?.data || data?.data; setFileContent(responseData?.content || ''); } catch { setFileContent('// Failed to load file content'); } finally { setContentLoading(false); } }; // Debounce search text by 300ms const debounceRef = useRef>(); useEffect(() => { debounceRef.current = setTimeout(() => setDebouncedSearch(searchText), 300); return () => clearTimeout(debounceRef.current); }, [searchText]); const folderTarget = useMemo( () => ({ type: 'folder' as const, repositoryId: selectedRepo?.id ?? 0, folderPath: selectedFolder ?? '', ref: currentRef, }), [selectedRepo?.id, selectedFolder, currentRef], ); if (!selectedRepo) { return ; } if (selectedRepo.status !== 'connected') { return ; } const filteredTree = debouncedSearch ? filterTree(treeData, debouncedSearch.toLowerCase()) : treeData; return (
{/* Left sidebar - file tree */}