mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 15:07:38 +00:00
feat: Advanced file editor and file tree improvements (#444)
# Features - File drag and drop upload: Support uploading files and folders via drag and drop - Binary file handling: Detect binary files and display a friendly message instead of trying to edit them - Folder download: Download folders as ZIP files (using JSZip library) - Context menu integration: Full right-click context menu for file operations (rename, delete, copy path, download, new file/folder)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import { ChevronRight, Folder, FolderOpen } from 'lucide-react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
import FileContextMenu from '../../FileContextMenu';
|
||||
import { Input } from '../../ui/input';
|
||||
import type { FileTreeNode as FileTreeNodeType, FileTreeViewMode } from '../types/types';
|
||||
|
||||
type FileTreeNodeProps = {
|
||||
@@ -12,6 +14,21 @@ type FileTreeNodeProps = {
|
||||
renderFileIcon: (filename: string) => ReactNode;
|
||||
formatFileSize: (bytes?: number) => string;
|
||||
formatRelativeTime: (date?: string) => string;
|
||||
onRename?: (item: FileTreeNodeType) => void;
|
||||
onDelete?: (item: FileTreeNodeType) => void;
|
||||
onNewFile?: (path: string) => void;
|
||||
onNewFolder?: (path: string) => void;
|
||||
onCopyPath?: (item: FileTreeNodeType) => void;
|
||||
onDownload?: (item: FileTreeNodeType) => void;
|
||||
onRefresh?: () => void;
|
||||
// Rename state for inline editing
|
||||
renamingItem?: FileTreeNodeType | null;
|
||||
renameValue?: string;
|
||||
setRenameValue?: (value: string) => void;
|
||||
handleConfirmRename?: () => void;
|
||||
handleCancelRename?: () => void;
|
||||
renameInputRef?: RefObject<HTMLInputElement>;
|
||||
operationLoading?: boolean;
|
||||
};
|
||||
|
||||
type TreeItemIconProps = {
|
||||
@@ -51,10 +68,25 @@ export default function FileTreeNode({
|
||||
renderFileIcon,
|
||||
formatFileSize,
|
||||
formatRelativeTime,
|
||||
onRename,
|
||||
onDelete,
|
||||
onNewFile,
|
||||
onNewFolder,
|
||||
onCopyPath,
|
||||
onDownload,
|
||||
onRefresh,
|
||||
renamingItem,
|
||||
renameValue,
|
||||
setRenameValue,
|
||||
handleConfirmRename,
|
||||
handleCancelRename,
|
||||
renameInputRef,
|
||||
operationLoading,
|
||||
}: FileTreeNodeProps) {
|
||||
const isDirectory = item.type === 'directory';
|
||||
const isOpen = isDirectory && expandedDirs.has(item.path);
|
||||
const hasChildren = Boolean(isDirectory && item.children && item.children.length > 0);
|
||||
const isRenaming = renamingItem?.path === item.path;
|
||||
|
||||
const nameClassName = cn(
|
||||
'text-[13px] leading-tight truncate',
|
||||
@@ -72,47 +104,100 @@ export default function FileTreeNode({
|
||||
(isDirectory && !isOpen) || !isDirectory ? 'border-l-2 border-transparent' : '',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="select-none">
|
||||
// Render rename input if this item is being renamed
|
||||
if (isRenaming && setRenameValue && handleConfirmRename && handleCancelRename) {
|
||||
return (
|
||||
<div
|
||||
className={rowClassName}
|
||||
className={cn(rowClassName, 'bg-accent/30')}
|
||||
style={{ paddingLeft: `${level * 16 + 4}px` }}
|
||||
onClick={() => onItemClick(item)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{viewMode === 'detailed' ? (
|
||||
<>
|
||||
<div className="col-span-5 flex items-center gap-1.5 min-w-0">
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<span className={nameClassName}>{item.name}</span>
|
||||
</div>
|
||||
<div className="col-span-2 text-sm text-muted-foreground tabular-nums">
|
||||
{item.type === 'file' ? formatFileSize(item.size) : ''}
|
||||
</div>
|
||||
<div className="col-span-3 text-sm text-muted-foreground">{formatRelativeTime(item.modified)}</div>
|
||||
<div className="col-span-2 text-sm text-muted-foreground font-mono">{item.permissionsRwx || ''}</div>
|
||||
</>
|
||||
) : viewMode === 'compact' ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<span className={nameClassName}>{item.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-shrink-0 ml-2">
|
||||
{item.type === 'file' && (
|
||||
<>
|
||||
<span className="tabular-nums">{formatFileSize(item.size)}</span>
|
||||
<span className="font-mono">{item.permissionsRwx}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<Input
|
||||
ref={renameInputRef}
|
||||
type="text"
|
||||
value={renameValue || ''}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') handleConfirmRename();
|
||||
if (e.key === 'Escape') handleCancelRename();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
handleConfirmRename();
|
||||
}, 100);
|
||||
}}
|
||||
className="h-6 text-sm flex-1"
|
||||
disabled={operationLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowContent = (
|
||||
<div
|
||||
className={rowClassName}
|
||||
style={{ paddingLeft: `${level * 16 + 4}px` }}
|
||||
onClick={() => onItemClick(item)}
|
||||
>
|
||||
{viewMode === 'detailed' ? (
|
||||
<>
|
||||
<div className="col-span-5 flex items-center gap-1.5 min-w-0">
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<span className={nameClassName}>{item.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-sm text-muted-foreground tabular-nums">
|
||||
{item.type === 'file' ? formatFileSize(item.size) : ''}
|
||||
</div>
|
||||
<div className="col-span-3 text-sm text-muted-foreground">{formatRelativeTime(item.modified)}</div>
|
||||
<div className="col-span-2 text-sm text-muted-foreground font-mono">{item.permissionsRwx || ''}</div>
|
||||
</>
|
||||
) : viewMode === 'compact' ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<span className={nameClassName}>{item.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-shrink-0 ml-2">
|
||||
{item.type === 'file' && (
|
||||
<>
|
||||
<span className="tabular-nums">{formatFileSize(item.size)}</span>
|
||||
<span className="font-mono">{item.permissionsRwx}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
|
||||
<span className={nameClassName}>{item.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Check if context menu callbacks are provided
|
||||
const hasContextMenu = onRename || onDelete || onNewFile || onNewFolder || onCopyPath || onDownload || onRefresh;
|
||||
|
||||
return (
|
||||
<div className="select-none">
|
||||
{hasContextMenu ? (
|
||||
<FileContextMenu
|
||||
item={item}
|
||||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onNewFile={onNewFile}
|
||||
onNewFolder={onNewFolder}
|
||||
onCopyPath={onCopyPath}
|
||||
onDownload={onDownload}
|
||||
onRefresh={onRefresh}
|
||||
>
|
||||
{rowContent}
|
||||
</FileContextMenu>
|
||||
) : (
|
||||
rowContent
|
||||
)}
|
||||
|
||||
{isDirectory && isOpen && hasChildren && (
|
||||
<div className="relative">
|
||||
@@ -132,6 +217,20 @@ export default function FileTreeNode({
|
||||
renderFileIcon={renderFileIcon}
|
||||
formatFileSize={formatFileSize}
|
||||
formatRelativeTime={formatRelativeTime}
|
||||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onNewFile={onNewFile}
|
||||
onNewFolder={onNewFolder}
|
||||
onCopyPath={onCopyPath}
|
||||
onDownload={onDownload}
|
||||
onRefresh={onRefresh}
|
||||
renamingItem={renamingItem}
|
||||
renameValue={renameValue}
|
||||
setRenameValue={setRenameValue}
|
||||
handleConfirmRename={handleConfirmRename}
|
||||
handleCancelRename={handleCancelRename}
|
||||
renameInputRef={renameInputRef}
|
||||
operationLoading={operationLoading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user