mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 15:07:38 +00:00
refactor(FileTree): make file tree context menu a typescript component and move it inside the file tree view
This commit is contained in:
@@ -1,312 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FileText,
|
||||
FolderPlus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Copy,
|
||||
Download,
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
/**
|
||||
* FileContextMenu Component
|
||||
* Right-click context menu for file/directory operations
|
||||
*/
|
||||
const FileContextMenu = ({
|
||||
children,
|
||||
item,
|
||||
onRename,
|
||||
onDelete,
|
||||
onNewFile,
|
||||
onNewFolder,
|
||||
onRefresh,
|
||||
onCopyPath,
|
||||
onDownload,
|
||||
isLoading = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const menuRef = useRef(null);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const isDirectory = item?.type === 'directory';
|
||||
const isFile = item?.type === 'file';
|
||||
const isBackground = !item; // Clicked on empty space
|
||||
|
||||
// Handle right-click
|
||||
const handleContextMenu = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// Adjust position if menu would go off screen
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 300;
|
||||
|
||||
let adjustedX = x;
|
||||
let adjustedY = y;
|
||||
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
adjustedX = window.innerWidth - menuWidth - 10;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
adjustedY = window.innerHeight - menuHeight - 10;
|
||||
}
|
||||
|
||||
setPosition({ x: adjustedX, y: adjustedY });
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
// Close menu
|
||||
const closeMenu = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isOpen, closeMenu]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const menuItems = menuRef.current?.querySelectorAll('[role="menuitem"]');
|
||||
if (!menuItems || menuItems.length === 0) return;
|
||||
|
||||
const currentIndex = Array.from(menuItems).findIndex(
|
||||
(item) => item === document.activeElement
|
||||
);
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
const nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
|
||||
menuItems[nextIndex]?.focus();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
const prevIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
|
||||
menuItems[prevIndex]?.focus();
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (document.activeElement?.hasAttribute('role', 'menuitem')) {
|
||||
e.preventDefault();
|
||||
document.activeElement.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen]);
|
||||
|
||||
// Handle action click
|
||||
const handleAction = (action, ...args) => {
|
||||
closeMenu();
|
||||
action?.(...args);
|
||||
};
|
||||
|
||||
// Menu item component
|
||||
const MenuItem = ({ icon: Icon, label, onClick, danger = false, disabled = false, shortcut }) => (
|
||||
<button
|
||||
role="menuitem"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled || isLoading}
|
||||
onClick={() => handleAction(onClick)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-sm text-left rounded-md transition-colors',
|
||||
'focus:outline-none focus:bg-accent',
|
||||
disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: danger
|
||||
? 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950'
|
||||
: 'hover:bg-accent',
|
||||
isLoading && 'pointer-events-none'
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
|
||||
<span className="flex-1">{label}</span>
|
||||
{shortcut && (
|
||||
<span className="text-xs text-muted-foreground font-mono">{shortcut}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
// Menu divider
|
||||
const MenuDivider = () => (
|
||||
<div className="h-px bg-border my-1 mx-2" />
|
||||
);
|
||||
|
||||
// Build menu items based on context
|
||||
const renderMenuItems = () => {
|
||||
if (isFile) {
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={Pencil}
|
||||
label={t('fileTree.context.rename', 'Rename')}
|
||||
onClick={() => onRename?.(item)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
label={t('fileTree.context.delete', 'Delete')}
|
||||
onClick={() => onDelete?.(item)}
|
||||
danger
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={Copy}
|
||||
label={t('fileTree.context.copyPath', 'Copy Path')}
|
||||
onClick={() => onCopyPath?.(item)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Download}
|
||||
label={t('fileTree.context.download', 'Download')}
|
||||
onClick={() => onDownload?.(item)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDirectory) {
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label={t('fileTree.context.newFile', 'New File')}
|
||||
onClick={() => onNewFile?.(item.path)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={FolderPlus}
|
||||
label={t('fileTree.context.newFolder', 'New Folder')}
|
||||
onClick={() => onNewFolder?.(item.path)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={Pencil}
|
||||
label={t('fileTree.context.rename', 'Rename')}
|
||||
onClick={() => onRename?.(item)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
label={t('fileTree.context.delete', 'Delete')}
|
||||
onClick={() => onDelete?.(item)}
|
||||
danger
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={Copy}
|
||||
label={t('fileTree.context.copyPath', 'Copy Path')}
|
||||
onClick={() => onCopyPath?.(item)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Download}
|
||||
label={t('fileTree.context.download', 'Download')}
|
||||
onClick={() => onDownload?.(item)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Background context (empty space)
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label={t('fileTree.context.newFile', 'New File')}
|
||||
onClick={() => onNewFile?.('')}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={FolderPlus}
|
||||
label={t('fileTree.context.newFolder', 'New Folder')}
|
||||
onClick={() => onNewFolder?.('')}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={RefreshCw}
|
||||
label={t('fileTree.context.refresh', 'Refresh')}
|
||||
onClick={onRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Trigger element */}
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={cn('contents', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Context menu portal */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
role="menu"
|
||||
aria-label={t('fileTree.context.menuLabel', 'File context menu')}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
zIndex: 9999
|
||||
}}
|
||||
className={cn(
|
||||
'min-w-[180px] py-1 px-1',
|
||||
'bg-popover border border-border rounded-lg shadow-lg',
|
||||
'animate-in fade-in-0 zoom-in-95',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
{t('fileTree.context.loading', 'Loading...')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
renderMenuItems()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileContextMenu;
|
||||
312
src/components/file-tree/view/FileContextMenu.tsx
Normal file
312
src/components/file-tree/view/FileContextMenu.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Copy, Download, FileText, FolderPlus, Pencil, RefreshCw, Trash2, type LucideIcon } from 'lucide-react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type FileContextItem = {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
path: string;
|
||||
size?: number;
|
||||
modified?: string;
|
||||
permissionsRwx?: string;
|
||||
children?: FileContextItem[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type ContextMenuAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
onSelect?: () => void;
|
||||
isDanger?: boolean;
|
||||
isDisabled?: boolean;
|
||||
shortcut?: string;
|
||||
showDividerBefore?: boolean;
|
||||
};
|
||||
|
||||
const CONTEXT_MENU_WIDTH = 200;
|
||||
const CONTEXT_MENU_HEIGHT = 300;
|
||||
const VIEWPORT_PADDING = 10;
|
||||
|
||||
function calculateViewportSafePosition(clientX: number, clientY: number) {
|
||||
// Keep the context menu inside the visible viewport.
|
||||
const safeX =
|
||||
clientX + CONTEXT_MENU_WIDTH > window.innerWidth
|
||||
? window.innerWidth - CONTEXT_MENU_WIDTH - VIEWPORT_PADDING
|
||||
: clientX;
|
||||
const safeY =
|
||||
clientY + CONTEXT_MENU_HEIGHT > window.innerHeight
|
||||
? window.innerHeight - CONTEXT_MENU_HEIGHT - VIEWPORT_PADDING
|
||||
: clientY;
|
||||
|
||||
return { x: Math.max(VIEWPORT_PADDING, safeX), y: Math.max(VIEWPORT_PADDING, safeY) };
|
||||
}
|
||||
|
||||
export default function FileContextMenu({
|
||||
children,
|
||||
item,
|
||||
onRename,
|
||||
onDelete,
|
||||
onNewFile,
|
||||
onNewFolder,
|
||||
onRefresh,
|
||||
onCopyPath,
|
||||
onDownload,
|
||||
isLoading = false,
|
||||
className = '',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
item?: FileContextItem | null;
|
||||
onRename?: (item: FileContextItem) => void;
|
||||
onDelete?: (item: FileContextItem) => void;
|
||||
onNewFile?: (path: string) => void;
|
||||
onNewFolder?: (path: string) => void;
|
||||
onRefresh?: () => void;
|
||||
onCopyPath?: (item: FileContextItem) => void;
|
||||
onDownload?: (item: FileContextItem) => void;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeContextMenu = useCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const openContextMenuAtCursor = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setMenuPosition(calculateViewportSafePosition(event.clientX, event.clientY));
|
||||
setIsMenuOpen(true);
|
||||
}, []);
|
||||
|
||||
const runMenuActionAndClose = useCallback((action?: () => void) => {
|
||||
closeContextMenu();
|
||||
action?.();
|
||||
}, [closeContextMenu]);
|
||||
|
||||
const menuActions = useMemo<ContextMenuAction[]>(() => {
|
||||
if (item?.type === 'file') {
|
||||
return [
|
||||
{
|
||||
key: 'rename',
|
||||
icon: Pencil,
|
||||
label: t('fileTree.context.rename', 'Rename'),
|
||||
onSelect: () => onRename?.(item),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: Trash2,
|
||||
label: t('fileTree.context.delete', 'Delete'),
|
||||
onSelect: () => onDelete?.(item),
|
||||
isDanger: true,
|
||||
},
|
||||
{
|
||||
key: 'copyPath',
|
||||
icon: Copy,
|
||||
label: t('fileTree.context.copyPath', 'Copy Path'),
|
||||
onSelect: () => onCopyPath?.(item),
|
||||
showDividerBefore: true,
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
icon: Download,
|
||||
label: t('fileTree.context.download', 'Download'),
|
||||
onSelect: () => onDownload?.(item),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (item?.type === 'directory') {
|
||||
return [
|
||||
{
|
||||
key: 'newFile',
|
||||
icon: FileText,
|
||||
label: t('fileTree.context.newFile', 'New File'),
|
||||
onSelect: () => onNewFile?.(item.path),
|
||||
},
|
||||
{
|
||||
key: 'newFolder',
|
||||
icon: FolderPlus,
|
||||
label: t('fileTree.context.newFolder', 'New Folder'),
|
||||
onSelect: () => onNewFolder?.(item.path),
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
icon: Pencil,
|
||||
label: t('fileTree.context.rename', 'Rename'),
|
||||
onSelect: () => onRename?.(item),
|
||||
showDividerBefore: true,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: Trash2,
|
||||
label: t('fileTree.context.delete', 'Delete'),
|
||||
onSelect: () => onDelete?.(item),
|
||||
isDanger: true,
|
||||
},
|
||||
{
|
||||
key: 'copyPath',
|
||||
icon: Copy,
|
||||
label: t('fileTree.context.copyPath', 'Copy Path'),
|
||||
onSelect: () => onCopyPath?.(item),
|
||||
showDividerBefore: true,
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
icon: Download,
|
||||
label: t('fileTree.context.download', 'Download'),
|
||||
onSelect: () => onDownload?.(item),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'newFile',
|
||||
icon: FileText,
|
||||
label: t('fileTree.context.newFile', 'New File'),
|
||||
onSelect: () => onNewFile?.(''),
|
||||
},
|
||||
{
|
||||
key: 'newFolder',
|
||||
icon: FolderPlus,
|
||||
label: t('fileTree.context.newFolder', 'New Folder'),
|
||||
onSelect: () => onNewFolder?.(''),
|
||||
},
|
||||
{
|
||||
key: 'refresh',
|
||||
icon: RefreshCw,
|
||||
label: t('fileTree.context.refresh', 'Refresh'),
|
||||
onSelect: onRefresh,
|
||||
showDividerBefore: true,
|
||||
},
|
||||
];
|
||||
}, [item, onCopyPath, onDelete, onDownload, onNewFile, onNewFolder, onRefresh, onRename, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleOutsideMouseDown = (event: MouseEvent) => {
|
||||
const menuElement = menuRef.current;
|
||||
if (menuElement && !menuElement.contains(event.target as Node)) {
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscapeKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleOutsideMouseDown);
|
||||
document.addEventListener('keydown', handleEscapeKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleOutsideMouseDown);
|
||||
document.removeEventListener('keydown', handleEscapeKeyDown);
|
||||
};
|
||||
}, [closeContextMenu, isMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow key support keeps the menu accessible without a mouse.
|
||||
const handleKeyboardMenuNavigation = (event: KeyboardEvent) => {
|
||||
const menuItems = menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
|
||||
if (!menuItems || menuItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
const currentIndex = Array.from(menuItems).findIndex((menuItem) => menuItem === activeElement);
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
const nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
|
||||
menuItems[nextIndex]?.focus();
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
const previousIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
|
||||
menuItems[previousIndex]?.focus();
|
||||
} else if (event.key === 'Enter' || event.key === ' ') {
|
||||
if (activeElement?.hasAttribute('role')) {
|
||||
event.preventDefault();
|
||||
activeElement.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyboardMenuNavigation);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyboardMenuNavigation);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onContextMenu={openContextMenuAtCursor} className={cn('contents', className)}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
role="menu"
|
||||
aria-label={t('fileTree.context.menuLabel', 'File context menu')}
|
||||
style={{ position: 'fixed', left: menuPosition.x, top: menuPosition.y, zIndex: 9999 }}
|
||||
className={cn(
|
||||
'min-w-[180px] py-1 px-1',
|
||||
'bg-popover border border-border rounded-lg shadow-lg',
|
||||
'animate-in fade-in-0 zoom-in-95',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">{t('fileTree.context.loading', 'Loading...')}</span>
|
||||
</div>
|
||||
) : (
|
||||
menuActions.map((action) => (
|
||||
<Fragment key={action.key}>
|
||||
{action.showDividerBefore && <div className="h-px bg-border my-1 mx-2" />}
|
||||
<button
|
||||
role="menuitem"
|
||||
tabIndex={action.isDisabled ? -1 : 0}
|
||||
disabled={isLoading || action.isDisabled}
|
||||
onClick={() => runMenuActionAndClose(action.onSelect)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-sm text-left rounded-md transition-colors',
|
||||
'focus:outline-none focus:bg-accent',
|
||||
action.isDisabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: action.isDanger
|
||||
? 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950'
|
||||
: 'hover:bg-accent',
|
||||
isLoading && 'pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{action.icon && <action.icon className="w-4 h-4 flex-shrink-0" />}
|
||||
<span className="flex-1">{action.label}</span>
|
||||
{action.shortcut && <span className="text-xs text-muted-foreground font-mono">{action.shortcut}</span>}
|
||||
</button>
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import { ChevronRight, Folder, FolderOpen } from 'lucide-react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
import FileContextMenu from '../../FileContextMenu';
|
||||
import FileContextMenu from './FileContextMenu';
|
||||
import type { FileTreeNode as FileTreeNodeType, FileTreeViewMode } from '../types/types';
|
||||
import { Input } from '../../../shared/view/ui';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user