-
- {flow.name}
-
-
- {status.label}
-
+
+
+
+
+
+
+
+
+ {flow.name}
+
+
+ {status.label}
+
+
+
+ {nodeCount} {nodeCount === 1 ? "node" : "nodes"}
+
-
- {nodeCount} {nodeCount === 1 ? "node" : "nodes"}
-
+
Updated {formatDate(flow.updated_at)}
diff --git a/components/flow-actions.tsx b/components/flow-actions.tsx
new file mode 100644
index 00000000..23128623
--- /dev/null
+++ b/components/flow-actions.tsx
@@ -0,0 +1,216 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Upload, Download, Loader2, GitBranch, FileJson, X } from "lucide-react";
+import type { Json } from "@/lib/types/database";
+
+interface FlowExportData {
+ name: string;
+ description?: string | null;
+ nodes: Json;
+ edges: Json;
+ version: number;
+ exportedAt: string;
+ source: "zernflow";
+}
+
+function isValidFlowData(data: unknown): data is FlowExportData {
+ if (!data || typeof data !== "object") return false;
+ const d = data as Record;
+ return (
+ typeof d.name === "string" &&
+ Array.isArray(d.nodes) &&
+ Array.isArray(d.edges)
+ );
+}
+
+export function ExportFlowButton({
+ flow,
+}: {
+ flow: { id: string; name: string; nodes: Json; edges: Json; description?: string | null; version?: number };
+}) {
+ function handleExport(e: React.MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const exportData: FlowExportData = {
+ name: flow.name,
+ description: flow.description || null,
+ nodes: flow.nodes,
+ edges: flow.edges,
+ version: flow.version || 1,
+ exportedAt: new Date().toISOString(),
+ source: "zernflow",
+ };
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${flow.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.flow.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ return (
+
+ );
+}
+
+export function ImportFlowButton() {
+ const router = useRouter();
+ const fileInputRef = useRef(null);
+ const [importing, setImporting] = useState(false);
+ const [error, setError] = useState(null);
+ const [preview, setPreview] = useState(null);
+
+ async function handleFileChange(e: React.ChangeEvent) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ setError(null);
+
+ try {
+ const text = await file.text();
+ const data = JSON.parse(text);
+
+ if (!isValidFlowData(data)) {
+ throw new Error("Invalid flow file. Expected name, nodes, and edges.");
+ }
+
+ setPreview(data);
+ } catch (err) {
+ if (err instanceof SyntaxError) {
+ setError("Invalid JSON file");
+ } else {
+ setError(err instanceof Error ? err.message : "Invalid file");
+ }
+ setTimeout(() => setError(null), 4000);
+ } finally {
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ }
+ }
+
+ async function handleConfirmImport() {
+ if (!preview) return;
+
+ setImporting(true);
+ setError(null);
+
+ try {
+ const res = await fetch("/api/v1/flows", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name: preview.name + " (imported)",
+ description: preview.description || null,
+ nodes: preview.nodes,
+ edges: preview.edges,
+ }),
+ });
+
+ if (!res.ok) {
+ let errorMsg = "Failed to import flow";
+ try {
+ const err = await res.json();
+ errorMsg = err.error || errorMsg;
+ } catch {
+ errorMsg = `Server returned ${res.status}`;
+ }
+ throw new Error(errorMsg);
+ }
+
+ const flow = await res.json();
+ setPreview(null);
+ router.push(`/dashboard/flows/${flow.id}`);
+ router.refresh();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to import");
+ setTimeout(() => setError(null), 4000);
+ } finally {
+ setImporting(false);
+ }
+ }
+
+ const nodeCount = preview ? (Array.isArray(preview.nodes) ? preview.nodes.length : 0) : 0;
+ const edgeCount = preview ? (Array.isArray(preview.edges) ? preview.edges.length : 0) : 0;
+
+ return (
+ <>
+
+
+
+ {error && (
+ {error}
+ )}
+
+ {/* Import preview */}
+ {preview && (
+ setPreview(null)}>
+
e.stopPropagation()}>
+
+
+
+
+
+
{preview.name}
+
+ {nodeCount} {nodeCount === 1 ? "node" : "nodes"} ยท {edgeCount} {edgeCount === 1 ? "connection" : "connections"}
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/components/flow-builder/flow-canvas.tsx b/components/flow-builder/flow-canvas.tsx
index 4f4c7ddc..bf1893ae 100644
--- a/components/flow-builder/flow-canvas.tsx
+++ b/components/flow-builder/flow-canvas.tsx
@@ -19,7 +19,7 @@ import "@xyflow/react/dist/style.css";
import { useCallback, useRef, useState, type DragEvent } from "react";
import { useRouter } from "next/navigation";
-import { ArrowLeft, Save, Rocket, Loader2, History, Play } from "lucide-react";
+import { ArrowLeft, Save, Rocket, Loader2, History, Play, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { createClient } from "@/lib/supabase/client";
import type { Database, FlowStatus, Json } from "@/lib/types/database";
@@ -338,6 +338,30 @@ function FlowCanvasInner({ flow }: FlowCanvasProps) {
History
+