diff --git a/apps/client/app/components/file-tree.tsx b/apps/client/app/components/file-tree.tsx index e84f2849..8e5df05a 100644 --- a/apps/client/app/components/file-tree.tsx +++ b/apps/client/app/components/file-tree.tsx @@ -9,7 +9,7 @@ */ import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2 } from "lucide-react"; -import { memo, type ReactNode, useEffect, useMemo, useState } from "react"; +import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { cn } from "~/lib/utils"; const NODE_PADDING_LEFT = 12; @@ -77,20 +77,23 @@ export const FileTree = memo((props: Props) => { return list; }, [fileList, collapsedFolders]); - const toggleCollapseState = (fullPath: string) => { - setCollapsedFolders((prevSet) => { - const newSet = new Set(prevSet); + const toggleCollapseState = useCallback( + (fullPath: string) => { + setCollapsedFolders((prevSet) => { + const newSet = new Set(prevSet); - if (newSet.has(fullPath)) { - newSet.delete(fullPath); - onFolderExpand?.(fullPath); - } else { - newSet.add(fullPath); - } + if (newSet.has(fullPath)) { + newSet.delete(fullPath); + onFolderExpand?.(fullPath); + } else { + newSet.add(fullPath); + } - return newSet; - }); - }; + return newSet; + }); + }, + [onFolderExpand], + ); // Add new folders to collapsed set when file list changes useEffect(() => { @@ -105,6 +108,13 @@ export const FileTree = memo((props: Props) => { }); }, [fileList, expandedFolders]); + const handleFileSelect = useCallback( + (filePath: string) => { + onFileSelect?.(filePath); + }, + [onFileSelect], + ); + return (
{filteredFileList.map((fileOrFolder) => { @@ -115,9 +125,7 @@ export const FileTree = memo((props: Props) => { key={fileOrFolder.id} selected={selectedFile === fileOrFolder.fullPath} file={fileOrFolder} - onClick={() => { - onFileSelect?.(fileOrFolder.fullPath); - }} + onFileSelect={handleFileSelect} /> ); } @@ -128,9 +136,7 @@ export const FileTree = memo((props: Props) => { folder={fileOrFolder} collapsed={collapsedFolders.has(fileOrFolder.fullPath)} loading={loadingFolders.has(fileOrFolder.fullPath)} - onClick={() => { - toggleCollapseState(fileOrFolder.fullPath); - }} + onToggle={toggleCollapseState} /> ); } @@ -147,12 +153,17 @@ interface FolderProps { folder: FolderNode; collapsed: boolean; loading?: boolean; - onClick: () => void; + onToggle: (fullPath: string) => void; } -function Folder({ folder: { depth, name }, collapsed, loading, onClick }: FolderProps) { +const Folder = memo(({ folder, collapsed, loading, onToggle }: FolderProps) => { + const { depth, name, fullPath } = folder; const FolderIconComponent = collapsed ? FolderIcon : FolderOpen; + const handleClick = useCallback(() => { + onToggle(fullPath); + }, [onToggle, fullPath]); + return ( ) } - onClick={onClick} + onClick={handleClick} > {name} ); -} +}); interface FileProps { file: FileNode; selected: boolean; - onClick: () => void; + onFileSelect: (filePath: string) => void; } -function File({ file: { depth, name }, onClick, selected }: FileProps) { +const File = memo(({ file, onFileSelect, selected }: FileProps) => { + const { depth, name, fullPath } = file; + + const handleClick = useCallback(() => { + onFileSelect(fullPath); + }, [onFileSelect, fullPath]); + return ( } - onClick={onClick} + onClick={handleClick} > {name} ); -} +}); interface ButtonProps { depth: number; @@ -204,19 +221,21 @@ interface ButtonProps { onClick?: () => void; } -function NodeButton({ depth, icon, onClick, className, children }: ButtonProps) { +const NodeButton = memo(({ depth, icon, onClick, className, children }: ButtonProps) => { + const paddingLeft = useMemo(() => `${8 + depth * NODE_PADDING_LEFT}px`, [depth]); + return ( ); -} +}); type Node = FileNode | FolderNode; diff --git a/apps/client/app/modules/details/tabs/files.tsx b/apps/client/app/modules/details/tabs/files.tsx index 32a33705..559fab40 100644 --- a/apps/client/app/modules/details/tabs/files.tsx +++ b/apps/client/app/modules/details/tabs/files.tsx @@ -1,9 +1,11 @@ import { useQuery } from "@tanstack/react-query"; import { FolderOpen } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; +import { listFilesOptions } from "~/api-client/@tanstack/react-query.gen"; import { listFiles } from "~/api-client/sdk.gen"; import { FileTree } from "~/components/file-tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; +import { parseError } from "~/lib/errors"; import type { Volume } from "~/lib/types"; type Props = { @@ -26,19 +28,11 @@ export const FilesTabContent = ({ volume }: Props) => { // Fetch root level files const { data, isLoading, error } = useQuery({ - queryKey: ["volume-files", volume.name, "/"], - queryFn: async () => { - const result = await listFiles({ - path: { name: volume.name }, - throwOnError: true, - }); - return result.data; - }, + ...listFilesOptions({ path: { name: volume.name } }), enabled: volume.status === "mounted", refetchInterval: 10000, }); - // Update allFiles when root data changes useMemo(() => { if (data?.files) { setAllFiles((prev) => { @@ -59,7 +53,6 @@ export const FilesTabContent = ({ volume }: Props) => { return next; }); - // If we haven't fetched this folder yet, fetch it if (!fetchedFolders.has(folderPath)) { setLoadingFolders((prev) => new Set(prev).add(folderPath)); @@ -123,7 +116,7 @@ export const FilesTabContent = ({ volume }: Props) => { )} {error && (
-

Failed to load files: {String(error)}

+

Failed to load files: {error.message}

)} {!isLoading && !error && (