"use client"; /* Reusable Export / Import controls for the four authorable entity types (tool, workflow, component, agent). Export opens a select-all picker and downloads a single-type JSON bundle; Import uploads such a bundle and re-creates its items IN THE CURRENT PROJECT (new ids, auto-renamed on collision - never overwrites). Dropped into each list screen's header; the same bundle format works across projects. */ import { useRef, useState } from "react"; import { Icon } from "./icons"; import { Modal } from "./primitives"; import { api, ImportReport, PortableType } from "@/lib/api"; export interface PortableItem { id: string; name: string; sub?: string; // optional secondary line (e.g. kind / model) } function downloadJson(filename: string, data: unknown) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function Check({ checked, indeterminate }: { checked: boolean; indeterminate?: boolean }) { return ( {indeterminate ? : checked ? : null} ); } export function ImportExport({ project, type, typeLabel, items, onImported, size = "sm", }: { project: { id: string; name?: string; slug?: string } | null | undefined; type: PortableType; typeLabel: string; // singular, lowercase (e.g. "tool") items: PortableItem[]; onImported: () => void; size?: "sm" | "md"; }) { const [exportOpen, setExportOpen] = useState(false); const [sel, setSel] = useState>(new Set()); const [downloading, setDownloading] = useState(false); const [importing, setImporting] = useState(false); const [report, setReport] = useState(null); const [error, setError] = useState(null); const fileRef = useRef(null); const btnCls = "btn btn-secondary " + (size === "sm" ? "btn-sm" : ""); const plural = `${typeLabel}s`; const allSelected = items.length > 0 && sel.size === items.length; const someSelected = sel.size > 0 && !allSelected; function openExport() { setSel(new Set(items.map((i) => i.id))); // default to "select all" setExportOpen(true); } function toggle(id: string) { setSel((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); } function toggleAll() { setSel(allSelected ? new Set() : new Set(items.map((i) => i.id))); } async function doDownload() { if (!project || sel.size === 0) return; setDownloading(true); try { const ids = items.filter((i) => sel.has(i.id)).map((i) => i.id); // preserve list order const bundle = await api.exportBundle(project.id, type, ids); const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, ""); const base = (project.slug || project.name || "forge").toString().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "forge"; downloadJson(`${base}-${plural}-${stamp}.json`, bundle); setExportOpen(false); } catch (e: any) { setError(String(e?.message || e)); } finally { setDownloading(false); } } async function onFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ""; // allow re-selecting the same file if (!file || !project) return; setImporting(true); setError(null); setReport(null); try { const text = await file.text(); let bundle: unknown; try { bundle = JSON.parse(text); } catch { throw new Error("That file isn't valid JSON. Choose a bundle exported from Forge."); } const r = await api.importBundle(project.id, type, bundle); setReport(r); onImported(); } catch (e: any) { setError(String(e?.message || e)); } finally { setImporting(false); } } return ( <> {/* Export picker: choose which rows go into the bundle (defaults to all). */} setExportOpen(false)} title={`Export ${plural}`} width={520} footer={ <> } >
{items.map((it) => { const on = sel.has(it.id); return ( ); })}
{/* Import result. */} { setReport(null); setError(null); }} title={error ? "Import failed" : "Import complete"} width={520} footer={} > {error ? (
{error}
) : report ? (
Imported {report.imported} {report.imported === 1 ? typeLabel : plural} {report.skipped > 0 && <> · skipped {report.skipped}} {(report.toolsets_imported ?? 0) > 0 && <> · {report.toolsets_imported} tool set{report.toolsets_imported === 1 ? "" : "s"}} into {project?.name}.
{report.items.some((i) => i.renamed) && (
Renamed to avoid clashes
{report.items.filter((i) => i.renamed).map((i, n) => (
{i.original_name} → {i.name}
))}
)} {report.warnings.length > 0 && (
Heads up
{report.warnings.map((w, n) => (
{w}
))}
)}
) : null}
); }