import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { CompanyPortabilityCollisionStrategy, CompanyPortabilityPreviewResult, CompanyPortabilitySource, } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { companiesApi } from "../api/companies"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { EmptyState } from "../components/EmptyState"; import { cn } from "../lib/utils"; import { ChevronDown, ChevronRight, Download, Github, Link2, Upload, } from "lucide-react"; import { Field } from "../components/agent-config-primitives"; // ── Preview tree types ──────────────────────────────────────────────── type PreviewTreeNode = { name: string; kind: "section" | "item"; action?: string; reason?: string | null; detail?: string; children: PreviewTreeNode[]; }; const TREE_BASE_INDENT = 16; const TREE_STEP_INDENT = 24; const TREE_ROW_HEIGHT_CLASS = "min-h-9"; // ── Build preview tree from preview result ──────────────────────────── function buildPreviewTree(preview: CompanyPortabilityPreviewResult): PreviewTreeNode[] { const sections: PreviewTreeNode[] = []; // Company section if (preview.plan.companyAction !== "none") { sections.push({ name: "Company", kind: "section", children: [ { name: preview.targetCompanyName ?? "New company", kind: "item", action: preview.plan.companyAction, detail: `Target: ${preview.targetCompanyName ?? "new"}`, children: [], }, ], }); } // Agents section if (preview.plan.agentPlans.length > 0) { sections.push({ name: `Agents (${preview.plan.agentPlans.length})`, kind: "section", children: preview.plan.agentPlans.map((ap) => ({ name: `${ap.slug} → ${ap.plannedName}`, kind: "item" as const, action: ap.action, reason: ap.reason, children: [], })), }); } // Projects section if (preview.plan.projectPlans.length > 0) { sections.push({ name: `Projects (${preview.plan.projectPlans.length})`, kind: "section", children: preview.plan.projectPlans.map((pp) => ({ name: `${pp.slug} → ${pp.plannedName}`, kind: "item" as const, action: pp.action, reason: pp.reason, children: [], })), }); } // Issues section if (preview.plan.issuePlans.length > 0) { sections.push({ name: `Tasks (${preview.plan.issuePlans.length})`, kind: "section", children: preview.plan.issuePlans.map((ip) => ({ name: `${ip.slug} → ${ip.plannedTitle}`, kind: "item" as const, action: ip.action, reason: ip.reason, children: [], })), }); } // Env inputs section if (preview.envInputs.length > 0) { sections.push({ name: `Environment inputs (${preview.envInputs.length})`, kind: "section", children: preview.envInputs.map((ei) => ({ name: ei.key + (ei.agentSlug ? ` (${ei.agentSlug})` : ""), kind: "item" as const, action: ei.requirement, detail: [ ei.kind, ei.requirement, ei.defaultValue !== null ? `default: ${JSON.stringify(ei.defaultValue)}` : null, ei.portability === "system_dependent" ? "system-dependent" : null, ] .filter(Boolean) .join(" · "), reason: ei.description, children: [], })), }); } return sections; } // ── Preview tree component ──────────────────────────────────────────── function ImportPreviewTree({ nodes, selectedItem, expandedSections, onToggleSection, onSelectItem, depth = 0, }: { nodes: PreviewTreeNode[]; selectedItem: string | null; expandedSections: Set; onToggleSection: (name: string) => void; onSelectItem: (name: string) => void; depth?: number; }) { return (
{nodes.map((node) => { if (node.kind === "section") { const expanded = expandedSections.has(node.name); return (
{expanded && ( )}
); } return ( ); })}
); } // ── Import detail pane ──────────────────────────────────────────────── function ImportDetailPane({ selectedItem, previewTree, }: { selectedItem: string | null; previewTree: PreviewTreeNode[]; }) { if (!selectedItem) { return ( ); } // Find the selected node let found: PreviewTreeNode | null = null; for (const section of previewTree) { for (const child of section.children) { if (child.name === selectedItem) { found = child; break; } } if (found) break; } if (!found) { return ( ); } return (

{found.name}

