import { useState, useEffect, useMemo } from 'react' import { X } from 'lucide-react' import { foldersApi } from '@/api/folders' import type { FolderListItem, FolderCreate, FolderUpdate } from '@/types' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import { Button } from '@/components/ui/Button' // Predefined color options const FOLDER_COLORS = [ '#6366f1', // Indigo (default) '#8b5cf6', // Violet '#ec4899', // Pink '#ef4444', // Red '#60a5fa', // Orange '#fbbf24', // Yellow '#22c55e', // Green '#14b8a6', // Teal '#3b82f6', // Blue '#64748b', // Slate ] interface FolderEditModalProps { folder: FolderListItem | null // null for create mode parentId?: string | null // Pre-selected parent for creating subfolders folders: FolderListItem[] // All folders for parent dropdown isOpen: boolean onClose: () => void onSave: () => void } // Get all descendant IDs of a folder (to prevent cycles) function getDescendantIds(folders: FolderListItem[], folderId: string): Set { const descendants = new Set() const children = folders.filter((f) => f.parent_id === folderId) children.forEach((child) => { descendants.add(child.id) getDescendantIds(folders, child.id).forEach((id) => descendants.add(id)) }) return descendants } // Calculate folder depth function getFolderDepth(folders: FolderListItem[], folderId: string | null): number { if (!folderId) return 0 const folder = folders.find((f) => f.id === folderId) if (!folder || !folder.parent_id) return 1 return 1 + getFolderDepth(folders, folder.parent_id) } // Get indented folder name for dropdown display function getIndentedName(folders: FolderListItem[], folderId: string): string { const depth = getFolderDepth(folders, folderId) const folder = folders.find((f) => f.id === folderId) const indent = ' '.repeat(depth - 1) return indent + (depth > 1 ? '└ ' : '') + (folder?.name || '') } export function FolderEditModal({ folder, parentId: initialParentId, folders, isOpen, onClose, onSave, }: FolderEditModalProps) { const [name, setName] = useState('') const [color, setColor] = useState(FOLDER_COLORS[0]) const [parentId, setParentId] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) const isEditMode = folder !== null // Build list of valid parent options const parentOptions = useMemo(() => { // Can't be own parent, can't create cycles const invalidIds = new Set() if (folder) { // Exclude self and all descendants invalidIds.add(folder.id) getDescendantIds(folders, folder.id).forEach((id) => invalidIds.add(id)) } // Filter to valid parents only (max depth 2 so that new folder is at depth 3) return folders .filter((f) => !invalidIds.has(f.id)) .filter((f) => { const depth = getFolderDepth(folders, f.id) // If creating new folder, parent can be at depth 1 or 2 (new folder at 2 or 3) // If editing, we need to check if moving would exceed depth limit if (folder) { // Get max depth of folder's subtree const getMaxSubtreeDepth = (folderId: string): number => { const children = folders.filter((c) => c.parent_id === folderId) if (children.length === 0) return 0 return 1 + Math.max(...children.map((c) => getMaxSubtreeDepth(c.id))) } const subtreeDepth = getMaxSubtreeDepth(folder.id) // New parent depth + 1 (for this folder) + subtree must be <= 3 return depth + 1 + subtreeDepth <= 3 } return depth < 3 // Can add child to folders at depth 1 or 2 }) .sort((a, b) => { // Sort by hierarchy for better UX const aPath = getPath(folders, a.id) const bPath = getPath(folders, b.id) return aPath.localeCompare(bPath) }) }, [folder, folders]) // Get path string for sorting function getPath(allFolders: FolderListItem[], folderId: string): string { const f = allFolders.find((x) => x.id === folderId) if (!f) return '' if (!f.parent_id) return f.name return getPath(allFolders, f.parent_id) + '/' + f.name } useEffect(() => { if (folder) { setName(folder.name) setColor(folder.color) setParentId(folder.parent_id || null) } else { setName('') setColor(FOLDER_COLORS[0]) setParentId(initialParentId || null) } }, [folder, initialParentId, isOpen]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) { toast.error('Folder name is required') return } setIsSubmitting(true) try { if (isEditMode && folder) { const updateData: FolderUpdate = { name, color } // Only include parent_id if it changed if (parentId !== folder.parent_id) { updateData.parent_id = parentId } await foldersApi.update(folder.id, updateData) toast.success('Folder updated successfully') } else { const createData: FolderCreate = { name, color } if (parentId) { createData.parent_id = parentId } await foldersApi.create(createData) toast.success('Folder created successfully') } onSave() onClose() // Dispatch event to refresh folder list window.dispatchEvent(new Event('folder-changed')) } catch (err) { const errorMessage = err instanceof Error && 'response' in err ? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail : undefined toast.error(errorMessage || 'Failed to save folder') } finally { setIsSubmitting(false) } } if (!isOpen) return null return (
{/* Backdrop */}
{/* Modal */}

{isEditMode ? 'Edit Folder' : initialParentId ? 'Create Subfolder' : 'Create Folder'}

{/* Name input */}
setName(e.target.value)} placeholder="e.g., Citrix Issues" className={cn( 'mt-1 block w-full rounded-md border px-3 py-2 text-sm', 'bg-card text-foreground placeholder:text-muted-foreground', 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20', 'border-border' )} autoFocus />
{/* Parent folder dropdown */}

Folders can be nested up to 3 levels deep.

{/* Color picker */}
{FOLDER_COLORS.map((c) => (
{/* Actions */}
) } export default FolderEditModal