77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { getRepositoryStructure, type RepoStructureEntry } from "@/lib/api/github"
|
|
import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react"
|
|
|
|
type Node = {
|
|
name: string
|
|
path: string
|
|
type: 'file' | 'directory'
|
|
}
|
|
|
|
export default function RepoTree({ repositoryId, rootPath = "", onSelectFile, onSelectDirectory }: { repositoryId: string, rootPath?: string, onSelectFile: (path: string) => void, onSelectDirectory?: (path: string) => void }) {
|
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
|
const [loading, setLoading] = useState<Record<string, boolean>>({})
|
|
const [children, setChildren] = useState<Record<string, Node[]>>({})
|
|
|
|
const toggle = async (nodePath: string) => {
|
|
const next = !expanded[nodePath]
|
|
setExpanded(prev => ({ ...prev, [nodePath]: next }))
|
|
if (next && !children[nodePath]) {
|
|
setLoading(prev => ({ ...prev, [nodePath]: true }))
|
|
try {
|
|
const res = await getRepositoryStructure(repositoryId, nodePath)
|
|
const list = (res?.structure || []) as RepoStructureEntry[]
|
|
const mapped: Node[] = list.map(e => ({
|
|
name: (e as any).name || (e as any).filename || (((e as any).path || (e as any).relative_path || '').split('/').slice(-1)[0]) || 'unknown',
|
|
path: (e as any).path || (e as any).relative_path || '',
|
|
type: (e as any).type === 'directory' || (e as any).type === 'dir' ? 'directory' : 'file'
|
|
}))
|
|
setChildren(prev => ({ ...prev, [nodePath]: mapped }))
|
|
onSelectDirectory && onSelectDirectory(nodePath)
|
|
} finally {
|
|
setLoading(prev => ({ ...prev, [nodePath]: false }))
|
|
}
|
|
}
|
|
}
|
|
|
|
// initial load for root
|
|
useEffect(() => {
|
|
toggle(rootPath)
|
|
onSelectDirectory && onSelectDirectory(rootPath)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [repositoryId, rootPath])
|
|
|
|
const renderNode = (node: Node) => {
|
|
const isDir = node.type === 'directory'
|
|
const isOpen = !!expanded[node.path]
|
|
return (
|
|
<div key={node.path} className="select-none">
|
|
<div className="flex items-center gap-1 py-1 px-1 hover:bg-white/5 rounded cursor-pointer"
|
|
onClick={() => isDir ? toggle(node.path) : onSelectFile(node.path)}>
|
|
{isDir ? (
|
|
isOpen ? <ChevronDown className="h-4 w-4 text-white/70"/> : <ChevronRight className="h-4 w-4 text-white/70"/>
|
|
) : (
|
|
<span className="w-4"/>
|
|
)}
|
|
{isDir ? <Folder className="h-4 w-4 mr-1"/> : <FileText className="h-4 w-4 mr-1"/>}
|
|
<span className="truncate text-sm text-white">{node.name}</span>
|
|
</div>
|
|
{isDir && isOpen && (
|
|
<div className="pl-5 border-l border-white/10 ml-2">
|
|
{loading[node.path] && <div className="text-xs text-white/60 py-1">Loading…</div>}
|
|
{(children[node.path] || []).map(ch => renderNode(ch))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="text-white/90">
|
|
{(children[rootPath] || []).map(n => renderNode(n))}
|
|
</div>
|
|
)
|
|
}
|