{found.action && ( {found.action} )}
{found.detail && (
{found.detail}
)} {found.reason && (
{found.reason}
)}
); } // ── Helpers ─────────────────────────────────────────────────────────── async function readLocalPackageSelection(fileList: FileList): Promise<{ rootPath: string | null; files: Record; }> { const files: Record = {}; let rootPath: string | null = null; for (const file of Array.from(fileList)) { const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath?.replace( /\\/g, "/", ) || file.name; const isMarkdown = relativePath.endsWith(".md"); const isPaperclipYaml = relativePath.endsWith(".paperclip.yaml") || relativePath.endsWith(".paperclip.yml"); if (!isMarkdown && !isPaperclipYaml) continue; const topLevel = relativePath.split("/")[0] ?? null; if (!rootPath && topLevel) rootPath = topLevel; files[relativePath] = await file.text(); } if (Object.keys(files).length === 0) { throw new Error("No package files were found in the selected folder."); } return { rootPath, files }; } // ── Main page ───────────────────────────────────────────────────────── export function CompanyImport() { const { selectedCompanyId, selectedCompany, setSelectedCompanyId, } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const packageInputRef = useRef(null); // Source state const [sourceMode, setSourceMode] = useState<"github" | "url" | "local">("github"); const [importUrl, setImportUrl] = useState(""); const [localPackage, setLocalPackage] = useState<{ rootPath: string | null; files: Record; } | null>(null); // Target state const [targetMode, setTargetMode] = useState<"existing" | "new">("existing"); const [collisionStrategy, setCollisionStrategy] = useState("rename"); const [newCompanyName, setNewCompanyName] = useState(""); // Preview state const [importPreview, setImportPreview] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [expandedSections, setExpandedSections] = useState>(new Set()); useEffect(() => { setBreadcrumbs([ { label: "Org Chart", href: "/org" }, { label: "Import" }, ]); }, [setBreadcrumbs]); function buildSource(): CompanyPortabilitySource | null { if (sourceMode === "local") { if (!localPackage) return null; return { type: "inline", rootPath: localPackage.rootPath, files: localPackage.files }; } const url = importUrl.trim(); if (!url) return null; if (sourceMode === "github") return { type: "github", url }; return { type: "url", url }; } // Preview mutation const previewMutation = useMutation({ mutationFn: () => { const source = buildSource(); if (!source) throw new Error("No source configured."); return companiesApi.importPreview({ source, include: { company: true, agents: true, projects: true, issues: true }, target: targetMode === "new" ? { mode: "new_company", newCompanyName: newCompanyName || null } : { mode: "existing_company", companyId: selectedCompanyId! }, collisionStrategy, }); }, onSuccess: (result) => { setImportPreview(result); // Expand all sections by default const sections = buildPreviewTree(result).map((s) => s.name); setExpandedSections(new Set(sections)); setSelectedItem(null); }, onError: (err) => { pushToast({ tone: "error", title: "Preview failed", body: err instanceof Error ? err.message : "Failed to preview import.", }); }, }); // Apply mutation const importMutation = useMutation({ mutationFn: () => { const source = buildSource(); if (!source) throw new Error("No source configured."); return companiesApi.importBundle({ source, include: { company: true, agents: true, projects: true, issues: true }, target: targetMode === "new" ? { mode: "new_company", newCompanyName: newCompanyName || null } : { mode: "existing_company", companyId: selectedCompanyId! }, collisionStrategy, }); }, onSuccess: async (result) => { await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); if (result.company.action === "created") { setSelectedCompanyId(result.company.id); } pushToast({ tone: "success", title: "Import complete", body: `${result.company.name}: ${result.agents.length} agent${result.agents.length === 1 ? "" : "s"} processed.`, }); // Reset setImportPreview(null); setLocalPackage(null); setImportUrl(""); }, onError: (err) => { pushToast({ tone: "error", title: "Import failed", body: err instanceof Error ? err.message : "Failed to apply import.", }); }, }); async function handleChooseLocalPackage(e: ChangeEvent) { const fileList = e.target.files; if (!fileList || fileList.length === 0) return; try { const pkg = await readLocalPackageSelection(fileList); setLocalPackage(pkg); setImportPreview(null); } catch (err) { pushToast({ tone: "error", title: "Package read failed", body: err instanceof Error ? err.message : "Failed to read folder.", }); } } const previewTree = importPreview ? buildPreviewTree(importPreview) : []; const hasSource = sourceMode === "local" ? !!localPackage : importUrl.trim().length > 0; const hasErrors = importPreview ? importPreview.errors.length > 0 : false; if (!selectedCompanyId) { return ; } return (
{/* Source form section */}

Import source

Choose a GitHub repo, direct URL, or local folder to import from.

{( [ { key: "github", icon: Github, label: "GitHub repo" }, { key: "url", icon: Link2, label: "Direct URL" }, { key: "local", icon: Upload, label: "Local folder" }, ] as const ).map(({ key, icon: Icon, label }) => ( ))}
{sourceMode === "local" ? (
{localPackage && ( {localPackage.rootPath ?? "package"} with{" "} {Object.keys(localPackage.files).length} file {Object.keys(localPackage.files).length === 1 ? "" : "s"} )}
{!localPackage && (

Select a folder that contains COMPANY.md and any referenced AGENTS.md files.

)}
) : ( { setImportUrl(e.target.value); setImportPreview(null); }} /> )}
{targetMode === "new" && ( setNewCompanyName(e.target.value)} placeholder="Imported Company" /> )}
{/* Preview results */} {importPreview && ( <> {/* Sticky import action bar */}
Import preview Target: {importPreview.targetCompanyName ?? "new company"} Strategy: {importPreview.collisionStrategy} {importPreview.warnings.length > 0 && ( {importPreview.warnings.length} warning{importPreview.warnings.length === 1 ? "" : "s"} )} {importPreview.errors.length > 0 && ( {importPreview.errors.length} error{importPreview.errors.length === 1 ? "" : "s"} )}
{/* Warnings */} {importPreview.warnings.length > 0 && (
{importPreview.warnings.map((w) => (
{w}
))}
)} {/* Errors */} {importPreview.errors.length > 0 && (
{importPreview.errors.map((e) => (
{e}
))}
)} {/* Two-column layout */}
)}
); }