Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 108 additions & 108 deletions apps/gitness/src/components/FileExplorer.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<PathParams>()
const { fullGitRef, fullResourcePath } = useCodePathDetails()
Expand All @@ -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<OpenapiContentInfo[]> => {
try {
const { body: response } = await getContent({
Expand All @@ -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<OpenapiContentInfo[]>(
['folderContents', repoRef, fullGitRef || selectedBranch, folderPath],
() => fetchFolderContents(folderPath),
{
staleTime: 300000,
cacheTime: 900000
}
)
if (!isStaticEntries) {
return useQuery<OpenapiContentInfo[]>(
['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 (
<FileExplorer.FileItem
Expand All @@ -111,7 +151,7 @@ export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps)
content={
<FolderContents
folderPath={itemPath || ''}
isOpen={openFolderPaths.includes(itemPath!)}
isOpen={openFolderPaths.includes(itemPath || '')}
renderEntries={renderEntries}
handleOpenFoldersChange={handleOpenFoldersChange}
openFolderPaths={openFolderPaths}
Expand All @@ -125,41 +165,30 @@ export default function Explorer({ selectedBranch, repoDetails }: ExplorerProps)
})
}

const FolderContents = ({
interface FolderContentsProps {
folderPath: string
isOpen: boolean
renderEntries: (entries: OpenapiContentInfo[], parentPath: string) => ReactNode[]
handleOpenFoldersChange: (newOpenFolderPaths: string[]) => void
openFolderPaths: string[]
}

const FolderContents: FC<FolderContentsProps> = ({
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 <div>Loading...</div>
}

if (error) {
return <div>Error loading folder contents</div>
}

if (!isOpen) return null
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading folder contents</div>
return (
<FileExplorer.Root
onValueChange={value => {
if (typeof value === 'string') {
handleOpenFoldersChange([value])
} else {
handleOpenFoldersChange(value)
}
if (typeof value === 'string') handleOpenFoldersChange([value])
else handleOpenFoldersChange(value)
}}
value={openFolderPaths}
>
Expand All @@ -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 (
<FileExplorer.Root
onValueChange={value => {
if (typeof value === 'string') {
handleOpenFoldersChange([value])
} else {
handleOpenFoldersChange(value)
}
if (typeof value === 'string') handleOpenFoldersChange([value])
else handleOpenFoldersChange(value)
}}
value={openFolderPaths}
>
Expand Down
Loading