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 && (