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:
115
src/components/documents/move-to-folder-dialog.tsx
Normal file
115
src/components/documents/move-to-folder-dialog.tsx
Normal 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 “{documentTitle}”</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user