import { useState, type ReactNode } from "react"; import { ChevronDown, Loader2 } from "lucide-react"; import { PillButton } from "@/app/components/ui/pill-button"; import { supabase } from "@/app/lib/supabase"; import type { EditAnnotation } from "../../shared/types"; import { applyOptimisticResolution } from "../EditCard"; /** * Card rendered above the per-edit EditCards when a message produced * multiple tracked-change proposals. Lets the user resolve every pending * edit in one click by firing the per-edit accept/reject endpoint for each * pending annotation and forwarding each response to `onResolved` so the * parent can bump the viewer version, persist override URLs, etc. * * This intentionally doesn't apply the optimistic DOM mutation that * EditCard does — bulk operations touch many edits at once and the real * re-render from the latest version will reconcile within a second or so. */ function BulkEditActions({ pending, onViewClick, onResolveStart, onResolved, onError, }: { pending: { annotation: EditAnnotation; filename: string; }[]; onViewClick?: (ann: EditAnnotation, filename: string) => void; onResolveStart?: (args: { editId: string; documentId: string; verb: "accept" | "reject"; }) => void; onResolved?: (args: { editId: string; documentId: string; status: "accepted" | "rejected"; versionId: string | null; downloadUrl: string | null; }) => void; onError?: (args: { editId: string; documentId: string; versionId: string | null; message: string; }) => void; }) { const [busy, setBusy] = useState<"accept" | "reject" | null>(null); const [progress, setProgress] = useState<{ done: number; total: number; } | null>(null); if (pending.length === 0) return null; const handleAll = async (verb: "accept" | "reject") => { if (busy) return; setBusy(verb); setProgress({ done: 0, total: pending.length }); try { const { data: { session }, } = await supabase.auth.getSession(); const token = session?.access_token; const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:3001"; // Sequential so the per-document version counter advances in a // predictable order and the viewer doesn't race between bumps. let done = 0; for (const { annotation } of pending) { onResolveStart?.({ editId: annotation.edit_id, documentId: annotation.document_id, verb, }); // Optimistically mutate the DOM so the viewer reflects the // resolution immediately. Revert if the backend call fails. let revert: (() => void) | null = null; try { revert = applyOptimisticResolution(annotation, verb); } catch (e) { console.error( "[BulkEditActions] optimistic update threw", e, ); } try { const resp = await fetch( `${apiBase}/single-documents/${annotation.document_id}/edits/${annotation.edit_id}/${verb}`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : undefined, }, ); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = (await resp.json()) as { ok: boolean; status?: "accepted" | "rejected"; version_id: string | null; download_url: string | null; }; const nextStatus = data.status ?? (verb === "accept" ? "accepted" : "rejected"); onResolved?.({ editId: annotation.edit_id, documentId: annotation.document_id, status: nextStatus, versionId: data.version_id, downloadUrl: data.download_url, }); } catch (e) { console.error("[BulkEditActions] resolve failed", e); try { revert?.(); } catch (revertErr) { console.error( "[BulkEditActions] revert threw", revertErr, ); } onError?.({ editId: annotation.edit_id, documentId: annotation.document_id, versionId: annotation.version_id ?? null, message: verb === "accept" ? "Couldn't save one or more accepts." : "Couldn't save one or more rejects.", }); } done++; setProgress({ done, total: pending.length }); } } finally { setBusy(null); setProgress(null); } }; // Optional: show a tiny "View first" action so bulk doesn't lose the // in-viewer scroll-to behaviour entirely. const first = pending[0]; return (
{summary}