diff --git a/moon/apps/web/components/CodeView/CodeTable.tsx b/moon/apps/web/components/CodeView/CodeTable.tsx index d022daf02..d726be6f2 100644 --- a/moon/apps/web/components/CodeView/CodeTable.tsx +++ b/moon/apps/web/components/CodeView/CodeTable.tsx @@ -39,10 +39,10 @@ const CodeTable = ({ directory, loading, readmeContent}: any) => { key: 'name', render: (_, record) => ( <> -
- {record.content_type === 'directory' && } - {record.content_type === 'file' && } - {record.name} +
+ {record.content_type === 'directory' && } + {record.content_type === 'file' && } + {record.name}
) diff --git a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx index 7e56e3077..0a92030be 100644 --- a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx +++ b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx @@ -143,11 +143,14 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( ); }); -const RepoTree = ({ directory }: { directory: any[] }) => { +const RepoTree = ( {flag, directory }: {flag:string, directory: any[] }) => { const router = useRouter(); const scope = router.query.org as string; const pathname = usePathname(); - const basePath = pathname?.replace(`/${scope}/code/tree`, ''); + const basePath = pathname?.replace( + new RegExp(`\\/${scope}\\/code\\/(tree|blob)`), + '' + ); const [treeData, setTreeData] = useState([]); @@ -168,8 +171,11 @@ const RepoTree = ({ directory }: { directory: any[] }) => { return sortProjectsByType(directory).map((item) => { - const currentPath = `${parentBasePath}/${item.name}`.replace('//', '/') || '/'; - // console.log('生成节点路径:', item.name, '=>', currentPath); + const nodeName = item?.name ?? ''; + const currentPath = `${parentBasePath}/${nodeName}`.replace('//', '/') || '/'; + + // eslint-disable-next-line no-console + // console.log('生成节点路径:', nodeName, '=>', currentPath); return { id: uuidv4(), @@ -277,6 +283,12 @@ const RepoTree = ({ directory }: { directory: any[] }) => { ? targetNode.path : `/${targetNode?.path}`; + // eslint-disable-next-line no-console + // console.log(targetNode?.path, 'reqPath=1') + + // eslint-disable-next-line no-console + // console.log(reqPath,'reqPath=reqPath') + if ( targetNode && targetNode.content_type === 'directory' && @@ -341,12 +353,12 @@ const RepoTree = ({ directory }: { directory: any[] }) => { } fullPath = fullPath?.replace(/^\//, ''); - if (node?.isLeaf) { + if (flag ==='contents' && node?.isLeaf) { router.push(`/${scope}/code/blob${basePath}/${fullPath}`); } }, - [pathname, router, treeData, scope, findNode], + [findNode, treeData, pathname, scope, flag, router], ); return ( diff --git a/moon/apps/web/components/DiffView/FileDiff.tsx b/moon/apps/web/components/DiffView/FileDiff.tsx index 84b46aa6f..ec2e54c8f 100644 --- a/moon/apps/web/components/DiffView/FileDiff.tsx +++ b/moon/apps/web/components/DiffView/FileDiff.tsx @@ -4,6 +4,7 @@ import { DiffFile, DiffModeEnum, DiffView } from '@git-diff-view/react' import { ExpandIcon, SparklesIcon } from '@gitmono/ui/Icons' import { cn } from '@gitmono/ui/src/utils' import { parsedDiffs } from '@/components/DiffView/parsedDiffs' +import TreeView from './TreeView' function calculateDiffStatsFromRawDiff(diffText: string): { additions: number; deletions: number } { const lines = diffText.split('\n'); @@ -51,7 +52,7 @@ export default function FileDiff({ diffs }: { diffs: string }) { const parsedFiles = useMemo(() => generateParsedFiles(diffFiles), [diffFiles]); - const [selectedPath, setSelectedPath] = useState(null); + // const [selectedPath, setSelectedPath] = useState(null); const [expandedMap, setExpandedMap] = useState>(() => Object.fromEntries(diffFiles.map((f) => [f.path, false])) @@ -89,9 +90,9 @@ export default function FileDiff({ diffs }: { diffs: string }) { return (
-
    + {/*
      {parsedFiles.map(({ file }) => (
    • {file.path}
    • - ))} -
    + )) + } + +
*/} +
diff --git a/moon/apps/web/components/DiffView/TreeView.tsx b/moon/apps/web/components/DiffView/TreeView.tsx new file mode 100644 index 000000000..6e7d4ccdf --- /dev/null +++ b/moon/apps/web/components/DiffView/TreeView.tsx @@ -0,0 +1,358 @@ +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'; +import { + TreeItemContent, + TreeItemGroupTransition, + TreeItemIconContainer, + TreeItemLabel, + 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?: FileType; + isLeaf?: boolean; + children?: MuiTreeNode[]; +} + +type FileType = 'file' | 'directory'; + +interface ExtendedTreeItemProps { + content_type?: FileType; + id: string; + label: string; +} + +interface CustomLabelProps { + children: React.ReactNode; + icon?: React.ElementType; + expandable?: boolean; +} + +function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) { + return ( + + {Icon && ( + + )} + {children} + + ); +} + +const getIconFromFileType = (fileType: FileType, isExpanded:Boolean) => { + switch (fileType) { + case 'file': + return ArticleIcon; + case 'directory': + return isExpanded ? FolderOpenIcon :FolderRounded; + default: + return ArticleIcon; + } +}; + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> { + } + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + { ...props }: CustomTreeItemProps, + ref: React.Ref, +) { + const { id, itemId, label, disabled, children, ...other } = props; + const { + getContextProviderProps, + getRootProps, + getContentProps, + getIconContainerProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = useTreeItemModel(itemId)!; + let icon; + + if (status.expandable) { + icon = getIconFromFileType(item.content_type || 'file', status.expanded); + } else if (item.content_type) { + icon = getIconFromFileType(item.content_type, false); + } + + const StyledGroupTransition = styled(TreeItemGroupTransition)(({ theme }) => ({ + marginLeft: 15, + borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, + })); + + return ( + + + {item.label === '加载中...' ? ( + + + + + + ) : ( + + + + + + {/* label */} + + + + )} + {children && } + + + ); +}); + +const RepoTree = ({ directory }: { directory: any[] }) => { + const router = useRouter(); + const scope = router.query.org as string; + const pathname = usePathname(); + const basePath = pathname?.replace( + new RegExp(`\\/${scope}\\/code\\/(tree|blob)`), + '' + ); + + const [treeData, setTreeData] = useState([]); + + const [expandedNodes, setExpandedNodes] = useState([]); + const [selectedNode, setSelectedNode] = useState(null); + + const [loadPath, setLoadPath] = useState(null); + const [targetNodeId, setTargetNodeId] = useState(null); + + 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: '我是文件夹', + path: currentPath, + isLeaf: item.content_type !== 'directory', + content_type: item.content_type, + children: item.content_type === 'directory' ? [ + { + id: uuidv4(), + label: '加载中...', + isLeaf: true, + }, + ] : undefined, + }; + }); + }, []); + + useEffect(() => { + + const rootPath = basePath || '/'; // 使用基路径作为根路径 + + setTreeData(convertToTreeData(rootPath, directory)); + }, [directory, convertToTreeData,basePath]); + + const sortProjectsByType = (projects: any[]) => { + 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; + } else if (a.content_type === 'file' && b.content_type === 'directory') { + return 1; + } else { + return 0; + } + }); + }; + + 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), + }; + } + return node; + }); + }, + [], + ); + + 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)); + + 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], + ); + + 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]); + + + const handleNodeSelect = useCallback( + (_event: React.SyntheticEvent | null, nodeId: string | null) => { + if (!nodeId) return; + + setSelectedNode(nodeId); + const node = findNode(treeData, nodeId); + + if (!node) return; + + }, + [treeData, findNode], + ); + + return ( + <> + ( + + ) + }} + > + + + ); +}; + +export default RepoTree; \ 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 8ab2d1731..7b3765125 100644 --- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx +++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Flex, Layout } from 'antd' import BreadCrumb from '@/components/CodeView/TreeView/BreadCrumb' import CodeContent from '@/components/CodeView/BlobView/CodeContent' @@ -8,6 +8,9 @@ import { useGetBlob } from '@/hooks/useGetBlob' import { useRouter } from 'next/router' import { CommentSection } from '@/components/CodeView/BlobView/CommentSection' import CommitHistory, { CommitInfo } from '@/components/CodeView/CommitHistory' +import RepoTree from '@/components/CodeView/TreeView/RepoTree' +import { CommonResultVecTreeCommitItem } from '@gitmono/types/generated' +import { useGetTreeCommitInfo } from '@/hooks/useGetTreeCommitInfo' const codeStyle = { borderRadius: 8, @@ -17,6 +20,14 @@ const codeStyle = { margin: '0 8px' } +const treeStyle = { + borderRadius: 8, + overflow: 'hidden', + width: 'calc(20% - 8px)', + maxWidth: 'calc(20% - 8px)', + background: '#fff' +} + interface Comment { id: string content: string @@ -77,6 +88,37 @@ function BlobPage() { hash: '5fe4235', date: '3 months ago' } + + const newPath = new_path?.split("/").slice(0, -1).join("/") + const { data: TreeCommitInfo } = useGetTreeCommitInfo(newPath) + + type DirectoryType = NonNullable + const directory: DirectoryType = useMemo(() => TreeCommitInfo?.data ?? [], [TreeCommitInfo]) + const [newDirectory, setNewDirectory] = useState([]) + + // eslint-disable-next-line no-console + console.log(directory, 'directory==directory') + + useEffect(()=>{ + const handleDirectory = () => { + const filteredItems = directory.filter((item) => { + return item.content_type !== 'directory'; + }); + + // eslint-disable-next-line no-console + console.log(filteredItems, 'filteredItems=='); + setNewDirectory(filteredItems); // 直接设置过滤后的数组,而非嵌套数组 + }; + + handleDirectory() + + },[directory, setNewDirectory]) + + + + + + const handleAddComment = (__content: string, __lineNumber?: number) => { //wait for complete @@ -93,17 +135,26 @@ function BlobPage() { - - + {/* tree */} + + + - - - - - - {/* @ts-ignore */} - + + + + + + + + + + {/* @ts-ignore */} + + + +
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 ced51ea23..eb3da8efc 100644 --- a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx +++ b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx @@ -89,7 +89,7 @@ function TreeDetailPage() { {/* tree */} - + {