From bb23f6f1553c27b671459f826015968b0d5b5c70 Mon Sep 17 00:00:00 2001 From: lixiaoshu Date: Tue, 10 Jun 2025 23:09:55 +0800 Subject: [PATCH 1/5] feat: The function implementation of tree component --- .../CodeView/TreeView/BreadCrumb.tsx | 4 +- .../components/CodeView/TreeView/RepoTree.tsx | 504 +++++++++++------- moon/apps/web/hooks/useGetTree.ts | 12 + .../web/pages/[org]/code/blob/[...path].tsx | 4 +- .../pages/[org]/code/tree/[...path]/index.tsx | 4 +- 5 files changed, 329 insertions(+), 199 deletions(-) create mode 100644 moon/apps/web/hooks/useGetTree.ts 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..a7557f16e 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,343 @@ function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { {...other} sx={{ display: 'flex', - alignItems: 'center' + alignItems: 'center', }} + onClick={() => {}} > - {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'> { + loadingNode?: string | null; + } const CustomTreeItem = React.forwardRef(function CustomTreeItem( - props: CustomTreeItemProps, - ref: React.Ref + { loadingNode, ...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 }) + status, + } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = useTreeItemModel(itemId)! - - 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 [loadingNode, setLoadingNode] = useState(null); + + const [loadPath, setLoadPath] = useState(null); + const [targetNodeId, setTargetNodeId] = useState(null); + const [loadingError, setLoadingError] = useState(null); + const [isInitialLoading, setIsInitialLoading] = useState(true); + + 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]); const sortProjectsByType = (projects: any[]) => { - return projects?.sort((a, b) => { + console.log(projects, 'projects===projects=='); + 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, + }; + } + if (node.children && node.children.length > 0) { + return { + ...node, + children: updateTreeData(node.children, nodeId, newChildren), + }; } - } else if (item.children) { - return { - ...item, - children: appendTreeData(item.children, subItems, nodeId) + return node; + }); + }, + [], + ); + + 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 === '加载中...' // 中文判断 + ) { + setLoadingNode(targetNode?.id??null); + setLoadPath(reqPath); + setTargetNodeId(newlyExpandedId); + setLoadingError(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 - - let responseData - const reqPath = pathname?.replace('/tree', '') + '/' + node.label - - 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()) - } - - 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) + + setExpandedNodes(nodeIds); + }, + [expandedNodes, treeData, setLoadingNode, setLoadPath, setTargetNodeId], + ); + + const QueryComponent = React.memo(() => { + if (!loadPath) return null; + + const { data: response, isLoading, error } = useGetTree({ path: loadPath }); + + useEffect(() => { + 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); + setLoadingNode(null); + setLoadPath(null); + setTargetNodeId(null); } + + if (error) { + console.error('加载子节点失败:', error); + setLoadingError(error.message || '加载失败'); + setLoadingNode(null); + } + }, [response, isLoading, error, loadPath, targetNodeId, treeData, convertToTreeData, updateTreeData]); + + return null; + }); - loadChildren() - } - - setExpandedNodes(nodeIds) - } - - const handleNodeSelect = (_event: React.SyntheticEvent | null, nodeId: string | null) => { - if (!nodeId) return - - setSelectedNode(nodeId) - const node = findNode(treeData, nodeId) - - // eslint-disable-next-line no-console - console.log('node====') + const handleNodeSelect = useCallback( + (_event: React.SyntheticEvent | null, nodeId: string | null) => { + if (!nodeId) return; - if (!node) return + setSelectedNode(nodeId); + const node = findNode(treeData, nodeId); - const real_path = pathname?.replace('/tree', '') - const modified_path = real_path?.replace('/code/', '/code/blob/') + if (!node) return; - 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) + const basePath = pathname?.replace(`/${scope}/code/tree`, '') || ''; + + let fullPath = node.path; + 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], + ); + + const findNode = useCallback( + (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => { + if (!Array.isArray(data) || data.length === 0) { + return null; + } + + for (const node of data) { + if (node.id === nodeId) return node; + if (node.children) { + const found = findNode(node.children, nodeId); + if (found) return found; + } + } + return null; + }, + [], + ); 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 && ( From 7926244bb504964291fd7b7f24e5a55b7f38d1a6 Mon Sep 17 00:00:00 2001 From: lixiaoshu Date: Wed, 11 Jun 2025 12:12:41 +0800 Subject: [PATCH 2/5] fix: fix eslint --- .../components/CodeView/TreeView/RepoTree.tsx | 133 +++++++++--------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx index a7557f16e..8cd28cf38 100644 --- a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx +++ b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx @@ -57,7 +57,6 @@ function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { display: 'flex', alignItems: 'center', }} - onClick={() => {}} > {Icon && ( @@ -81,11 +80,10 @@ const getIconFromFileType = (fileType: FileType, isExpanded:Boolean) => { interface CustomTreeItemProps extends Omit, Omit, 'onFocus'> { - loadingNode?: string | null; } const CustomTreeItem = React.forwardRef(function CustomTreeItem( - { loadingNode, ...props }: CustomTreeItemProps, + { ...props }: CustomTreeItemProps, ref: React.Ref, ) { const { id, itemId, label, disabled, children, ...other } = props; @@ -152,25 +150,26 @@ const RepoTree = ({ directory }: { directory: any[] }) => { const basePath = pathname?.replace(`/${scope}/code/tree`, ''); const [treeData, setTreeData] = useState([]); + const [expandedNodes, setExpandedNodes] = useState([]); const [selectedNode, setSelectedNode] = useState(null); - const [loadingNode, setLoadingNode] = useState(null); const [loadPath, setLoadPath] = useState(null); const [targetNodeId, setTargetNodeId] = useState(null); - const [loadingError, setLoadingError] = 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); + // console.warn('convertToTreeData: 接收到非数组或空数据', directory); return []; } return sortProjectsByType(directory).map((item) => { const currentPath = `${parentBasePath}/${item.name}`.replace('//', '/') || '/'; - console.log('生成节点路径:', item.name, '=>', currentPath); + // console.log('生成节点路径:', item.name, '=>', currentPath); return { id: uuidv4(), @@ -191,20 +190,20 @@ const RepoTree = ({ directory }: { directory: any[] }) => { useEffect(() => { if (!Array.isArray(directory) || directory.length === 0) { - console.warn('RepoTree: 接收到非数组或空的初始数据', directory); + // console.warn('RepoTree: 接收到非数组或空的初始数据', directory); setIsInitialLoading(false); return; } const rootPath = basePath || '/'; // 使用基路径作为根路径 + setTreeData(convertToTreeData(rootPath, directory)); setIsInitialLoading(false); - }, [directory, convertToTreeData]); + }, [directory, convertToTreeData,basePath]); const sortProjectsByType = (projects: any[]) => { - console.log(projects, 'projects===projects=='); if (!Array.isArray(projects) || projects.length === 0) { - console.warn('sortProjectsByType: 接收到非数组或空数据', projects); + // console.warn('sortProjectsByType: 接收到非数组或空数据', projects); return []; } @@ -222,7 +221,7 @@ const RepoTree = ({ directory }: { directory: any[] }) => { const updateTreeData = useCallback( (currentTree: MuiTreeNode[], nodeId: string, newChildren: MuiTreeNode[]): any => { if (!Array.isArray(currentTree) || currentTree.length === 0) { - console.warn('updateTreeData: 接收到非数组或空的当前树数据', currentTree); + // console.warn('updateTreeData: 接收到非数组或空的当前树数据', currentTree); return []; } @@ -246,6 +245,27 @@ const RepoTree = ({ directory }: { directory: any[] }) => { [], ); + const findNode = useCallback( + (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => { + if (!Array.isArray(data) || data.length === 0) { + return null; + } + + for (const node of data) { + + if (node.id === nodeId) return node; + + if (node.children) { + const found = findNode(node.children, nodeId); + + if (found) return found; + } + } + return null; + }, + [], + ); + const handleNodeToggle = useCallback( (_event: React.SyntheticEvent | null, nodeIds: string[]) => { const newlyExpandedId = nodeIds.find((id) => !expandedNodes.includes(id)); @@ -262,54 +282,46 @@ const RepoTree = ({ directory }: { directory: any[] }) => { targetNode.content_type === 'directory' && targetNode.children?.[0]?.label === '加载中...' // 中文判断 ) { - setLoadingNode(targetNode?.id??null); setLoadPath(reqPath); setTargetNodeId(newlyExpandedId); - setLoadingError(null); + // setLoadingError(null); } } setExpandedNodes(nodeIds); }, - [expandedNodes, treeData, setLoadingNode, setLoadPath, setTargetNodeId], + [expandedNodes, treeData, setLoadPath, setTargetNodeId, findNode], ); - const QueryComponent = React.memo(() => { - if (!loadPath) return null; - - const { data: response, isLoading, error } = useGetTree({ path: loadPath }); - - useEffect(() => { - 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); - setLoadingNode(null); - setLoadPath(null); - setTargetNodeId(null); + 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; } - if (error) { - console.error('加载子节点失败:', error); - setLoadingError(error.message || '加载失败'); - setLoadingNode(null); - } - }, [response, isLoading, error, loadPath, targetNodeId, treeData, convertToTreeData, updateTreeData]); + const items = response.data; + const newChildren = convertToTreeData(loadPath, items); + const newTree = updateTreeData(treeData, targetNodeId!, newChildren); + + setTreeData(newTree); + setLoadPath(null); + setTargetNodeId(null); + } - return null; - }); + // if (error) { + // console.error('Error fetching data:', error); + // // setLoadingError(error.message || '加载失败'); + // } + }, [response, isLoading, error, loadPath, targetNodeId, treeData, convertToTreeData, updateTreeData]); + const handleNodeSelect = useCallback( (_event: React.SyntheticEvent | null, nodeId: string | null) => { @@ -323,6 +335,7 @@ const RepoTree = ({ directory }: { directory: any[] }) => { const basePath = pathname?.replace(`/${scope}/code/tree`, '') || ''; let fullPath = node.path; + if (basePath && fullPath?.startsWith(basePath)) { fullPath = fullPath?.substring(basePath.length); } @@ -333,30 +346,11 @@ const RepoTree = ({ directory }: { directory: any[] }) => { } }, - [pathname, router, treeData], - ); - - const findNode = useCallback( - (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => { - if (!Array.isArray(data) || data.length === 0) { - return null; - } - - for (const node of data) { - if (node.id === nodeId) return node; - if (node.children) { - const found = findNode(node.children, nodeId); - if (found) return found; - } - } - return null; - }, - [], + [pathname, router, treeData, scope, findNode], ); return ( <> - {isInitialLoading ? ( @@ -377,8 +371,7 @@ const RepoTree = ({ directory }: { directory: any[] }) => { slots={{ item: (itemProps) => ( ) }} From 48408ee34f4f1a08d67f322d9bb7f448d836f602 Mon Sep 17 00:00:00 2001 From: lixiaoshu Date: Tue, 17 Jun 2025 20:43:28 +0800 Subject: [PATCH 3/5] feat: Add page scrolling, display README files, and show commit history. --- .../web/components/CodeView/CodeTable.tsx | 19 ++++-- .../web/components/CodeView/CommitDetails.tsx | 16 +++++ .../web/components/CodeView/CommitHistory.tsx | 42 ++++++++++-- .../CodeView/TreeView/CloneTabs.tsx | 10 +-- .../components/CodeView/TreeView/RepoTree.tsx | 2 +- moon/apps/web/components/CodeView/index.tsx | 39 ++--------- .../pages/[org]/code/tree/[...path]/index.tsx | 66 +++++++------------ moon/apps/web/pages/_app.tsx | 2 +- 8 files changed, 100 insertions(+), 96 deletions(-) create mode 100644 moon/apps/web/components/CodeView/CommitDetails.tsx diff --git a/moon/apps/web/components/CodeView/CodeTable.tsx b/moon/apps/web/components/CodeView/CodeTable.tsx index 76c91b231..d022daf02 100644 --- a/moon/apps/web/components/CodeView/CodeTable.tsx +++ b/moon/apps/web/components/CodeView/CodeTable.tsx @@ -6,11 +6,9 @@ import { useMemo } from 'react' import { DocumentIcon, FolderIcon } from '@heroicons/react/20/solid' import { formatDistance, fromUnixTime } from 'date-fns' import { usePathname, useRouter } from 'next/navigation' -import Markdown from 'react-markdown' - -import styles from './CodeTable.module.css' import RTable from './Table' import { columnsType, DirectoryType } from './Table/type' +import Markdown from 'react-markdown' export interface DataType { oid: string @@ -20,11 +18,19 @@ export interface DataType { date: number } -const CodeTable = ({ directory, readmeContent, loading }: any) => { +const CodeTable = ({ directory, loading, readmeContent}: any) => { const router = useRouter() const pathname = usePathname() let real_path = pathname?.replace('/tree', '') + const markdownContentStyle = { + margin:' 0 auto', + marginTop: 20, + border: '1px solid rgba(0, 0, 0, 0.112)', + padding: '2%', + borderRadius: '0.5rem', + } + const columns = useMemo[]>( () => [ { @@ -102,9 +108,8 @@ const CodeTable = ({ directory, readmeContent, loading }: any) => { onClick={handleRowClick} loading={loading} /> - - {readmeContent && ( -
+ {readmeContent && ( +
{readmeContent}
diff --git a/moon/apps/web/components/CodeView/CommitDetails.tsx b/moon/apps/web/components/CodeView/CommitDetails.tsx new file mode 100644 index 000000000..5e6b5c757 --- /dev/null +++ b/moon/apps/web/components/CodeView/CommitDetails.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const CommitDetails = () => { + + return ( +
+ CommitDetails
+ CommitDetails
+ CommitDetails
+ CommitDetails
+ CommitDetails
+
+ ); +}; + +export default CommitDetails; \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/CommitHistory.tsx b/moon/apps/web/components/CodeView/CommitHistory.tsx index 68d193caa..60db7eadb 100644 --- a/moon/apps/web/components/CodeView/CommitHistory.tsx +++ b/moon/apps/web/components/CodeView/CommitHistory.tsx @@ -1,6 +1,8 @@ -import { Card, Flex } from '@radix-ui/themes' -import { Avatar, Button, ClockIcon } from '@gitmono/ui' +import { Flex } from '@radix-ui/themes' +import { Avatar, Button, ClockIcon, EyeIcon } from '@gitmono/ui' import { MemberHovercard } from '@/components/InlinePost/MemberHovercard' +import CommitDetails from './CommitDetails' +import { useState } from 'react' interface UserInfo { avatar_url: string @@ -14,10 +16,23 @@ export interface CommitInfo { date: string } -export default function CommitHistory({ info }: { info: CommitInfo }) { +const CommitHyStyle = { + width: '100%', + background: '#fff', + border: '1px solid #d1d9e0', + borderRadius: 8 +} + +export default function CommitHistory({ flag, info }: {flag:string, info: CommitInfo }) { + const [Expand,setExpand] = useState(false) + const ExpandDetails =()=>{ + setExpand(!Expand) + } + return ( - - + <> +
+ @@ -29,6 +44,19 @@ export default function CommitHistory({ info }: { info: CommitInfo }) { {info.message} + { + flag === 'contents' && + + + + } {info.hash} · {info.date} @@ -44,6 +72,8 @@ export default function CommitHistory({ info }: { info: CommitInfo }) { - +
+ {Expand &&} + ) } diff --git a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx index 483c1f21b..ff70ce9ff 100644 --- a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx +++ b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react' import { Button, Input, Popover, Space, Tabs, TabsProps } from 'antd' import copy from 'copy-to-clipboard' import { usePathname } from 'next/navigation' - import { CheckIcon, CopyIcon, DownloadIcon } from '@gitmono/ui/Icons' +import {LEGACY_API_URL} from '@gitmono/config' const CloneTabs = ({ endpoint }: any) => { const pathname = usePathname() @@ -16,13 +16,13 @@ const CloneTabs = ({ endpoint }: any) => { } useEffect(() => { - if (endpoint) { - const url = new URL(endpoint) + if (LEGACY_API_URL) { + const url = new URL(LEGACY_API_URL) if (active_tab === '1') { - setText(`${url.href}${pathname?.replace('/tree/', '')}.git`) + setText(`${url.href}${pathname?.replace('/myorganization/code/tree/', '')}.git`) } else { - setText(`ssh://git@${url.host}${pathname?.replace('/tree', '')}.git`) + setText(`ssh://git@${url.host}${pathname?.replace('/myorganization/code/tree', '')}.git`) } } }, [pathname, active_tab, endpoint]) diff --git a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx index 8cd28cf38..7e56e3077 100644 --- a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx +++ b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx @@ -61,7 +61,7 @@ function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { {Icon && ( )} - {children} + {children} ); } diff --git a/moon/apps/web/components/CodeView/index.tsx b/moon/apps/web/components/CodeView/index.tsx index ad37c5317..d0fcf9b90 100644 --- a/moon/apps/web/components/CodeView/index.tsx +++ b/moon/apps/web/components/CodeView/index.tsx @@ -1,12 +1,13 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useMemo } from 'react' import { CommonResultVecTreeCommitItem } from '@gitmono/types/generated' import { useGetTreeCommitInfo } from '@/hooks/useGetTreeCommitInfo' import SpinnerTable from './TableWithLoading' +import { useGetBlob } from '@/hooks/useGetBlob' export default function CodeView() { const { data: TreeCommitInfo } = useGetTreeCommitInfo('/') @@ -14,38 +15,8 @@ export default function CodeView() { type DirectoryType = NonNullable const directory: DirectoryType = useMemo(() => TreeCommitInfo?.data ?? [], [TreeCommitInfo]) - const [readmeContent, setReadmeContent] = useState('') + const reqPath = `/README.md` + const {data: readmeContent} = useGetBlob({path:reqPath}) - const fetchData = useCallback(async () => { - if (directory.length === 0) return - - try { - const content = await getReadmeContent('/', directory) - - setReadmeContent(content) - } catch (error) { - // console.error('Error fetching data:', error); - } - }, [directory]) - - useEffect(() => { - fetchData() - }, [fetchData]) - - return -} - -async function getReadmeContent(pathname: string, directory: any) { - let readmeContent = '' - - for (const project of directory || []) { - if (project.name === 'README.md' && project.content_type === 'file') { - const res = await fetch(`/api/blob?path=${pathname}/README.md`) - const response = await res.json() - - readmeContent = response.data.data - break - } - } - return readmeContent + return } 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 a5e7df4ca..ced51ea23 100644 --- a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx +++ b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { Theme } from '@radix-ui/themes' import { Flex, Layout } from 'antd' import { useParams } from 'next/navigation' @@ -16,6 +16,8 @@ import { AppLayout } from '@/components/Layout/AppLayout' import AuthAppProviders from '@/components/Providers/AuthAppProviders' import { useGetTreeCommitInfo } from '@/hooks/useGetTreeCommitInfo' import { useGetTreePathCanClone } from '@/hooks/useGetTreePathCanClone' +import CommitHistory from '@/components/CodeView/CommitHistory' +import { useGetBlob } from '@/hooks/useGetBlob' function TreeDetailPage() { const params = useParams() @@ -28,26 +30,22 @@ function TreeDetailPage() { const directory: DirectoryType = useMemo(() => TreeCommitInfo?.data ?? [], [TreeCommitInfo]) const { data: canClone } = useGetTreePathCanClone({ path: new_path }) - const [readmeContent, setReadmeContent] = useState('') - const [endpoint, setEndPoint] = useState('') - useEffect(() => { - const fetchData = async () => { - try { - const readmeContent = await getReadmeContent(new_path, directory) + const reqPath = `${new_path}/README.md` + const {data: readmeContent}=useGetBlob({path:reqPath}) - setReadmeContent(readmeContent) - const endpoint = await getEndpoint() - setEndPoint(endpoint) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching data:', error) - } - } + const commitInfo = { + user: { + avatar_url: 'https://avatars.githubusercontent.com/u/112836202?v=4&size=40', + name: 'yetianxing2014' + }, + message: '[feat(libra)]: 为 config 命令添加 --default参数 (#1119)', + hash: '5fe4235', + date: '3 months ago' + } + - fetchData() - }, [new_path, directory]) const treeStyle = { borderRadius: 8, @@ -61,6 +59,7 @@ function TreeDetailPage() { borderRadius: 8, overflow: 'hidden', width: 'calc(80% - 8px)', + height:'100%', background: '#fff' } @@ -73,7 +72,7 @@ function TreeDetailPage() { } return ( -
+
{!TreeCommitInfo ? (
@@ -84,7 +83,7 @@ function TreeDetailPage() { {canClone?.data && ( - + )} @@ -93,7 +92,12 @@ function TreeDetailPage() { - + { + commitInfo && + + + } + )} @@ -101,28 +105,6 @@ function TreeDetailPage() { ) } -async function getReadmeContent(pathname: string, directory: any) { - let readmeContent = '' - - for (const project of directory || []) { - if (project.name === 'README.md' && project.content_type === 'file') { - const res = await fetch(`/api/blob?path=${pathname}/README.md`) - const response = await res.json() - - readmeContent = response.data.data - break - } - } - return readmeContent -} - -async function getEndpoint() { - const res = await fetch(`/host`) - const response = await res.json() - - return response.endpoint -} - TreeDetailPage.getProviders = ( page: | string diff --git a/moon/apps/web/pages/_app.tsx b/moon/apps/web/pages/_app.tsx index 4ac09dca7..0cccf4356 100644 --- a/moon/apps/web/pages/_app.tsx +++ b/moon/apps/web/pages/_app.tsx @@ -4,7 +4,7 @@ import 'styles/editor.css' import 'styles/global.css' // web only import 'styles/prose.css' import '@radix-ui/themes/styles.css' -import '@git-diff-view/react/styles/diff-view.css'; +// import '@git-diff-view/react/styles/diff-view.css'; import { useEffect } from 'react' import { IS_PRODUCTION, LAST_CLIENT_JS_BUILD_ID_LS_KEY } from '@gitmono/config' From 851b3e3a90c1ac93270ec5f6d1e4fe78f3f2a194 Mon Sep 17 00:00:00 2001 From: lixiaoshu Date: Wed, 18 Jun 2025 09:51:03 +0800 Subject: [PATCH 4/5] fix: fix conflicts --- moon/apps/web/pages/_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon/apps/web/pages/_app.tsx b/moon/apps/web/pages/_app.tsx index 0cccf4356..4ac09dca7 100644 --- a/moon/apps/web/pages/_app.tsx +++ b/moon/apps/web/pages/_app.tsx @@ -4,7 +4,7 @@ import 'styles/editor.css' import 'styles/global.css' // web only import 'styles/prose.css' import '@radix-ui/themes/styles.css' -// import '@git-diff-view/react/styles/diff-view.css'; +import '@git-diff-view/react/styles/diff-view.css'; import { useEffect } from 'react' import { IS_PRODUCTION, LAST_CLIENT_JS_BUILD_ID_LS_KEY } from '@gitmono/config' From 25951e19270d6582e465b7b92747993d0dff5f80 Mon Sep 17 00:00:00 2001 From: lixiaoshu Date: Wed, 18 Jun 2025 10:14:49 +0800 Subject: [PATCH 5/5] fix: fix eslint --- moon/apps/web/pages/[org]/code/blob/[...path].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon/apps/web/pages/[org]/code/blob/[...path].tsx b/moon/apps/web/pages/[org]/code/blob/[...path].tsx index 600da3460..8ab2d1731 100644 --- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx +++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx @@ -94,7 +94,7 @@ function BlobPage() { - +