diff --git a/apps/gitness/src/components/FileExplorer.tsx b/apps/gitness/src/components/FileExplorer.tsx index be4d17078e..cb262bc319 100644 --- a/apps/gitness/src/components/FileExplorer.tsx +++ b/apps/gitness/src/components/FileExplorer.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { FC, ReactNode, useEffect } from 'react' import { useLocation, useParams } from 'react-router-dom' import { useQuery, useQueryClient } from '@tanstack/react-query' @@ -13,26 +13,50 @@ import useCodePathDetails from '../hooks/useCodePathDetails' import { PathParams } from '../RouteDefinitions' import { normalizeGitRef } from '../utils/git-utils' +/** + * ExplorerProps: + * - In API mode (default), repoDetails is used (and data is fetched on demand). + * - In Static mode, the parent passes a complete tree via the "entries" prop and sets isStaticEntries={true}. + */ interface ExplorerProps { selectedBranch?: string - repoDetails: OpenapiGetContentOutput + repoDetails?: OpenapiGetContentOutput + entries?: ExplorerStaticContentInfo[] // static enteries + isStaticEntries?: boolean +} + +export interface ExplorerStaticContentInfo extends OpenapiContentInfo { + entries?: ExplorerStaticContentInfo[] } const sortEntriesByType = (entries: OpenapiContentInfo[]): OpenapiContentInfo[] => { return entries.sort((a, b) => { - if (a.type === 'dir' && b.type === 'file') { - return -1 - } else if (a.type === 'file' && b.type === 'dir') { - return 1 - } + if (a.type === 'dir' && b.type === 'file') return -1 + if (a.type === 'file' && b.type === 'dir') return 1 return 0 }) } /** - * TODO: This code was migrated from V2 and needs to be refactored. + * Helper for static mode: Given a complete tree and a folder path, + * return the entries inside that folder. */ -export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps) { +const getStaticFolderEntries = ( + entries: ExplorerStaticContentInfo[], + folderPath: string +): OpenapiContentInfo[] | undefined => { + if (!folderPath) return entries + const parts = folderPath.split('/') + let currentEntries = entries + for (const part of parts) { + const found = currentEntries.find(entry => entry.name === part && entry.type === 'dir') + if (!found) return undefined + currentEntries = found.entries || [] + } + return currentEntries +} + +export default function Explorer({ selectedBranch, repoDetails, entries, isStaticEntries = false }: ExplorerProps) { const repoRef = useGetRepoRef() const { spaceId, repoId } = useParams() const { fullGitRef, fullResourcePath } = useCodePathDetails() @@ -42,24 +66,24 @@ export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps) const { openFolderPaths, setOpenFolderPaths } = useOpenFolderPaths() const routes = useRoutes() + console.log('root enteries,', repoDetails?.content?.entries) + + // Updates the open folder state and, if not staticEnteries, prefetches new folder contents. const handleOpenFoldersChange = (newOpenFolderPaths: string[]) => { const newlyOpenedFolders = newOpenFolderPaths.filter(path => !openFolderPaths.includes(path)) - - // contents for newly opened folders - newlyOpenedFolders.forEach(folderPath => { - queryClient.prefetchQuery( - ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], - () => fetchFolderContents(folderPath), - { - staleTime: 300000, - cacheTime: 900000 - } - ) - }) - + if (!isStaticEntries) { + newlyOpenedFolders.forEach(folderPath => { + queryClient.prefetchQuery( + ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], + () => fetchFolderContents(folderPath), + { staleTime: 300000, cacheTime: 900000 } + ) + }) + } setOpenFolderPaths(newOpenFolderPaths) } + // Data fetching / Static Entries lookup const fetchFolderContents = async (folderPath: string): Promise => { try { const { body: response } = await getContent({ @@ -74,23 +98,39 @@ export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps) } } + // Unified hook: in static mode: perform a lookup, otherwise use React Query; const useFolderContents = (folderPath: string) => { - return useQuery( - ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], - () => fetchFolderContents(folderPath), - { - staleTime: 300000, - cacheTime: 900000 - } - ) + if (!isStaticEntries) { + return useQuery( + ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], + () => fetchFolderContents(folderPath), + { staleTime: 300000, cacheTime: 900000 } + ) + } else { + const staticData = entries ? getStaticFolderEntries(entries, folderPath) : undefined + return { data: staticData, isLoading: false, error: undefined } + } } - const renderEntries = (entries: OpenapiContentInfo[], parentPath: string = '') => { - const sortedEntries = sortEntriesByType(entries) - return sortedEntries.map((item, idx) => { + // Root entries: either fetched from API or provided statically. + const { + data: rootEntries, + isLoading: isRootLoading, + error: rootError + } = !isStaticEntries + ? useQuery(['folderContents', repoRef, fullGitRef || selectedBranch, ''], () => fetchFolderContents(''), { + staleTime: 300000, + cacheTime: 900000, + initialData: repoDetails?.content?.entries + }) + : { data: entries, isLoading: false, error: undefined } + + // Recursively renders a list of folder/file entries. + const renderEntries = (entries: OpenapiContentInfo[], parentPath: string = ''): ReactNode[] => { + const sorted = sortEntriesByType(entries) + return sorted.map((item, idx) => { const itemPath = parentPath ? `${parentPath}/${item.name}` : item.name const fullPath = `${routes.toRepoFiles({ spaceId, repoId })}/${fullGitRef || selectedBranch}/~/${itemPath}` - if (item.type === 'file') { return ( ReactNode[] + handleOpenFoldersChange: (newOpenFolderPaths: string[]) => void + openFolderPaths: string[] + } + + const FolderContents: FC = ({ folderPath, isOpen, renderEntries, handleOpenFoldersChange, openFolderPaths - }: { - folderPath: string - isOpen: boolean - renderEntries: (entries: OpenapiContentInfo[], parentPath: string) => React.ReactNode[] - handleOpenFoldersChange: (newOpenFolderPaths: string[]) => void - openFolderPaths: string[] }) => { const { data: contents, isLoading, error } = useFolderContents(folderPath) - - if (!isOpen) { - return null - } - - if (isLoading) { - return
Loading...
- } - - if (error) { - return
Error loading folder contents
- } - + if (!isOpen) return null + if (isLoading) return
Loading...
+ if (error) return
Error loading folder contents
return ( { - if (typeof value === 'string') { - handleOpenFoldersChange([value]) - } else { - handleOpenFoldersChange(value) - } + if (typeof value === 'string') handleOpenFoldersChange([value]) + else handleOpenFoldersChange(value) }} value={openFolderPaths} > @@ -168,68 +197,39 @@ export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps) ) } + // Automatically expand folders along the current fullResourcePath ie the current open folder/file useEffect(() => { - // Automatically expand folders along the fullResourcePath const expandFoldersAlongPath = async () => { if (fullResourcePath) { - const pathSegments = fullResourcePath.split('/') - const isFile = - pathSegments[pathSegments.length - 1].includes('.') && !pathSegments[pathSegments.length - 1].startsWith('.') - const folderSegments = isFile ? pathSegments.slice(0, -1) : pathSegments - + const segments = fullResourcePath.split('/') + const isFile = segments[segments.length - 1].includes('.') && !segments[segments.length - 1].startsWith('.') + const folderSegments = isFile ? segments.slice(0, -1) : segments const folderPaths: string[] = [] - let currentPath = '' + let current = '' folderSegments.forEach(segment => { - currentPath = currentPath ? `${currentPath}/${segment}` : segment - folderPaths.push(currentPath) - }) - - // Update openFolderPaths - setOpenFolderPaths(prevOpenFolderPaths => { - const newOpenFolderPaths = [...prevOpenFolderPaths] - folderPaths.forEach(folderPath => { - if (!newOpenFolderPaths.includes(folderPath)) { - newOpenFolderPaths.push(folderPath) - } - }) - return newOpenFolderPaths + current = current ? `${current}/${segment}` : segment + folderPaths.push(current) }) - - // Prefetch contents for folders along the path - for (const folderPath of folderPaths) { - queryClient.prefetchQuery( - ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], - () => fetchFolderContents(folderPath), - { - staleTime: 300000, - cacheTime: 900000 - } - ) + setOpenFolderPaths(prev => Array.from(new Set([...prev, ...folderPaths]))) + if (!isStaticEntries) { + for (const folderPath of folderPaths) { + queryClient.prefetchQuery( + ['folderContents', repoRef, fullGitRef || selectedBranch, folderPath], + () => fetchFolderContents(folderPath), + { staleTime: 300000, cacheTime: 900000 } + ) + } } } } expandFoldersAlongPath() - }, [fullResourcePath]) - - // Fetch root contents - const { - data: rootEntries, - isLoading: isRootLoading, - error: rootError - } = useQuery(['folderContents', repoRef, fullGitRef || selectedBranch, ''], () => fetchFolderContents(''), { - staleTime: 300000, - cacheTime: 900000, - initialData: repoDetails?.content?.entries - }) + }, [fullResourcePath, isStaticEntries, queryClient, repoRef, fullGitRef, selectedBranch, setOpenFolderPaths]) return ( { - if (typeof value === 'string') { - handleOpenFoldersChange([value]) - } else { - handleOpenFoldersChange(value) - } + if (typeof value === 'string') handleOpenFoldersChange([value]) + else handleOpenFoldersChange(value) }} value={openFolderPaths} >