import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { X, FolderKanban, Loader2, XCircle, Search } from 'lucide-react'; import { api } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; interface BatchProjectModalProps { selectedIds: number[]; onClose: () => void; } export function BatchProjectModal({ selectedIds, onClose }: BatchProjectModalProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [query, setQuery] = useState(''); const { data: projects, isLoading } = useQuery({ queryKey: ['projects'], queryFn: () => api.getProjects(), }); const sortedProjects = useMemo( () => (projects ? [...projects].sort((a, b) => a.name.localeCompare(b.name)) : undefined), [projects], ); const trimmed = query.trim().toLowerCase(); const visibleProjects = trimmed ? sortedProjects?.filter((p) => p.name.toLowerCase().includes(trimmed)) : sortedProjects; const showSearch = (sortedProjects?.length ?? 0) > 5; // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); // Helper to invalidate all project-related queries const invalidateProjectQueries = () => { queryClient.invalidateQueries({ queryKey: ['archives'] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); // Invalidate project detail pages (partial match catches all project IDs) queryClient.invalidateQueries({ queryKey: ['project'] }); queryClient.invalidateQueries({ queryKey: ['project-archives'] }); }; // Assign to project mutation (uses bulk API) const assignMutation = useMutation({ mutationFn: async (projectId: number) => { await api.addArchivesToProject(projectId, selectedIds); return projectId; }, onSuccess: (projectId) => { const project = projects?.find(p => p.id === projectId); invalidateProjectQueries(); showToast(`Added ${selectedIds.length} archive${selectedIds.length !== 1 ? 's' : ''} to "${project?.name}"`); onClose(); }, onError: () => { showToast('Failed to assign project', 'error'); }, }); // Remove from project mutation (updates each archive individually) const removeMutation = useMutation({ mutationFn: async () => { for (const id of selectedIds) { await api.updateArchive(id, { project_id: null }); } return selectedIds.length; }, onSuccess: (count) => { invalidateProjectQueries(); showToast(`Removed ${count} archive${count !== 1 ? 's' : ''} from project`); onClose(); }, onError: () => { showToast('Failed to remove from project', 'error'); }, }); const isPending = assignMutation.isPending || removeMutation.isPending; return (
Assign {selectedIds.length} selected archive{selectedIds.length !== 1 ? 's' : ''} to a project
{isLoading ? (No projects yet. Create one from the Projects page.
)} {sortedProjects && sortedProjects.length > 0 && visibleProjects?.length === 0 && (—
)}