"use client"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { X, Upload, Search, Loader2 } from "lucide-react"; import { uploadStandaloneDocument, uploadProjectDocument, addDocumentToProject, deleteDocument, } from "@/app/lib/mikeApi"; import type { MikeDocument } from "./types"; import { FileDirectory } from "./FileDirectory"; import { useDirectoryData, invalidateDirectoryCache } from "./useDirectoryData"; import { OwnerOnlyModal } from "./OwnerOnlyModal"; import { useAuth } from "@/contexts/AuthContext"; export { invalidateDirectoryCache }; interface Props { open: boolean; onClose: () => void; onSelect: (documents: MikeDocument[], projectId?: string) => void; breadcrumb: string[]; allowMultiple?: boolean; projectId?: string; } export function AddDocumentsModal({ open, onClose, onSelect, breadcrumb, allowMultiple = true, projectId, }: Props) { const { loading, standaloneDocuments, projects } = useDirectoryData(open); const { user } = useAuth(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [uploading, setUploading] = useState(false); const [uploadingFilenames, setUploadingFilenames] = useState([]); const [search, setSearch] = useState(""); const [extraUploadedDocs, setExtraUploadedDocs] = useState([]); // IDs deleted in this session — hidden locally since `useDirectoryData`'s // cached state won't re-fetch until the modal reopens. const [deletedIds, setDeletedIds] = useState>(new Set()); const [ownerOnlyAction, setOwnerOnlyAction] = useState(null); const fileInputRef = useRef(null); useEffect(() => { if (!open) return; setSearch(""); setSelectedIds(new Set()); setExtraUploadedDocs([]); setDeletedIds(new Set()); setUploadingFilenames([]); }, [open]); if (!open) return null; const q = search.toLowerCase().trim(); const allStandalone = [ ...extraUploadedDocs.filter( (u) => !standaloneDocuments.some((d) => d.id === u.id), ), ...standaloneDocuments, ].filter((d) => !deletedIds.has(d.id)); const filteredStandalone = q ? allStandalone.filter((d) => d.filename.toLowerCase().includes(q)) : allStandalone; const filteredProjects = projects .filter((p) => p.id !== projectId) .map((p) => ({ ...p, documents: (p.documents || []).filter( (d) => !deletedIds.has(d.id) && (!q || d.filename.toLowerCase().includes(q)), ), })) .filter( (p) => !q || p.name.toLowerCase().includes(q) || p.documents.length > 0, ); const allDocs = [ ...allStandalone, ...projects.flatMap((p) => p.documents || []), ]; async function handleConfirm() { const selected = allDocs.filter((d) => selectedIds.has(d.id)); if (projectId) { const toAssign = selected.filter((d) => d.project_id !== projectId); const alreadyHere = selected.filter( (d) => d.project_id === projectId, ); if (toAssign.length > 0) { setUploading(true); try { const assigned = await Promise.all( toAssign.map((d) => addDocumentToProject(projectId, d.id), ), ); onSelect([...alreadyHere, ...assigned], projectId); } catch (err) { console.error("Failed to assign documents:", err); } finally { setUploading(false); } } else { onSelect(alreadyHere, projectId); } onClose(); return; } const projectIds = new Set( selected.map((d) => d.project_id).filter(Boolean), ); const singleProjectId = projectIds.size === 1 ? [...projectIds][0]! : undefined; onSelect(selected, singleProjectId); onClose(); } async function handleDelete(ids: string[]) { // Server only allows the doc creator to delete. Filter to owned // and warn for the rest. const docsById = new Map(); for (const d of [ ...standaloneDocuments, ...extraUploadedDocs, ...projects.flatMap((p) => p.documents ?? []), ]) { docsById.set(d.id, d); } const owned = ids.filter((id) => { const d = docsById.get(id); return !d || !d.user_id || !user?.id || d.user_id === user.id; }); const blocked = ids.length - owned.length; if (owned.length === 0 && blocked > 0) { setOwnerOnlyAction( "delete these documents — only the document creator can delete a document", ); return; } const idSet = new Set(owned); try { await Promise.all(owned.map((id) => deleteDocument(id))); } catch (err) { console.error("Delete failed:", err); return; } invalidateDirectoryCache(); setExtraUploadedDocs((prev) => prev.filter((d) => !idSet.has(d.id))); setDeletedIds((prev) => { const next = new Set(prev); owned.forEach((id) => next.add(id)); return next; }); if (blocked > 0) { setOwnerOnlyAction( `delete ${blocked} of the selected documents — only the document creator can delete a document`, ); } } async function handleUpload(e: React.ChangeEvent) { const files = Array.from(e.target.files || []); if (!files.length) return; setUploadingFilenames(files.map((file) => file.name)); setUploading(true); try { const uploaded = await Promise.all( files.map((f) => projectId ? uploadProjectDocument(projectId, f) : uploadStandaloneDocument(f), ), ); invalidateDirectoryCache(); setExtraUploadedDocs((prev) => [...uploaded, ...prev]); uploaded.forEach((d) => setSelectedIds((prev) => new Set([...prev, d.id])), ); } catch (err) { console.error("Upload failed:", err); } finally { setUploading(false); setUploadingFilenames([]); if (fileInputRef.current) fileInputRef.current.value = ""; } } return createPortal(
{/* Header */}
{breadcrumb.map((segment, i) => ( {i > 0 && } {segment} ))}
{/* Search bar */}
setSearch(e.target.value)} className="flex-1 bg-transparent text-sm text-gray-700 placeholder:text-gray-400 outline-none" autoFocus /> {search && ( )}
{/* File browser */}
{/* Footer */}
{selectedIds.size > 0 && ( {selectedIds.size} selected )}
setOwnerOnlyAction(null)} />
, document.body, ); }