diff --git a/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx b/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx index 15236ac15..d4b5a0575 100644 --- a/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx +++ b/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx @@ -5,7 +5,7 @@ import { BreadcrumbLabel } from '@/components/Titlebar/BreadcrumbTitlebar' import { Link } from '@gitmono/ui' import { UrlObject } from 'url'; -const Bread = ({ path }:any) => { +const Breadcrumb = ({ path }:any) => { const router = useRouter(); const scope = router.query.org as string @@ -46,4 +46,4 @@ const Bread = ({ path }:any) => { ); }; -export default Bread; +export default Breadcrumb; diff --git a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx index 664c3d824..8cd28cf38 100644 --- a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx +++ b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx @@ -1,49 +1,52 @@ -import * as React from 'react' -import { useCallback, useEffect, useState } from 'react' -import ArticleIcon from '@mui/icons-material/Article' -import FolderRounded from '@mui/icons-material/FolderRounded' -import { Box, IconButton } from '@mui/material' +import * as React from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import ArticleIcon from '@mui/icons-material/Article'; +import FolderRounded from '@mui/icons-material/FolderRounded'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import { Box, CircularProgress } from '@mui/material'; import { TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemProvider, useTreeItem, useTreeItemModel, - UseTreeItemParameters -} from '@mui/x-tree-view' -import { RichTreeView } from '@mui/x-tree-view/RichTreeView' + UseTreeItemParameters, +} from '@mui/x-tree-view'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { - TreeItemCheckbox, TreeItemContent, TreeItemGroupTransition, TreeItemIconContainer, TreeItemLabel, - TreeItemRoot -} from '@mui/x-tree-view/TreeItem' -import { usePathname } from 'next/navigation' -import { useRouter } from 'next/router' + TreeItemRoot, +} from '@mui/x-tree-view/TreeItem'; +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import { styled, alpha } from '@mui/material/styles'; +import { useGetTree } from '@/hooks/useGetTree'; +import { v4 as uuidv4 } from 'uuid'; interface MuiTreeNode { - id: string - label: string - path: string - content_type: string - // isLeaf: boolean; - children?: MuiTreeNode[] + id?: string; + label?: string; + path?: string; + content_type?: FileType; + isLeaf?: boolean; + children?: MuiTreeNode[]; } -type FileType = 'file' | 'directory' +type FileType = 'file' | 'directory'; interface ExtendedTreeItemProps { - content_type?: FileType - id: string - label: string + content_type?: FileType; + id: string; + label: string; } interface CustomLabelProps { - children: React.ReactNode - icon?: React.ElementType - expandable?: boolean + children: React.ReactNode; + icon?: React.ElementType; + expandable?: boolean; } function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { @@ -52,228 +55,336 @@ function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { {...other} sx={{ display: 'flex', - alignItems: 'center' + alignItems: 'center', }} > - {Icon && } - + {Icon && ( + + )} {children} - ) + ); } -const getIconFromFileType = (fileType: FileType) => { +const getIconFromFileType = (fileType: FileType, isExpanded:Boolean) => { switch (fileType) { case 'file': - return ArticleIcon + return ArticleIcon; case 'directory': - return FolderRounded + return isExpanded ? FolderOpenIcon :FolderRounded; default: - return ArticleIcon + return ArticleIcon; } -} +}; interface CustomTreeItemProps extends Omit, - Omit, 'onFocus'> {} + Omit, 'onFocus'> { + } const CustomTreeItem = React.forwardRef(function CustomTreeItem( - props: CustomTreeItemProps, - ref: React.Ref + { ...props }: CustomTreeItemProps, + ref: React.Ref, ) { - const { id, itemId, label, disabled, children, ...other } = props + const { id, itemId, label, disabled, children, ...other } = props; const { getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, - getCheckboxProps, getLabelProps, getGroupTransitionProps, getDragAndDropOverlayProps, - status - } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }) - - const item = useTreeItemModel(itemId)! + status, + } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - let icon + const item = useTreeItemModel(itemId)!; + let icon; if (status.expandable) { - icon = FolderRounded + icon = getIconFromFileType(item.content_type || 'file', status.expanded); } else if (item.content_type) { - icon = getIconFromFileType(item.content_type) + icon = getIconFromFileType(item.content_type, false); } - const handleClick = async (event: React.MouseEvent) => { - // eslint-disable-next-line no-console - console.log(event, 'event===event====') - // interactions.handleExpansion(event); - } + const StyledGroupTransition = styled(TreeItemGroupTransition)(({ theme }) => ({ + marginLeft: 15, + borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, + })); return ( - - - - - - {/* icon */} - - - - - - - {/* label */} - - - - {children && } + {item.label === '加载中...' ? ( + + + + + + ) : ( + + + + + + {/* label */} + + + + )} + {children && } - ) -}) - -const RepoTree = ({ directory }: any) => { - const router = useRouter() - const pathname = usePathname() - const [treeData, setTreeData] = useState([]) - const [expandedNodes, setExpandedNodes] = useState([]) - const [selectedNode, setSelectedNode] = useState(null) - - const convertToTreeData = useCallback((directory: any) => { - return sortProjectsByType(directory)?.map((item) => ({ - id: item.date + item.name, - label: item.name, - path: item.path, - content_type: item.content_type, - children: item.content_type === 'directory' ? [] : undefined - })) - }, []) + ); +}); + +const RepoTree = ({ directory }: { directory: any[] }) => { + const router = useRouter(); + const scope = router.query.org as string; + const pathname = usePathname(); + const basePath = pathname?.replace(`/${scope}/code/tree`, ''); + + const [treeData, setTreeData] = useState([]); + + const [expandedNodes, setExpandedNodes] = useState([]); + const [selectedNode, setSelectedNode] = useState(null); + + const [loadPath, setLoadPath] = useState(null); + const [targetNodeId, setTargetNodeId] = useState(null); + // const [loadingError, setLoadingError] = useState(null); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const { data: response, isLoading, error } = useGetTree({ path: loadPath??undefined}); + + const convertToTreeData = useCallback((parentBasePath: string, directory: any[]) => { + if (!Array.isArray(directory) || directory.length === 0) { + // console.warn('convertToTreeData: 接收到非数组或空数据', directory); + return []; + } + + return sortProjectsByType(directory).map((item) => { + + const currentPath = `${parentBasePath}/${item.name}`.replace('//', '/') || '/'; + // console.log('生成节点路径:', item.name, '=>', currentPath); + + return { + id: uuidv4(), + label: item.name, + path: currentPath, + isLeaf: item.content_type !== 'directory', + content_type: item.content_type, + children: item.content_type === 'directory' ? [ + { + id: uuidv4(), + label: '加载中...', + isLeaf: true, + }, + ] : undefined, + }; + }); + }, []); useEffect(() => { - setTreeData(convertToTreeData(directory)) - }, [directory, convertToTreeData]) + if (!Array.isArray(directory) || directory.length === 0) { + // console.warn('RepoTree: 接收到非数组或空的初始数据', directory); + setIsInitialLoading(false); + return; + } + + const rootPath = basePath || '/'; // 使用基路径作为根路径 + + setTreeData(convertToTreeData(rootPath, directory)); + setIsInitialLoading(false); + }, [directory, convertToTreeData,basePath]); const sortProjectsByType = (projects: any[]) => { - return projects?.sort((a, b) => { + if (!Array.isArray(projects) || projects.length === 0) { + // console.warn('sortProjectsByType: 接收到非数组或空数据', projects); + return []; + } + + return projects.sort((a, b) => { if (a.content_type === 'directory' && b.content_type === 'file') { - return -1 + return -1; } else if (a.content_type === 'file' && b.content_type === 'directory') { - return 1 + return 1; } else { - return 0 + return 0; } - }) - } - - const appendTreeData = (treeData: any, subItems: any, nodeId: string) => { - return treeData.map((item: any) => { - if (item.id === nodeId) { - return { - ...item, - children: subItems + }); + }; + + const updateTreeData = useCallback( + (currentTree: MuiTreeNode[], nodeId: string, newChildren: MuiTreeNode[]): any => { + if (!Array.isArray(currentTree) || currentTree.length === 0) { + // console.warn('updateTreeData: 接收到非数组或空的当前树数据', currentTree); + return []; + } + + return currentTree.map((node) => { + if (node.id === nodeId) { + return { + ...node, + children: newChildren, + isLeaf: newChildren.length === 0, + }; } - } else if (item.children) { - return { - ...item, - children: appendTreeData(item.children, subItems, nodeId) + if (node.children && node.children.length > 0) { + return { + ...node, + children: updateTreeData(node.children, nodeId, newChildren), + }; } + return node; + }); + }, + [], + ); + + const findNode = useCallback( + (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => { + if (!Array.isArray(data) || data.length === 0) { + return null; } - return item - }) - } - - const handleNodeToggle = (_event: React.SyntheticEvent | null, nodeIds: string[]) => { - const newlyExpandedNodeId = nodeIds.find((id) => !expandedNodes.includes(id)) - - // eslint-disable-next-line no-console - console.log('11111====') - if (newlyExpandedNodeId) { - const loadChildren = async () => { - try { - const node = findNode(treeData, newlyExpandedNodeId) - - // eslint-disable-next-line no-console - console.log(node, 'node===node====') - - if (!node) return + + for (const node of data) { - let responseData - const reqPath = pathname?.replace('/tree', '') + '/' + node.label + if (node.id === nodeId) return node; - if (node.path && node.path !== '' && node.path !== undefined) { - responseData = await fetch(`/api/tree?path=${node.path}`).then((response) => response.json()) - } else { - responseData = await fetch(`/api/tree?path=${reqPath}`).then((response) => response.json()) - } + if (node.children) { + const found = findNode(node.children, nodeId); - const subTreeData = convertToTreeData(responseData.data.data) - const newTreeData = appendTreeData(treeData, subTreeData, newlyExpandedNodeId) - - setTreeData(newTreeData) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching tree data:', error) + if (found) return found; + } + } + return null; + }, + [], + ); + + const handleNodeToggle = useCallback( + (_event: React.SyntheticEvent | null, nodeIds: string[]) => { + const newlyExpandedId = nodeIds.find((id) => !expandedNodes.includes(id)); + + if (newlyExpandedId) { + const targetNode = findNode(treeData, newlyExpandedId); + + const reqPath = targetNode?.path?.startsWith('/') + ? targetNode.path + : `/${targetNode?.path}`; + + if ( + targetNode && + targetNode.content_type === 'directory' && + targetNode.children?.[0]?.label === '加载中...' // 中文判断 + ) { + setLoadPath(reqPath); + setTargetNodeId(newlyExpandedId); + // setLoadingError(null); } } + + setExpandedNodes(nodeIds); + }, + [expandedNodes, treeData, setLoadPath, setTargetNodeId, findNode], + ); - loadChildren() + useEffect(() => { + if (!loadPath) return; + + if (response && !isLoading) { + if ( + !response.req_result || + !Array.isArray(response.data) || + response.data.length === 0 + ) { + // console.error('API数据格式错误:', response); + // setLoadingError('数据格式错误或为空'); + return; + } + + const items = response.data; + const newChildren = convertToTreeData(loadPath, items); + const newTree = updateTreeData(treeData, targetNodeId!, newChildren); + + setTreeData(newTree); + setLoadPath(null); + setTargetNodeId(null); } + + // if (error) { + // console.error('Error fetching data:', error); + // // setLoadingError(error.message || '加载失败'); + // } + }, [response, isLoading, error, loadPath, targetNodeId, treeData, convertToTreeData, updateTreeData]); - setExpandedNodes(nodeIds) - } - - const handleNodeSelect = (_event: React.SyntheticEvent | null, nodeId: string | null) => { - if (!nodeId) return - setSelectedNode(nodeId) - const node = findNode(treeData, nodeId) + const handleNodeSelect = useCallback( + (_event: React.SyntheticEvent | null, nodeId: string | null) => { + if (!nodeId) return; - // eslint-disable-next-line no-console - console.log('node====') + setSelectedNode(nodeId); + const node = findNode(treeData, nodeId); - if (!node) return + if (!node) return; - const real_path = pathname?.replace('/tree', '') - const modified_path = real_path?.replace('/code/', '/code/blob/') + const basePath = pathname?.replace(`/${scope}/code/tree`, '') || ''; + + let fullPath = node.path; - if (node?.content_type === 'directory') { - router.push(`${pathname}/${node.label}`) - } else { - router.push(`/${modified_path}/${node.label}`) - } - } - - const findNode = (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => { - for (const node of data) { - if (node.id === nodeId) return node - if (node.children) { - const found = findNode(node.children, nodeId) + if (basePath && fullPath?.startsWith(basePath)) { + fullPath = fullPath?.substring(basePath.length); + } - if (found) return found + fullPath = fullPath?.replace(/^\//, ''); + if (node?.isLeaf) { + router.push(`/${scope}/code/blob${basePath}/${fullPath}`); } - } - return null - } + + }, + [pathname, router, treeData, scope, findNode], + ); return ( - - ) -} - -export default RepoTree + <> + {isInitialLoading ? ( + + + + ) : treeData.length === 0 ? ( + + no data + + ) : ( + ( + + ) + }} + > + + )} + {/* {loadingError && ( + + 加载错误: {loadingError} + + )} */} + + ); +}; + +export default RepoTree; \ No newline at end of file diff --git a/moon/apps/web/hooks/useGetTree.ts b/moon/apps/web/hooks/useGetTree.ts new file mode 100644 index 000000000..8541d43b1 --- /dev/null +++ b/moon/apps/web/hooks/useGetTree.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import { GetApiTreeParams, RequestParams } from '@gitmono/types' + +export function useGetTree(params: GetApiTreeParams, requestParams?: RequestParams) { + return useQuery({ + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: legacyApiClient.v1.getApiTree().requestKey(params), + queryFn: () => legacyApiClient.v1.getApiTree().request(params, requestParams), + enabled: !!params.path, + }) +} \ No newline at end of file diff --git a/moon/apps/web/pages/[org]/code/blob/[...path].tsx b/moon/apps/web/pages/[org]/code/blob/[...path].tsx index df00ebcbb..750fa2716 100644 --- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx +++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx @@ -1,6 +1,6 @@ import React from 'react' import { Flex, Layout } from 'antd' -import Bread from '@/components/CodeView/TreeView/BreadCrumb' +import BreadCrumb from '@/components/CodeView/TreeView/BreadCrumb' import CodeContent from '@/components/CodeView/BlobView/CodeContent' import { AppLayout } from '@/components/Layout/AppLayout' import AuthAppProviders from '@/components/Providers/AuthAppProviders' @@ -80,7 +80,7 @@ function BlobPage() {
- + diff --git a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx index 318b1852f..a5e7df4ca 100644 --- a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx +++ b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx @@ -9,7 +9,7 @@ import { CommonResultVecTreeCommitItem } from '@gitmono/types/generated' import { LoadingSpinner } from '@gitmono/ui' import CodeTable from '@/components/CodeView/CodeTable' -import Bread from '@/components/CodeView/TreeView/BreadCrumb' +import BreadCrumb from '@/components/CodeView/TreeView/BreadCrumb' import CloneTabs from '@/components/CodeView/TreeView/CloneTabs' import RepoTree from '@/components/CodeView/TreeView/RepoTree' import { AppLayout } from '@/components/Layout/AppLayout' @@ -81,7 +81,7 @@ function TreeDetailPage() { ) : ( - + {canClone?.data && (