'use client'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'; import { Fragment, useEffect, useState } from 'react'; import { BaseDocument } from '@/types/documents'; interface DocumentSelectionModalProps { isOpen: boolean; onClose: () => void; onConfirm: (selectedFiles: BaseDocument[]) => void; title: string; confirmLabel: string; isProcessing: boolean; defaultSelected?: boolean; /** * Data source: * 1. `initialFiles`: Pass local files directly (synchronous). * 2. `fetcher`: Pass an async function to load files (e.g. from server). */ initialFiles?: BaseDocument[]; fetcher?: () => Promise; } export function DocumentSelectionModal({ isOpen, onClose, onConfirm, title, confirmLabel, isProcessing, defaultSelected = false, initialFiles, fetcher, }: DocumentSelectionModalProps) { const [files, setFiles] = useState([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [isLoading, setIsLoading] = useState(false); const [lastSelectedId, setLastSelectedId] = useState(null); useEffect(() => { if (isOpen) { if (initialFiles) { setFiles(initialFiles); if (defaultSelected) { setSelectedIds(new Set(initialFiles.map((f) => f.id))); } else { setSelectedIds(new Set()); } setLastSelectedId(null); } else if (fetcher) { setIsLoading(true); fetcher() .then((data) => { setFiles(data); if (defaultSelected) { setSelectedIds(new Set(data.map((f) => f.id))); } else { setSelectedIds(new Set()); } setLastSelectedId(null); }) .catch((err) => console.error('Failed to load documents:', err)) .finally(() => setIsLoading(false)); } else { setFiles([]); setSelectedIds(new Set()); } } }, [isOpen, initialFiles, fetcher, defaultSelected]); const toggleSelection = (id: string, multiSelect: boolean, rangeSelect: boolean) => { const newSelected = new Set(multiSelect ? selectedIds : []); if (rangeSelect && lastSelectedId && files.some((f) => f.id === lastSelectedId)) { const lastIndex = files.findIndex((f) => f.id === lastSelectedId); const currentIndex = files.findIndex((f) => f.id === id); const start = Math.min(lastIndex, currentIndex); const end = Math.max(lastIndex, currentIndex); for (let i = start; i <= end; i++) { newSelected.add(files[i].id); } } else { if (newSelected.has(id)) { newSelected.delete(id); } else { newSelected.add(id); } } setSelectedIds(newSelected); setLastSelectedId(id); }; const handleRowClick = (e: React.MouseEvent, id: string) => { if (e.metaKey || e.ctrlKey) { toggleSelection(id, true, false); } else if (e.shiftKey) { toggleSelection(id, true, true); } else { // "Finder" behavior: Click selects only this one (unless multiselect modifier used) // Checkbox click is handled separately to allow toggling without clearing const newSelected = new Set(); newSelected.add(id); setSelectedIds(newSelected); setLastSelectedId(id); } }; const handleCheckboxChange = (id: string, checked: boolean) => { const newSelected = new Set(selectedIds); if (checked) newSelected.add(id); else newSelected.delete(id); setSelectedIds(newSelected); setLastSelectedId(id); }; const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedIds(new Set(files.map(f => f.id))); } else { setSelectedIds(new Set()); } }; const selectedCount = selectedIds.size; const allSelected = files.length > 0 && selectedCount === files.length; const isIndeterminate = selectedCount > 0 && selectedCount < files.length; const handleConfirmClick = () => { const selectedFiles = files.filter((f) => selectedIds.has(f.id)); onConfirm(selectedFiles); }; const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return (
{title} {files.length > 0 && (
)}
{isLoading ? (
Loading documents...
) : files.length === 0 ? (
No documents found.
) : (
{files.map((file) => { const isSelected = selectedIds.has(file.id); return (
handleRowClick(e, file.id)} className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none ${isSelected ? 'bg-accent/10' : 'hover:bg-offbase'} `} >
e.stopPropagation()}> handleCheckboxChange(file.id, e.target.checked)} className="rounded border-muted text-accent focus:ring-accent" />
{file.name}
{formatSize(file.size)}
); })}
)}
); }