From d587c94f50833b5e4dc2792d48ae47d23f823b06 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:19:30 +0800 Subject: [PATCH] feat: wire file explorer to real backend API Co-Authored-By: Claude --- web/src/components/layout/WorkspaceShell.tsx | 26 ++- .../workspace/FileExplorerPanel.tsx | 206 +++++++++++++----- web/src/lib/api/files.ts | 64 ++++++ web/src/lib/store/file-tree-store.ts | 24 ++ web/src/lib/store/workspace-ui-store.ts | 2 +- web/src/pages/workspace/WorkspacePage.tsx | 25 ++- web/src/pages/workspace/mock-data.ts | 74 +------ 7 files changed, 282 insertions(+), 139 deletions(-) create mode 100644 web/src/lib/api/files.ts create mode 100644 web/src/lib/store/file-tree-store.ts diff --git a/web/src/components/layout/WorkspaceShell.tsx b/web/src/components/layout/WorkspaceShell.tsx index 160f92e..fdc8062 100644 --- a/web/src/components/layout/WorkspaceShell.tsx +++ b/web/src/components/layout/WorkspaceShell.tsx @@ -8,23 +8,33 @@ import { TerminalPanel } from "@/components/terminal/TerminalPanel"; import { FileExplorerPanel } from "@/components/workspace/FileExplorerPanel"; import { StatusBar } from "@/components/workspace/StatusBar"; import { WorkspaceSelector } from "@/components/workspace/WorkspaceSelector"; +import { inferLanguage } from "@/lib/api/files"; import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store"; -import { - findFileByPath, - type MockFileNode, -} from "@/pages/workspace/mock-data"; +import type { MockFileNode } from "@/pages/workspace/mock-data"; interface WorkspaceShellProps { - files: MockFileNode[]; + workspaceId: string; } -export function WorkspaceShell({ files }: WorkspaceShellProps) { +function fileNodeFromPath(path: string): MockFileNode { + const name = path.split("/").pop() ?? path; + return { + name, + path, + kind: "file", + language: inferLanguage(name), + }; +} + +export function WorkspaceShell({ workspaceId }: WorkspaceShellProps) { const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath); const sidebarCollapsed = useWorkspaceUiStore((s) => s.sidebarCollapsed); const terminalVisible = useWorkspaceUiStore((s) => s.terminalVisible); const toggleSidebar = useWorkspaceUiStore((s) => s.toggleSidebar); const toggleTerminal = useWorkspaceUiStore((s) => s.toggleTerminal); - const activeFile = findFileByPath(files, activeFilePath); + const activeFile = activeFilePath + ? fileNodeFromPath(activeFilePath) + : undefined; return (
@@ -65,7 +75,7 @@ export function WorkspaceShell({ files }: WorkspaceShellProps) { {!sidebarCollapsed && ( <> - + diff --git a/web/src/components/workspace/FileExplorerPanel.tsx b/web/src/components/workspace/FileExplorerPanel.tsx index 809a0c2..d5fcea6 100644 --- a/web/src/components/workspace/FileExplorerPanel.tsx +++ b/web/src/components/workspace/FileExplorerPanel.tsx @@ -1,78 +1,155 @@ import { ChevronRight, FileCode2, Folder } from "lucide-react"; +import type { ReactNode } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { useFileList, type FileEntry } from "@/lib/api/files"; +import { useFileTreeStore } from "@/lib/store/file-tree-store"; import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store"; import { cn } from "@/lib/utils"; -import type { MockFileNode } from "@/pages/workspace/mock-data"; interface FileExplorerPanelProps { - files: MockFileNode[]; + workspaceId: string | null; } -interface FileTreeNodeProps { - node: MockFileNode; +interface FileTreeDirectoryProps { + workspaceId: string; + entry: FileEntry; depth: number; - activeFilePath: string; onSelectFile: (path: string) => void; } -function FileTreeNode({ - node, - depth, - activeFilePath, - onSelectFile, -}: FileTreeNodeProps) { - const isFile = node.kind === "file"; - const isActive = isFile && node.path === activeFilePath; +interface FileTreeFileProps { + entry: FileEntry; + depth: number; + onSelectFile: (path: string) => void; +} + +function FileTreeFile({ entry, depth, onSelectFile }: FileTreeFileProps) { + const selectedPath = useFileTreeStore((s) => s.selectedPath); + const isSelected = selectedPath === entry.path; const paddingLeft = `${depth * 0.875 + 0.75}rem`; return (
  • - {node.children?.length ? ( -
      - {node.children.map((child) => ( - - ))} -
    - ) : null}
  • ); } -export function FileExplorerPanel({ files }: FileExplorerPanelProps) { - const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath); +function FileTreeDirectory({ + workspaceId, + entry, + depth, + onSelectFile, +}: FileTreeDirectoryProps) { + const expanded = useFileTreeStore((s) => s.expandedPaths.has(entry.path)); + const toggleExpanded = useFileTreeStore((s) => s.toggleExpanded); + const { data, isLoading, error } = useFileList( + workspaceId, + entry.path, + expanded, + ); + const paddingLeft = `${depth * 0.875 + 0.75}rem`; + + return ( +
  • + + {expanded && ( +
      + {isLoading && ( +
    • + Loading… +
    • + )} + {error && ( +
    • + {error.message} +
    • + )} + {data?.map((child) => + child.isDir ? ( + + ) : ( + + ), + )} +
    + )} +
  • + ); +} + +function EmptyState({ children }: { children: ReactNode }) { + return ( +
    + {children} +
    + ); +} + +export function FileExplorerPanel({ workspaceId }: FileExplorerPanelProps) { const setActiveFilePath = useWorkspaceUiStore((s) => s.setActiveFilePath); + const setSelectedPath = useFileTreeStore((s) => s.setSelectedPath); + const { data, isLoading, error } = useFileList(workspaceId, "."); + + const onSelectFile = (path: string) => { + setSelectedPath(path); + setActiveFilePath(path); + }; + + if (!workspaceId) { + return ( + + ); + } return (