From bd8bb2e0321028b1f659439bd50b2480caee3101 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 May 2026 11:59:19 +0200 Subject: [PATCH] feat(documents): FolderActionsMenu (create / rename / delete dialogs) DropdownMenu trigger with three actions: New folder (works at root or inside the selected folder), Rename, Delete (confirm-then-soft-rescue). Delete copy explicitly tells reps the contents move to the parent so nothing dies silently. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../documents/folder-actions-menu.tsx | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/components/documents/folder-actions-menu.tsx diff --git a/src/components/documents/folder-actions-menu.tsx b/src/components/documents/folder-actions-menu.tsx new file mode 100644 index 00000000..bdbf593e --- /dev/null +++ b/src/components/documents/folder-actions-menu.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { useState } from 'react'; +import { FolderPlus, Pencil, Trash2, MoreHorizontal } from 'lucide-react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { ConfirmationDialog } from '@/components/shared/confirmation-dialog'; +import { toastError } from '@/lib/api/toast-error'; +import { + useCreateFolder, + useDeleteFolder, + useRenameFolder, + useDocumentFolders, +} from '@/hooks/use-document-folders'; + +interface FolderActionsMenuProps { + /** The folder these actions apply to. `null` means root → only the + * Create-new-folder action is available. */ + selectedFolderId: string | null | undefined; + /** Callback after delete so parent can reset selection. */ + onAfterDelete?: () => void; +} + +export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderActionsMenuProps) { + const [createOpen, setCreateOpen] = useState(false); + const [renameOpen, setRenameOpen] = useState(false); + const [name, setName] = useState(''); + + const createMutation = useCreateFolder(); + const renameMutation = useRenameFolder(); + const deleteMutation = useDeleteFolder(); + const { data: tree = [] } = useDocumentFolders(); + + const isFolderSelected = typeof selectedFolderId === 'string'; + const currentName = (() => { + if (!isFolderSelected) return ''; + function find(nodes: typeof tree): string | null { + for (const n of nodes) { + if (n.id === selectedFolderId) return n.name; + const inChild = find(n.children); + if (inChild) return inChild; + } + return null; + } + return find(tree) ?? ''; + })(); + + return ( + <> + + + + + + { + setName(''); + setCreateOpen(true); + }} + > + + New folder {isFolderSelected ? 'inside this' : 'at root'} + + {isFolderSelected ? ( + <> + { + setName(currentName); + setRenameOpen(true); + }} + > + + Rename + + e.preventDefault()} + className="text-destructive" + > + + Delete + + } + title="Delete folder?" + description="Subfolders and documents inside will move up to the parent. The folder itself is removed." + confirmLabel="Delete folder" + onConfirm={async () => { + try { + await deleteMutation.mutateAsync(selectedFolderId as string); + toast.success('Folder deleted; contents moved to parent.'); + onAfterDelete?.(); + } catch (err) { + toastError(err); + } + }} + /> + + ) : null} + + + + + + + + New folder {isFolderSelected ? 'inside the current folder' : 'at root'} + + +
+ + setName(e.target.value)} + autoFocus + maxLength={200} + /> +
+ + + + +
+
+ + + + + Rename folder + +
+ + setName(e.target.value)} + autoFocus + maxLength={200} + /> +
+ + + + +
+
+ + ); +}