feat(documents): MoveToFolderDialog single-doc move picker

cmdk Combobox dialog showing all folder paths flat (' / '-separated),
plus a "Root (no folder)" pseudo-option. Move button disabled when the
picked folder matches the document's current folder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 12:04:24 +02:00
parent ebede74ca0
commit e6103a4473

View File

@@ -0,0 +1,115 @@
'use client';
import { useMemo, useState } from 'react';
import { Check, FolderInput } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { toastError } from '@/lib/api/toast-error';
import {
buildFolderPaths,
useDocumentFolders,
useMoveDocument,
} from '@/hooks/use-document-folders';
interface MoveToFolderDialogProps {
documentId: string;
documentTitle: string;
currentFolderId: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function MoveToFolderDialog({
documentId,
documentTitle,
currentFolderId,
open,
onOpenChange,
}: MoveToFolderDialogProps) {
const { data: tree = [] } = useDocumentFolders();
const move = useMoveDocument();
const [pickedId, setPickedId] = useState<string | null>(currentFolderId);
const paths = useMemo(() => buildFolderPaths(tree), [tree]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Move &ldquo;{documentTitle}&rdquo;</DialogTitle>
</DialogHeader>
<Command>
<CommandInput placeholder="Search folders…" />
<CommandList>
<CommandEmpty>No folders match.</CommandEmpty>
<CommandGroup heading="Special">
<CommandItem
value="__root__"
onSelect={() => setPickedId(null)}
className="flex items-center gap-2"
>
<Check
className={pickedId === null ? 'h-4 w-4 opacity-100' : 'h-4 w-4 opacity-0'}
/>
Root (no folder)
</CommandItem>
</CommandGroup>
{paths.length > 0 ? (
<CommandGroup heading="Folders">
{paths.map((p) => (
<CommandItem
key={p.id}
value={p.path}
onSelect={() => setPickedId(p.id)}
className="flex items-center gap-2"
>
<Check
className={pickedId === p.id ? 'h-4 w-4 opacity-100' : 'h-4 w-4 opacity-0'}
/>
<span className="truncate">{p.path}</span>
</CommandItem>
))}
</CommandGroup>
) : null}
</CommandList>
</Command>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
disabled={pickedId === currentFolderId || move.isPending}
onClick={async () => {
try {
await move.mutateAsync({ docId: documentId, folderId: pickedId });
toast.success('Document moved');
onOpenChange(false);
} catch (err) {
toastError(err);
}
}}
>
<FolderInput className="mr-1.5 h-4 w-4" />
Move
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}