From b6d91b9a08a8a922153fe59916c626522b9d53b9 Mon Sep 17 00:00:00 2001 From: Chandini Date: Mon, 29 Sep 2025 15:41:28 +0530 Subject: [PATCH] frontend changes --- src/app/github/repo/RepoTree.tsx | 76 +++++++++++++++ src/app/github/repo/repo-client.tsx | 145 ++++++++++++++++++++++------ src/lib/api/github.ts | 15 +++ 3 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 src/app/github/repo/RepoTree.tsx diff --git a/src/app/github/repo/RepoTree.tsx b/src/app/github/repo/RepoTree.tsx new file mode 100644 index 0000000..0d0a2a1 --- /dev/null +++ b/src/app/github/repo/RepoTree.tsx @@ -0,0 +1,76 @@ +"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>({}) + const [loading, setLoading] = useState>({}) + const [children, setChildren] = useState>({}) + + 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 ( +
+
isDir ? toggle(node.path) : onSelectFile(node.path)}> + {isDir ? ( + isOpen ? : + ) : ( + + )} + {isDir ? : } + {node.name} +
+ {isDir && isOpen && ( +
+ {loading[node.path] &&
Loading…
} + {(children[node.path] || []).map(ch => renderNode(ch))} +
+ )} +
+ ) + } + + return ( +
+ {(children[rootPath] || []).map(n => renderNode(n))} +
+ ) +} diff --git a/src/app/github/repo/repo-client.tsx b/src/app/github/repo/repo-client.tsx index 6006264..cfed557 100644 --- a/src/app/github/repo/repo-client.tsx +++ b/src/app/github/repo/repo-client.tsx @@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { getRepositoryStructure, getRepositoryFileContent, getRepositoryCommitSummary, type RepoStructureEntry, type CommitSummaryResponse } from "@/lib/api/github" import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search, GitCommit } from "lucide-react" +import RepoTree from "./RepoTree" export default function RepoByIdClient({ repositoryId, initialPath = "" }: { repositoryId: string, initialPath?: string }) { const [path, setPath] = useState(initialPath) @@ -15,6 +16,11 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep const [fileQuery, setFileQuery] = useState("") const [readme, setReadme] = useState(null) const [commitSummary, setCommitSummary] = useState(null) + const [selectedFile, setSelectedFile] = useState(null) + const [previewLoading, setPreviewLoading] = useState(false) + const [previewContent, setPreviewContent] = useState(null) + const [selectedDir, setSelectedDir] = useState("") + const [dirEntries, setDirEntries] = useState([]) const timeAgo = (iso?: string) => { if (!iso) return '' @@ -73,9 +79,9 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep return entries.filter(e => e.name.toLowerCase().includes(q)) }, [entries, fileQuery]) - const navigateFolder = (name: string) => { - const next = path ? `${path}/${name}` : name - setPath(next) + const navigateFolder = (rel: string) => { + // rel is expected to be a relative path from repo root + setPath(rel) } const goUp = () => { @@ -130,31 +136,116 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep )} - - - {loading && ( -
Loading...
+ {/* Three-column layout: left tree, middle directory listing, right preview */} +
+
+ { + // Update center panel with this directory contents + setSelectedDir(dirPath) + try { + const res = await getRepositoryStructure(repositoryId, dirPath) + setDirEntries(res?.structure || []) + } catch { + setDirEntries([]) + } + }} + onSelectFile={async (filePath) => { + setSelectedFile(filePath) + try { + setPreviewLoading(true) + const file = await getRepositoryFileContent(repositoryId, filePath) + setPreviewContent(file?.content || null) + } finally { + setPreviewLoading(false) + } + }} + /> +
+ {/* Middle: Directory listing */} +
+
+ Folder + {selectedDir || "/"} +
+
+ {dirEntries.length === 0 && ( +
Select a folder from the tree.
+ )} + {dirEntries.map((e, i) => { + const rawPath = (e as any).path || (e as any).relative_path || '' + const displayName = (e as any).name || (e as any).filename || (rawPath ? rawPath.split('/').slice(-1)[0] : '(unknown)') + const typeVal = (e as any).type || (e as any).entry_type + const isDirHint = typeVal === 'directory' || typeVal === 'dir' || (e as any).is_directory === true || (e as any).isDir === true + // heuristic fallback if type missing: no dot in last segment often means folder + const isDir = isDirHint || (!!displayName && !displayName.includes('.')) + return ( +
{ + const relPath = (e as any).path || (e as any).relative_path || displayName + if (isDir) { + // drill down in tree and center + setSelectedDir(relPath) + try { + const res = await getRepositoryStructure(repositoryId, relPath) + setDirEntries(res?.structure || []) + } catch { + setDirEntries([]) + } + return + } + // file + setSelectedFile(relPath) + try { + setPreviewLoading(true) + let file = await getRepositoryFileContent(repositoryId, relPath) + // if backend still reports directory, probe and navigate instead of showing error + if (!file?.content) { + try { + const probe = await getRepositoryStructure(repositoryId, relPath) + if (Array.isArray(probe?.structure) && probe.structure.length > 0) { + setPreviewContent(null) + setSelectedFile(null) + setSelectedDir(relPath) + setDirEntries(probe.structure) + return + } + } catch {} + } + setPreviewContent(file?.content || null) + } finally { + setPreviewLoading(false) + } + }}> +
+ {isDir ? : } +
+
{displayName}
+
{isDir ? 'folder' : 'file'}
+
+ ) + })} +
+
+ {/* Right: Preview */} +
+
+ Preview + {selectedFile && {selectedFile}} +
+ {previewLoading ? ( +
Loading file...
+ ) : !selectedFile ? ( +
Select a file to preview its contents.
+ ) : previewContent ? ( +
{previewContent}
+ ) : ( +
No preview available (binary or empty file).
)} - {!loading && visible.length === 0 && ( -
No entries found.
- )} - {visible.map((e, i) => { - const displayName = (e as any).name || (e as any).filename || (e as any).relative_path || (e as any).path || '(unknown)' - const isDir = e.type === 'directory' || (e as any).type === 'dir' - return ( -
isDir ? navigateFolder(displayName) : void 0}> -
- {isDir ? : } -
-
{displayName}
-
- -
-
- )})} - - +
+
diff --git a/src/lib/api/github.ts b/src/lib/api/github.ts index 1155752..3772b1f 100644 --- a/src/lib/api/github.ts +++ b/src/lib/api/github.ts @@ -108,6 +108,21 @@ export async function getRepositoryCommits(repositoryId: string, opts?: { page?: return res.data?.data as CommitsListResponse } +export interface ResolvePathInfo { + repository_id: string + local_path: string + requested_file_path: string + resolved_absolute_path: string | null + exists: boolean + is_directory: boolean +} + +export async function resolveRepositoryPath(repositoryId: string, filePath: string): Promise { + const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/resolve-path?file_path=${encodeURIComponent(filePath)}` + const res = await authApiClient.get(url) + return res.data?.data as ResolvePathInfo +} + export interface AttachRepositoryResponse { success: boolean message?: string