cmdk filters by the CommandItem value prop, so the sentinel "__root__" silently failed to match natural search terms like "no folder". Use the human label instead. Also reset pickedId when the dialog re-opens so a cancelled pick doesn't carry a stale highlight into the next open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, 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);
|
|
|
|
useEffect(() => {
|
|
if (open) setPickedId(currentFolderId);
|
|
}, [open, 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 (no folder)"
|
|
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>
|
|
);
|
|
}
|