semblance-dev/src/components/FolderTreeItem.tsx

273 lines
No EOL
8.9 KiB
TypeScript

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Folder, FolderPlus, MoreHorizontal, Check, X, ChevronRight, ChevronDown } from 'lucide-react';
import { useDraggable, useDroppable } from '@dnd-kit/core';
interface Folder {
_id: string;
id?: string;
name: string;
parent_folder_id?: string | null;
level: number;
created_by?: string;
created_at?: string;
updated_at?: string;
}
interface FolderTreeItemProps {
folder: Folder;
children?: Folder[];
allPersonas: any[];
selectedFolder: string;
isExpanded: boolean;
folderToRename: Folder | null;
renameFolderName: string;
onFolderSelect: (folderId: string) => void;
onToggleExpansion: (folderId: string) => void;
onStartRename: (folder: Folder) => void;
onCompleteRename: () => void;
onCancelRename: () => void;
onStartDelete: (folder: Folder) => void;
onStartCreateSubfolder: (parentId: string) => void;
onRenameFolderNameChange: (name: string) => void;
isDragOverlay?: boolean;
currentlyDraggedFolderId?: string | null;
}
const FolderTreeItem = ({
folder,
children = [],
allPersonas,
selectedFolder,
isExpanded,
folderToRename,
renameFolderName,
onFolderSelect,
onToggleExpansion,
onStartRename,
onCompleteRename,
onCancelRename,
onStartDelete,
onStartCreateSubfolder,
onRenameFolderNameChange,
isDragOverlay = false,
currentlyDraggedFolderId = null,
}: FolderTreeItemProps) => {
const [isHovered, setIsHovered] = useState(false);
// Calculate persona count for this folder (explicit membership only)
const personaCount = allPersonas.filter(persona =>
persona.folder_ids && persona.folder_ids.includes(folder._id)
).length;
const hasChildren = children.length > 0;
const canHaveChildren = folder.level === 0; // Only root folders can have children
const isAllPersonasFolder = folder._id === 'all-personas-root';
// Check if this folder is the current parent of the dragged folder
const isCurrentParentOfDraggedFolder = currentlyDraggedFolderId &&
children.some(child => child._id === currentlyDraggedFolderId);
// Don't allow dropping into current parent or invalid targets
// All Personas folder can always receive drops (special case for root moves)
const canReceiveDrop = isAllPersonasFolder || (canHaveChildren && !isCurrentParentOfDraggedFolder);
const {
attributes,
listeners,
setNodeRef: setDragRef,
isDragging,
} = useDraggable({
id: folder._id,
data: {
type: 'folder',
folder,
},
});
const {
setNodeRef: setDropRef,
isOver,
} = useDroppable({
id: `drop-${folder._id}`,
data: {
type: 'folder',
folder,
},
disabled: !canReceiveDrop, // Only enable drop zone for valid targets
});
const setNodeRef = (node: HTMLElement | null) => {
setDragRef(node);
setDropRef(node);
};
return (
<div ref={setNodeRef} className={isDragOverlay ? 'z-50' : ''}>
<div
className={`flex items-center justify-between group ${
isOver && canReceiveDrop
? 'bg-blue-50 border-2 border-blue-200 border-dashed rounded-md'
: ''
} ${
isDragging ? 'opacity-50' : ''
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{folderToRename && folderToRename._id === folder._id ? (
// Rename mode
<div className="flex-1 flex items-center px-3 py-2 space-x-2">
<div className="w-4 h-4" /> {/* Spacer for alignment */}
<Folder className="h-4 w-4" />
<Input
value={renameFolderName}
onChange={e => onRenameFolderNameChange(e.target.value)}
placeholder="Folder name"
className="h-7 text-sm"
autoFocus
onKeyDown={e => {
if (e.key === 'Enter') {
onCompleteRename();
} else if (e.key === 'Escape') {
onCancelRename();
}
}}
/>
<Button
size="sm"
variant="ghost"
onClick={onCompleteRename}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onCancelRename}
className="h-7 w-7 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
// Normal display mode
<>
<div
className={`flex-1 flex items-center space-x-2 px-3 py-2 text-sm rounded-md transition-colors ${
selectedFolder === folder._id
? 'bg-primary/10 text-primary font-medium'
: 'hover:bg-slate-100'
}`}
{...attributes}
{...listeners}
>
{/* Expansion chevron */}
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation();
onToggleExpansion(folder._id);
}}
className="h-4 w-4 hover:bg-slate-200 rounded flex-shrink-0"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
)}
{!hasChildren && <div className="w-4 h-4 flex-shrink-0" />} {/* Spacer for alignment */}
<Folder className="h-4 w-4 flex-shrink-0" />
<button
onClick={() => onFolderSelect(folder._id)}
className="truncate text-left flex-1 hover:underline"
>
{folder.name}
</button>
<span className="text-muted-foreground text-xs ml-auto flex-shrink-0">
{isAllPersonasFolder ? allPersonas.length : personaCount}
</span>
</div>
{/* Actions dropdown - or invisible spacer for All Personas */}
{!isAllPersonasFolder ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className={`h-7 w-7 p-0 transition-opacity ${
isHovered ? 'opacity-100' : 'opacity-0'
} group-hover:opacity-100`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onStartRename(folder)}>
Rename
</DropdownMenuItem>
{canHaveChildren && (
<DropdownMenuItem onClick={() => onStartCreateSubfolder(folder._id)}>
<FolderPlus className="h-4 w-4 mr-2" />
Create Sub-folder
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-red-600"
onClick={() => onStartDelete(folder)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
// Invisible spacer to match dropdown menu width
<div className="h-7 w-7 flex-shrink-0" />
)}
</>
)}
</div>
{/* Child folders */}
{hasChildren && isExpanded && (
<div className="ml-4 border-l border-slate-200">
{children.map(childFolder => (
<FolderTreeItem
key={childFolder._id}
folder={childFolder}
children={[]} // Children can't have children in 2-level system
allPersonas={allPersonas}
selectedFolder={selectedFolder}
isExpanded={false} // Children can't be expanded
folderToRename={folderToRename}
renameFolderName={renameFolderName}
onFolderSelect={onFolderSelect}
onToggleExpansion={onToggleExpansion}
onStartRename={onStartRename}
onCompleteRename={onCompleteRename}
onCancelRename={onCancelRename}
onStartDelete={onStartDelete}
onStartCreateSubfolder={onStartCreateSubfolder}
onRenameFolderNameChange={onRenameFolderNameChange}
currentlyDraggedFolderId={currentlyDraggedFolderId}
/>
))}
</div>
)}
</div>
);
};
export default FolderTreeItem;