@zenith-open/zenithcms-plugin-workflows-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aman T Shekar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ import '@xyflow/react/dist/style.css';
2
+ import React from 'react';
3
+ declare const FlowBuilderPage: React.FC;
4
+ export default FlowBuilderPage;
@@ -0,0 +1,601 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { addEdge, Background, Controls, Handle, MarkerType, MiniMap, Panel, Position, ReactFlow, useEdgesState, useNodesState, } from '@xyflow/react';
3
+ import '@xyflow/react/dist/style.css';
4
+ import dagre from 'dagre';
5
+ import { AnimatePresence, motion } from 'framer-motion';
6
+ import { AlertCircle, CheckCircle2, Clock, Cpu, Database, Globe, History, Info, Loader2, Mail, MessageSquare, Play, Plus, Save, Settings, Share2, Terminal, Trash2, Wand2, Webhook, Workflow, X, Zap, } from 'lucide-react';
7
+ import React, { useCallback, useEffect, useState } from 'react';
8
+ import toast from 'react-hot-toast';
9
+ import { PageHeader } from '../components/ui/PageHeader';
10
+ import { useTheme } from '../context/ThemeContext';
11
+ import api from '../lib/api';
12
+ import { cn } from '../lib/utils';
13
+ // ── Constants ──────────────────────────────────────────────────────────────────
14
+ const ACTION_TYPES = [
15
+ {
16
+ id: 'http',
17
+ name: 'HTTP Request',
18
+ icon: Globe,
19
+ color: 'text-z-active-text',
20
+ desc: 'Outbound REST API call',
21
+ },
22
+ {
23
+ id: 'ai_prompt',
24
+ name: 'AI Engine',
25
+ icon: Cpu,
26
+ color: 'text-purple-400',
27
+ desc: 'Pass payload to AI model',
28
+ },
29
+ {
30
+ id: 'update_content',
31
+ name: 'Database Update',
32
+ icon: Database,
33
+ color: 'text-amber-400',
34
+ desc: 'Mutate Zenith records',
35
+ },
36
+ { id: 'email', name: 'Email Dispatch', icon: Mail, color: 'text-sky-400', desc: 'SMTP relay' },
37
+ {
38
+ id: 'slack',
39
+ name: 'Slack Alert',
40
+ icon: MessageSquare,
41
+ color: 'text-z-active-text',
42
+ desc: 'Team webhook notification',
43
+ },
44
+ {
45
+ id: 'webhook',
46
+ name: 'Webhook',
47
+ icon: Webhook,
48
+ color: 'text-pink-400',
49
+ desc: 'Standard webhook push',
50
+ },
51
+ {
52
+ id: 'delay',
53
+ name: 'Delay / Sleep',
54
+ icon: Clock,
55
+ color: 'text-indigo-300',
56
+ desc: 'Pause execution timer',
57
+ },
58
+ {
59
+ id: 'code',
60
+ name: 'Data Transformer',
61
+ icon: Terminal,
62
+ color: 'text-emerald-400',
63
+ desc: 'Raw JS Payload Mutation',
64
+ },
65
+ {
66
+ id: 'loop',
67
+ name: 'Sub-Flow Iterator',
68
+ icon: Workflow,
69
+ color: 'text-indigo-400',
70
+ desc: 'Spawn Sub-Flow per item',
71
+ },
72
+ {
73
+ id: 'log',
74
+ name: 'System Log',
75
+ icon: Terminal,
76
+ color: 'text-z-muted',
77
+ desc: 'Permanent audit entry',
78
+ },
79
+ ];
80
+ const LOGIC_TYPES = [
81
+ {
82
+ id: 'condition',
83
+ name: 'If / Else',
84
+ icon: Share2,
85
+ color: 'text-indigo-500',
86
+ desc: 'Branch based on payload rules',
87
+ },
88
+ ];
89
+ const TRIGGER_TYPES = [
90
+ {
91
+ id: 'webhook',
92
+ name: 'Webhook',
93
+ icon: Zap,
94
+ detail: 'POST',
95
+ desc: 'Triggered by inbound HTTP POST',
96
+ },
97
+ {
98
+ id: 'collection_change',
99
+ name: 'Data Event',
100
+ icon: Database,
101
+ detail: 'DB',
102
+ desc: 'On create/update/delete',
103
+ },
104
+ {
105
+ id: 'schedule',
106
+ name: 'Cron Schedule',
107
+ icon: Clock,
108
+ detail: 'CRON',
109
+ desc: 'Time-based execution',
110
+ },
111
+ ];
112
+ const EDGE_STYLE = {
113
+ type: 'smoothstep',
114
+ animated: true,
115
+ style: { stroke: '#10B981', strokeWidth: 2 },
116
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#10B981' },
117
+ };
118
+ // ── Custom Nodes ───────────────────────────────────────────────────────────────
119
+ const TriggerNode = ({ data, selected }) => {
120
+ const { theme } = useTheme();
121
+ const dark = theme === 'dark';
122
+ const t = TRIGGER_TYPES.find((x) => x.id === data.triggerType) || TRIGGER_TYPES[0];
123
+ const statusClass = data.runStatus === 'success'
124
+ ? 'ring-2 ring-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.3)] border-emerald-500'
125
+ : data.runStatus === 'error'
126
+ ? 'ring-2 ring-red-500 shadow-[0_0_15px_rgba(239,68,68,0.4)] border-red-500 animate-pulse'
127
+ : data.runStatus === 'skipped'
128
+ ? 'opacity-40 grayscale'
129
+ : selected
130
+ ? 'ring-2 ring-amber-500 border-amber-500'
131
+ : 'hover:border-amber-500/50';
132
+ let badgeText = '';
133
+ if (t.id === 'webhook')
134
+ badgeText = 'Awaiting POST payload';
135
+ if (t.id === 'schedule')
136
+ badgeText = data.cron || 'Every 1 hour';
137
+ if (t.id === 'collection_change')
138
+ badgeText = data.collection ? `Watch: ${data.collection}` : 'Any collection change';
139
+ return (_jsxs("div", { className: cn('px-5 py-4 border min-w-[220px] transition-all duration-300 cursor-pointer overflow-hidden backdrop-blur-md shadow-[0_4px_30px_rgba(0,0,0,0.1)]', dark ? 'bg-black/65 border-white/10 text-white' : 'bg-white/80 border-black/10 text-black', statusClass), children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-9 h-9 flex items-center justify-center bg-amber-500/10 border border-amber-500/30 text-amber-500 flex-shrink-0 rounded-sm", children: _jsx(t.icon, { size: 16 }) }), _jsxs("div", { children: [_jsxs("div", { className: "text-[10px] font-bold uppercase tracking-wider text-amber-500", children: ["Trigger \u00B7 ", t.detail] }), _jsx("div", { className: "text-sm font-bold mt-0.5", children: t.name })] })] }), badgeText && (_jsx("div", { className: cn('mt-3 px-2.5 py-1.5 border-t text-[10px] font-mono font-bold truncate', dark ? 'border-white/10 text-gray-300' : 'border-black/10 text-gray-700'), children: badgeText })), _jsx(Handle, { type: "source", position: Position.Bottom, className: "!w-3 !h-3 !bg-emerald-500 !border-2 !border-white dark:!border-black" })] }));
140
+ };
141
+ const ActionNode = ({ data, selected }) => {
142
+ const { theme } = useTheme();
143
+ const dark = theme === 'dark';
144
+ const t = ACTION_TYPES.find((x) => x.id === data.actionType) || ACTION_TYPES[0];
145
+ const statusClass = data.runStatus === 'success'
146
+ ? 'ring-2 ring-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.3)] border-emerald-500'
147
+ : data.runStatus === 'error'
148
+ ? 'ring-2 ring-red-500 shadow-[0_0_15px_rgba(239,68,68,0.4)] border-red-500 animate-pulse'
149
+ : data.runStatus === 'skipped'
150
+ ? 'opacity-40 grayscale'
151
+ : selected
152
+ ? 'ring-2 ring-z-accent border-z-accent'
153
+ : 'hover:border-z-accent/50';
154
+ let badgeText = '';
155
+ if (t.id === 'http')
156
+ badgeText = `[${data.method || 'GET'}] ${data.url || 'No URL'}`;
157
+ else if (t.id === 'slack')
158
+ badgeText = `Channel: ${data.channel || '#general'}`;
159
+ else if (t.id === 'email')
160
+ badgeText = `To: ${data.to || 'Unknown'}`;
161
+ else if (t.id === 'update_content')
162
+ badgeText = `Update: ${data.collection || 'Record'}`;
163
+ else if (t.id === 'ai_prompt')
164
+ badgeText = `Prompt: ${data.prompt ? data.prompt.substring(0, 20) + '...' : 'Unknown'}`;
165
+ else if (t.id === 'delay')
166
+ badgeText = `Wait: ${data.amount || 0} ${data.unit || 'seconds'}`;
167
+ else if (t.id === 'code')
168
+ badgeText = `{ JS Transformer }`;
169
+ else if (t.id === 'loop')
170
+ badgeText = `Iterate: ${data.arrayPath || 'Array'}`;
171
+ return (_jsxs("div", { className: cn('px-5 py-4 border min-w-[220px] transition-all duration-300 cursor-pointer overflow-hidden backdrop-blur-md shadow-[0_4px_30px_rgba(0,0,0,0.1)]', dark ? 'bg-black/65 border-white/10 text-white' : 'bg-white/80 border-black/10 text-black', statusClass), children: [_jsx(Handle, { type: "target", position: Position.Top, className: "!w-3 !h-3 !bg-emerald-500 !border-2 !border-white dark:!border-black" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('w-9 h-9 flex items-center justify-center border flex-shrink-0 rounded-sm', dark ? 'bg-z-hover border-z-border' : 'bg-gray-50 border-gray-200', t.color), children: _jsx(t.icon, { size: 16 }) }), _jsxs("div", { children: [_jsxs("div", { className: cn('text-[10px] font-bold uppercase tracking-wider', t.color.replace('text-', 'text-').replace('-400', '-500')), children: ["Action \u00B7 ", t.id] }), _jsx("div", { className: "text-sm font-bold mt-0.5", children: data.label || t.name })] })] }), badgeText && (_jsx("div", { className: cn('mt-3 px-2.5 py-1.5 border-t text-[10px] font-mono font-bold truncate', dark ? 'border-white/10 text-gray-300' : 'border-black/10 text-gray-700'), children: badgeText })), _jsx(Handle, { type: "source", position: Position.Bottom, className: "!w-3 !h-3 !bg-emerald-500 !border-2 !border-white dark:!border-black" })] }));
172
+ };
173
+ const ConditionNode = ({ data, selected }) => {
174
+ const { theme } = useTheme();
175
+ const dark = theme === 'dark';
176
+ const statusClass = data.runStatus === 'success'
177
+ ? 'ring-2 ring-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.3)] border-emerald-500'
178
+ : data.runStatus === 'error'
179
+ ? 'ring-2 ring-red-500 shadow-[0_0_15px_rgba(239,68,68,0.4)] border-red-500 animate-pulse'
180
+ : data.runStatus === 'skipped'
181
+ ? 'opacity-40 grayscale'
182
+ : selected
183
+ ? 'ring-2 ring-indigo-500 border-indigo-500'
184
+ : 'hover:border-indigo-500/50';
185
+ return (_jsxs("div", { className: cn('px-5 py-4 border-2 min-w-[220px] transition-all duration-300 cursor-pointer shadow-[4px_4px_0_0_#000]', dark ? 'bg-black border-white text-white shadow-[4px_4px_0_0_#fff]' : 'bg-white border-black text-black', statusClass), children: [_jsx(Handle, { type: "target", position: Position.Top, className: "!w-3 !h-3 !bg-emerald-500 !border-2 !border-white dark:!border-black" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-9 h-9 flex items-center justify-center bg-indigo-500/10 border border-indigo-500/30 text-indigo-500 flex-shrink-0 rounded-sm", children: _jsx(Share2, { size: 16 }) }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold uppercase tracking-wider text-indigo-500", children: "Condition" }), _jsx("div", { className: "text-sm font-bold mt-0.5", children: data.label || 'If / Else' })] })] }), _jsx("div", { className: cn('mt-3 px-2.5 py-1.5 border-t text-[10px] font-mono font-bold flex items-center justify-center', dark ? 'border-white/10 text-gray-300' : 'border-black/10 text-gray-700'), children: data.condition || 'No condition set' }), _jsxs("div", { className: cn("mt-4 pt-2 border-t flex justify-between px-2 text-[8px] font-black uppercase tracking-wider", dark ? "border-white/10" : "border-black/10"), children: [_jsx("span", { className: "text-emerald-500", children: "True" }), _jsx("span", { className: "text-red-500", children: "False" })] }), _jsx(Handle, { id: "true", type: "source", position: Position.Bottom, className: "!w-3 !h-3 !bg-emerald-500 !border-2 !border-white dark:!border-black !left-[20%]" }), _jsx(Handle, { id: "false", type: "source", position: Position.Bottom, className: "!w-3 !h-3 !bg-red-500 !border-2 !border-white dark:!border-black !left-[80%]" })] }));
186
+ };
187
+ const nodeTypes = { trigger: TriggerNode, action: ActionNode, condition: ConditionNode };
188
+ // ── Config Panel Fields ────────────────────────────────────────────────────────
189
+ const FieldInput = ({ label, value, onChange, placeholder, type = 'text', mono = false }) => (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: label }), _jsx("input", { type: type, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: placeholder, className: cn('w-full bg-z-panel backdrop-blur-md border border-z-border px-3 py-2.5 text-[11px] text-white outline-none rounded-none', 'focus:border-z-accent/50 transition-colors placeholder:text-gray-700', mono && 'font-mono') })] }));
190
+ const FieldTextarea = ({ label, value, onChange, placeholder, rows = 4, mono = false }) => (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: label }), _jsx("textarea", { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: placeholder, rows: rows, className: cn('w-full bg-z-panel backdrop-blur-md border border-z-border px-3 py-2.5 text-[11px] text-white outline-none resize-none rounded-none', 'focus:border-z-accent/50 transition-colors placeholder:text-gray-700', mono && 'font-mono') })] }));
191
+ const FieldSelect = ({ label, value, onChange, options }) => (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: label }), _jsx("select", { value: value || '', onChange: (e) => onChange(e.target.value), className: "w-full bg-z-panel backdrop-blur-md border border-z-border px-3 py-2.5 text-[11px] text-white outline-none focus:border-z-accent/50 transition-colors appearance-none rounded-none", children: options.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })] }));
192
+ // ── Dagre Auto Layout ────────────────────────────────────────────────────────
193
+ const getLayoutedElements = (nodes, edges, direction = 'TB') => {
194
+ const dagreGraph = new dagre.graphlib.Graph();
195
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
196
+ dagreGraph.setGraph({ rankdir: direction });
197
+ nodes.forEach((node) => {
198
+ dagreGraph.setNode(node.id, { width: 260, height: 160 });
199
+ });
200
+ edges.forEach((edge) => {
201
+ dagreGraph.setEdge(edge.source, edge.target);
202
+ });
203
+ dagre.layout(dagreGraph);
204
+ const newNodes = nodes.map((node) => {
205
+ const nodeWithPosition = dagreGraph.node(node.id);
206
+ return {
207
+ ...node,
208
+ position: {
209
+ x: nodeWithPosition.x - 260 / 2,
210
+ y: nodeWithPosition.y - 160 / 2,
211
+ },
212
+ };
213
+ });
214
+ return { nodes: newNodes, edges };
215
+ };
216
+ // ── Main Page ──────────────────────────────────────────────────────────────────
217
+ const FlowBuilderPage = () => {
218
+ const { theme } = useTheme();
219
+ const dark = theme === 'dark';
220
+ const [flows, setFlows] = useState([]);
221
+ const [selectedFlow, setSelectedFlow] = useState(null);
222
+ const [loading, setLoading] = useState(true);
223
+ const [saving, setSaving] = useState(false);
224
+ const [showNodeMenu, setShowNodeMenu] = useState(false);
225
+ const [testRunning, setTestRunning] = useState(false);
226
+ const [runLogs, setRunLogs] = useState([]);
227
+ const [showLogs, setShowLogs] = useState(false);
228
+ const [runs, setRuns] = useState([]);
229
+ const [activeRunId, setActiveRunId] = useState(null);
230
+ const [showTestModal, setShowTestModal] = useState(false);
231
+ const [testPayload, setTestPayload] = useState('{\n "amount": 1500,\n "type": "sale"\n}');
232
+ const [rfInstance, setRfInstance] = useState(null);
233
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
234
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
235
+ const activeNode = nodes.find((n) => n.selected);
236
+ // ── Visual Tracing Hook ──────────────────────────────────────────────────────
237
+ const activeRun = runs.find((r) => r._id === activeRunId || r.id === activeRunId);
238
+ const visualNodes = React.useMemo(() => {
239
+ if (!activeRun || !showLogs)
240
+ return nodes.map((n) => ({ ...n, data: { ...n.data, runStatus: 'idle' } }));
241
+ const errorLog = runLogs.find((l) => l.runId === activeRunId && l.level === 'error');
242
+ const errorNodeId = errorLog?.nodeId;
243
+ return nodes.map((n) => {
244
+ let status = 'skipped';
245
+ if (activeRun.completedNodes?.[n.id])
246
+ status = 'success';
247
+ else if (n.id === errorNodeId)
248
+ status = 'error';
249
+ return { ...n, data: { ...n.data, runStatus: status } };
250
+ });
251
+ }, [nodes, activeRun, showLogs, runLogs, activeRunId]);
252
+ const visualEdges = React.useMemo(() => {
253
+ if (!activeRun || !showLogs)
254
+ return edges.map((e) => ({ ...e, animated: false, style: EDGE_STYLE.style }));
255
+ return edges.map((e) => {
256
+ let traversed = false;
257
+ const parentResult = activeRun.completedNodes?.[e.source];
258
+ if (parentResult) {
259
+ if (!parentResult.isCondition) {
260
+ traversed = true;
261
+ }
262
+ else {
263
+ const expectedHandle = e.sourceHandle || e.source;
264
+ if (parentResult.branch === expectedHandle)
265
+ traversed = true;
266
+ }
267
+ }
268
+ if (traversed) {
269
+ return {
270
+ ...e,
271
+ animated: true,
272
+ style: {
273
+ stroke: '#10B981',
274
+ strokeWidth: 3,
275
+ filter: 'drop-shadow(0 0 5px rgba(16,185,129,0.5))',
276
+ },
277
+ zIndex: 10,
278
+ };
279
+ }
280
+ else {
281
+ return {
282
+ ...e,
283
+ animated: false,
284
+ style: { stroke: '#444', strokeWidth: 1, opacity: 0.3 },
285
+ };
286
+ }
287
+ });
288
+ }, [edges, activeRun, showLogs]);
289
+ // ── Data Fetching ────────────────────────────────────────────────────────────
290
+ const fetchFlows = async () => {
291
+ try {
292
+ const res = await api.get('/flows');
293
+ setFlows(res.data.data || []);
294
+ }
295
+ catch {
296
+ toast.error('Registry sync failed');
297
+ }
298
+ finally {
299
+ setLoading(false);
300
+ }
301
+ };
302
+ useEffect(() => {
303
+ fetchFlows();
304
+ }, []);
305
+ useEffect(() => {
306
+ if (!selectedFlow)
307
+ return;
308
+ if (selectedFlow.nodes?.length > 0) {
309
+ setNodes(selectedFlow.nodes);
310
+ setEdges(selectedFlow.edges || []);
311
+ }
312
+ else {
313
+ const n = [
314
+ {
315
+ id: 'trigger_1',
316
+ type: 'trigger',
317
+ position: { x: 200, y: 80 },
318
+ data: {
319
+ triggerType: selectedFlow.trigger?.type || 'webhook',
320
+ ...selectedFlow.trigger?.config,
321
+ },
322
+ },
323
+ ];
324
+ setNodes(n);
325
+ setEdges([]);
326
+ }
327
+ setRunLogs([]);
328
+ setShowLogs(false);
329
+ }, [selectedFlow?._id]);
330
+ // ── Handlers ─────────────────────────────────────────────────────────────────
331
+ const onConnect = useCallback((params) => {
332
+ setEdges((eds) => addEdge({ ...params, ...EDGE_STYLE }, eds));
333
+ }, [setEdges]);
334
+ const onLayout = useCallback(() => {
335
+ const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(nodes, edges);
336
+ setNodes([...layoutedNodes]);
337
+ setEdges([...layoutedEdges]);
338
+ setTimeout(() => {
339
+ rfInstance?.fitView({ padding: 0.3, duration: 800 });
340
+ }, 100);
341
+ }, [nodes, edges, rfInstance, setNodes, setEdges]);
342
+ const createNewFlow = () => {
343
+ const initialNodes = [
344
+ {
345
+ id: 'trigger_1',
346
+ type: 'trigger',
347
+ position: { x: 200, y: 80 },
348
+ data: { triggerType: 'webhook' },
349
+ },
350
+ ];
351
+ const newFlow = {
352
+ name: 'NEW_AUTOMATION',
353
+ description: 'Untitled automation sequence',
354
+ active: false,
355
+ nodes: initialNodes,
356
+ edges: [],
357
+ };
358
+ setSelectedFlow(newFlow);
359
+ setNodes(initialNodes);
360
+ setEdges([]);
361
+ setRunLogs([]);
362
+ setShowLogs(false);
363
+ };
364
+ const saveFlow = async () => {
365
+ if (!selectedFlow)
366
+ return;
367
+ setSaving(true);
368
+ try {
369
+ const payload = { ...selectedFlow, nodes, edges };
370
+ if (selectedFlow._id) {
371
+ await api.patch(`/flows/${selectedFlow._id}`, payload);
372
+ toast.success('Automation saved');
373
+ }
374
+ else {
375
+ const res = await api.post('/flows', payload);
376
+ setSelectedFlow(res.data.data);
377
+ setFlows((prev) => [res.data.data, ...prev]);
378
+ toast.success('Automation created');
379
+ }
380
+ fetchFlows();
381
+ }
382
+ catch {
383
+ toast.error('Save failed');
384
+ }
385
+ finally {
386
+ setSaving(false);
387
+ }
388
+ };
389
+ const deleteFlow = async (id) => {
390
+ if (!id) {
391
+ setSelectedFlow(null);
392
+ return;
393
+ }
394
+ if (!confirm('Delete this automation permanently?'))
395
+ return;
396
+ try {
397
+ await api.delete(`/flows/${id}`);
398
+ setFlows((prev) => prev.filter((f) => f._id !== id));
399
+ setSelectedFlow(null);
400
+ toast.success('Automation deleted');
401
+ }
402
+ catch {
403
+ toast.error('Delete failed');
404
+ }
405
+ };
406
+ const addActionNode = (actionType) => {
407
+ const sourceId = activeNode?.id || nodes[nodes.length - 1]?.id;
408
+ const newNode = {
409
+ id: `node_${Date.now()}`,
410
+ type: 'action',
411
+ position: { x: 200, y: nodes.length * 170 + 80 },
412
+ data: { actionType, label: ACTION_TYPES.find((a) => a.id === actionType)?.name },
413
+ };
414
+ setNodes((nds) => [...nds, newNode]);
415
+ if (sourceId) {
416
+ setEdges((eds) => addEdge({
417
+ id: `e_${sourceId}_${newNode.id}`,
418
+ source: sourceId,
419
+ target: newNode.id,
420
+ ...EDGE_STYLE,
421
+ }, eds));
422
+ }
423
+ setShowNodeMenu(false);
424
+ };
425
+ const addLogicNode = () => {
426
+ const sourceId = activeNode?.id || nodes[nodes.length - 1]?.id;
427
+ const newNode = {
428
+ id: `node_${Date.now()}`,
429
+ type: 'condition',
430
+ position: { x: 200, y: nodes.length * 170 + 80 },
431
+ data: { label: 'If / Else', condition: '{{payload.amount}} > 1000' },
432
+ };
433
+ setNodes((nds) => [...nds, newNode]);
434
+ if (sourceId) {
435
+ setEdges((eds) => addEdge({
436
+ id: `e_${sourceId}_${newNode.id}`,
437
+ source: sourceId,
438
+ target: newNode.id,
439
+ ...EDGE_STYLE,
440
+ }, eds));
441
+ }
442
+ setShowNodeMenu(false);
443
+ };
444
+ const updateNodeData = (id, patch) => {
445
+ setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, ...patch } } : n)));
446
+ };
447
+ const deselectAll = () => {
448
+ setNodes((nds) => nds.map((n) => ({ ...n, selected: false })));
449
+ };
450
+ const executeTestFlow = async () => {
451
+ if (!selectedFlow?._id) {
452
+ toast.error('Please save the automation before testing');
453
+ return;
454
+ }
455
+ let parsed;
456
+ try {
457
+ parsed = JSON.parse(testPayload);
458
+ }
459
+ catch {
460
+ toast.error('Invalid JSON payload');
461
+ return;
462
+ }
463
+ setTestRunning(true);
464
+ setShowLogs(true);
465
+ setShowTestModal(false);
466
+ setRunLogs([{ level: 'info', msg: 'Dispatching test trigger...' }]);
467
+ try {
468
+ const res = await api.post(`/flows/${selectedFlow._id}/test`, { payload: parsed });
469
+ setActiveRunId(res.data?.data?.runId);
470
+ fetchLogsLoop(res.data?.data?.runId);
471
+ }
472
+ catch (err) {
473
+ setRunLogs([{ level: 'error', msg: err?.response?.data?.message || 'Test execution failed' }]);
474
+ setTestRunning(false);
475
+ }
476
+ };
477
+ const fetchLogsLoop = async (runId) => {
478
+ let polling = true;
479
+ let attempts = 0;
480
+ while (polling && attempts < 15) {
481
+ attempts++;
482
+ try {
483
+ const res = await api.get(`/flows/${selectedFlow?._id}/logs`);
484
+ const allRuns = res.data?.data?.runs || [];
485
+ const allLogs = res.data?.data?.logs || [];
486
+ setRuns(allRuns);
487
+ const myLogs = allLogs
488
+ .filter((l) => l.runId === runId)
489
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
490
+ setRunLogs(myLogs);
491
+ const myRun = allRuns.find((r) => r._id === runId || r.id === runId);
492
+ if (myRun && myRun.status !== 'running') {
493
+ polling = false;
494
+ setTestRunning(false);
495
+ }
496
+ }
497
+ catch (err) {
498
+ console.error(err);
499
+ }
500
+ if (polling)
501
+ await new Promise((r) => setTimeout(r, 2000));
502
+ }
503
+ setTestRunning(false);
504
+ };
505
+ const openHistory = async () => {
506
+ setShowLogs(true);
507
+ try {
508
+ const res = await api.get(`/flows/${selectedFlow?._id}/logs`);
509
+ setRuns(res.data?.data?.runs || []);
510
+ setRunLogs(res.data?.data?.logs || []);
511
+ setActiveRunId(res.data?.data?.runs?.[0]?._id || null);
512
+ }
513
+ catch (err) {
514
+ toast.error('Failed to load history');
515
+ }
516
+ };
517
+ // ── Loading State ─────────────────────────────────────────────────────────────
518
+ if (loading) {
519
+ return (_jsxs("div", { className: "h-[calc(100vh-73px)] flex flex-col items-center justify-center gap-4", children: [_jsx(Loader2, { className: "w-8 h-8 animate-spin text-gray-600" }), _jsx("span", { className: "text-[9px] font-black uppercase tracking-[0.4em] text-gray-600", children: "Loading Automations\u2026" })] }));
520
+ }
521
+ // ── Render ────────────────────────────────────────────────────────────────────
522
+ return (_jsxs("div", { className: cn('absolute inset-0 flex text-white overflow-hidden', dark ? 'bg-black' : 'bg-gray-50'), children: [_jsxs("div", { className: cn('w-64 flex-shrink-0 border-r flex flex-col z-10', dark ? 'bg-black border-z-border' : 'bg-z-panel border-z-border'), children: [_jsxs("div", { className: cn('p-5 border-b flex items-center justify-between flex-shrink-0', dark ? 'border-z-border' : 'border-z-border'), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "w-2 h-2 rounded-full bg-z-accent shadow-sm" }), _jsx("span", { className: cn('text-[9px] font-black uppercase tracking-[0.3em]', dark ? 'text-white' : 'text-black'), children: "Automations" })] }), _jsx("button", { onClick: createNewFlow, title: "New automation", className: cn('w-7 h-7 flex items-center justify-center hover:scale-105 transition-all', dark ? 'bg-white text-black' : 'bg-black text-white'), children: _jsx(Plus, { size: 14 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-3 space-y-1", children: [flows.length === 0 && (_jsx("p", { className: "text-[9px] text-gray-600 uppercase tracking-widest p-3 text-center", children: "No automations yet" })), flows.map((flow) => (_jsxs("button", { onClick: () => setSelectedFlow(flow), className: cn('w-full text-left px-3 py-3 transition-all flex flex-col border group', selectedFlow?._id === flow._id
523
+ ? dark
524
+ ? 'bg-white/[0.06] border-z-border-strong'
525
+ : 'bg-black/5 border-gray-400'
526
+ : dark
527
+ ? 'border-transparent hover:bg-z-hover hover:border-z-border'
528
+ : 'border-transparent hover:bg-black/[0.03] hover:border-z-border'), children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("span", { className: cn('text-[9px] font-black uppercase tracking-tight truncate max-w-[140px]', dark ? 'text-white' : 'text-z-primary'), children: flow.name }), _jsx("div", { className: cn('w-1.5 h-1.5 rounded-full flex-shrink-0', flow.active ? 'bg-z-accent shadow-sm' : 'bg-gray-700') })] }), _jsxs("p", { className: "text-[8px] text-z-secondary uppercase tracking-tight truncate", children: [flow.nodes?.length || 0, " nodes \u00B7 ", flow.active ? 'Live' : 'Idle'] })] }, flow._id)))] })] }), _jsx("div", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: selectedFlow ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: _jsx("input", { value: selectedFlow.name, onChange: (e) => setSelectedFlow({ ...selectedFlow, name: e.target.value }), className: "bg-transparent text-xl font-black uppercase tracking-tighter outline-none focus-visible:ring-2 focus-visible:ring-z-active-border min-w-[200px] max-w-[400px]" }), description: _jsx("input", { value: selectedFlow.description, onChange: (e) => setSelectedFlow({ ...selectedFlow, description: e.target.value }), className: "bg-transparent text-inherit uppercase tracking-widest outline-none focus-visible:ring-2 focus-visible:ring-z-active-border w-full max-w-md" }), actions: _jsxs(_Fragment, { children: [_jsxs("button", { onClick: onLayout, title: "Auto-Layout Graph", className: cn('px-4 h-9 border text-[9px] font-black uppercase tracking-widest transition-all flex items-center gap-2', 'border-z-border text-z-muted hover:border-white/20 hover:text-white'), children: [_jsx(Wand2, { size: 12 }), "Tidy"] }), _jsxs("button", { onClick: openHistory, disabled: !selectedFlow._id, title: "View Execution History", className: cn('px-4 h-9 border text-[9px] font-black uppercase tracking-widest transition-all flex items-center gap-2', 'border-z-border text-z-muted hover:border-white/20 hover:text-white'), children: [_jsx(History, { size: 12 }), "History"] }), _jsxs("button", { onClick: () => setShowTestModal(true), disabled: testRunning || !selectedFlow._id, title: !selectedFlow._id ? 'Save first to test' : 'Run test execution', className: cn('px-4 h-9 border text-[9px] font-black uppercase tracking-widest transition-all flex items-center gap-2', 'border-z-border text-z-muted hover:border-white/20 hover:text-white disabled:opacity-40 disabled:cursor-not-allowed'), children: [testRunning ? (_jsx(Loader2, { size: 12, className: "animate-spin" })) : (_jsx(Play, { size: 12 })), "Test Run"] }), _jsx("button", { onClick: () => setSelectedFlow({ ...selectedFlow, active: !selectedFlow.active }), className: cn('px-4 h-9 border text-[9px] font-black uppercase tracking-widest transition-all', selectedFlow.active
529
+ ? 'border-z-accent text-z-active-text bg-z-active-bg hover:bg-z-accent/20'
530
+ : 'border-z-border text-z-secondary hover:border-white/20 hover:text-white'), children: selectedFlow.active ? '● Live' : '○ Idle' }), _jsxs("button", { onClick: saveFlow, disabled: saving, className: "px-5 h-9 bg-z-accent text-white text-[9px] font-black uppercase tracking-widest hover:opacity-90 transition-all flex items-center gap-2 disabled:opacity-50", children: [saving ? _jsx(Loader2, { size: 12, className: "animate-spin" }) : _jsx(Save, { size: 12 }), "Save"] }), _jsx("button", { onClick: () => deleteFlow(selectedFlow._id), className: "h-9 w-9 flex items-center justify-center text-gray-600 hover:text-red-500 hover:bg-red-500/10 transition-all border border-transparent hover:border-red-500/20", children: _jsx(Trash2, { size: 15 }) })] }) }), _jsxs("div", { className: "flex-1 flex overflow-hidden", children: [_jsx("div", { className: "flex-1 overflow-hidden", children: _jsxs(ReactFlow, { nodes: visualNodes, edges: visualEdges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: onConnect, onInit: setRfInstance, nodeTypes: nodeTypes, onPaneClick: () => {
531
+ deselectAll();
532
+ setShowNodeMenu(false);
533
+ }, fitView: true, fitViewOptions: { padding: 0.3 }, minZoom: 0.2, maxZoom: 2, deleteKeyCode: "Delete", children: [_jsx(Background, { color: dark ? '#222' : '#ddd', gap: 20, size: 1 }), _jsx(Controls, { showInteractive: false, className: cn('!border-2 !rounded-none !shadow-[4px_4px_0_0_#000]', dark
534
+ ? '!bg-black !border-white !text-white !shadow-[4px_4px_0_0_#fff] [&_button]:!bg-black [&_button]:!border-b [&_button]:!border-white/20 [&_button_svg]:!fill-white hover:[&_button]:!bg-white hover:[&_button_svg]:!fill-black'
535
+ : '!bg-white !border-black !text-black [&_button]:!bg-white [&_button]:!border-b [&_button]:!border-black/20 [&_button_svg]:!fill-black hover:[&_button]:!bg-black hover:[&_button_svg]:!fill-white') }), _jsx(MiniMap, { nodeColor: () => (dark ? '#333' : '#eee'), maskColor: dark ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)', className: cn('!border-2 !rounded-none !shadow-[4px_4px_0_0_#000]', dark ? '!bg-black !border-white !shadow-[4px_4px_0_0_#fff]' : '!bg-white !border-black') }), _jsx(Panel, { position: "bottom-center", className: "mb-6 pointer-events-auto", children: _jsxs("div", { className: "relative flex flex-col items-center", children: [_jsx(AnimatePresence, { children: showNodeMenu && (_jsxs(motion.div, { initial: { opacity: 0, y: 8, scale: 0.95 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: { opacity: 0, y: 8, scale: 0.95 }, transition: { duration: 0.15 }, className: cn('absolute bottom-full mb-3 p-2 grid grid-cols-2 gap-1.5 min-w-[340px] shadow-lg rounded-md border', dark ? 'bg-[#1c1c1c] border-gray-800' : 'bg-white border-gray-300'), children: [_jsx("div", { className: cn('col-span-2 px-2 pt-1 pb-2 border-b', dark ? 'border-gray-800' : 'border-gray-200'), children: _jsx("p", { className: "text-xs font-bold uppercase tracking-wider text-gray-500", children: "Add Action Node" }) }), ACTION_TYPES.map((type) => (_jsxs("button", { onClick: () => addActionNode(type.id), className: cn('flex items-center gap-2.5 px-3 py-2.5 border rounded-sm transition-all text-left', dark
536
+ ? 'border-transparent hover:bg-[#2c2c2c] hover:border-gray-700'
537
+ : 'border-transparent hover:bg-gray-50 hover:border-gray-200'), children: [_jsx("div", { className: cn('w-8 h-8 flex items-center justify-center border flex-shrink-0 rounded-sm', type.color, dark
538
+ ? 'bg-[#2c2c2c] border-gray-700'
539
+ : 'bg-gray-50 border-gray-200'), children: _jsx(type.icon, { size: 14 }) }), _jsxs("div", { children: [_jsx("div", { className: "text-[9px] font-bold text-white", children: type.name }), _jsx("div", { className: "text-[7px] text-gray-600 uppercase tracking-wider", children: type.desc })] })] }, type.id))), _jsx("div", { className: cn('col-span-2 px-2 pt-1 pb-2 border-b mt-2', dark ? 'border-gray-800' : 'border-gray-200'), children: _jsx("p", { className: "text-xs font-bold uppercase tracking-wider text-gray-500", children: "Logic & Routing" }) }), LOGIC_TYPES.map((type) => (_jsxs("button", { onClick: () => addLogicNode(), className: cn('flex items-center gap-2.5 px-3 py-2.5 border rounded-sm transition-all text-left', dark
540
+ ? 'border-transparent hover:bg-[#2c2c2c] hover:border-gray-700'
541
+ : 'border-transparent hover:bg-gray-50 hover:border-gray-200'), children: [_jsx("div", { className: cn('w-8 h-8 flex items-center justify-center border flex-shrink-0 rounded-sm', type.color, dark
542
+ ? 'bg-[#2c2c2c] border-gray-700'
543
+ : 'bg-gray-50 border-gray-200'), children: _jsx(type.icon, { size: 14 }) }), _jsxs("div", { children: [_jsx("div", { className: "text-[9px] font-bold text-white", children: type.name }), _jsx("div", { className: "text-[7px] text-gray-600 uppercase tracking-wider", children: type.desc })] })] }, type.id)))] })) }), _jsx("button", { onClick: () => setShowNodeMenu((v) => !v), className: cn('w-12 h-12 flex items-center justify-center border-2 transition-all duration-200 !shadow-[4px_4px_0_0_#000]', showNodeMenu
544
+ ? 'bg-z-accent border-z-active-border text-white rotate-45'
545
+ : dark ? 'bg-black border-white text-white !shadow-[4px_4px_0_0_#fff]' : 'bg-white border-black text-black'), children: _jsx(Plus, { size: 22, className: "transition-transform duration-200" }) })] }) })] }) }), _jsx(AnimatePresence, { mode: "wait", children: activeNode && (_jsxs(motion.div, { initial: { width: 0, opacity: 0 }, animate: { width: 360, opacity: 1 }, exit: { width: 0, opacity: 0 }, transition: { duration: 0.2 }, className: cn('flex-shrink-0 border-l flex flex-col overflow-hidden', 'z-panel'), style: { width: 360 }, children: [_jsxs("div", { className: cn('px-5 py-4 border-b flex items-center justify-between flex-shrink-0', dark ? 'border-z-border' : 'border-z-border'), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx(Settings, { size: 13, className: "text-z-secondary" }), _jsx("span", { className: cn('text-[9px] font-black uppercase tracking-[0.25em]', dark ? 'text-white' : 'text-z-primary'), children: "Configure Node" })] }), _jsx("button", { onClick: deselectAll, className: "text-z-secondary hover:text-white transition-colors p-1", children: _jsx(X, { size: 14 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-5 space-y-5", children: [_jsxs("div", { className: "bg-z-accent/5 border border-z-accent/15 p-3 flex gap-2.5", children: [_jsx(Info, { size: 11, className: "text-z-active-text flex-shrink-0 mt-0.5" }), _jsxs("p", { className: "text-[8px] text-z-muted uppercase tracking-widest leading-relaxed", children: ["Use", ' ', _jsx("code", { className: "text-z-active-text font-mono", children: '{{payload.field}}' }), ' ', "to inject data from the trigger into any field."] })] }), activeNode.type === 'trigger' ? (_jsx(TriggerConfigPanel, { node: activeNode, flowId: selectedFlow._id, updateNodeData: updateNodeData, dark: dark })) : activeNode.type === 'condition' ? (_jsx(ConditionConfigPanel, { node: activeNode, updateNodeData: updateNodeData, dark: dark })) : (_jsx(ActionConfigPanel, { node: activeNode, updateNodeData: updateNodeData, dark: dark }))] })] }, activeNode.id)) })] }), _jsx(AnimatePresence, { children: showLogs && (_jsxs(motion.div, { initial: { height: 0 }, animate: { height: 250 }, exit: { height: 0 }, className: cn('flex-shrink-0 border-t flex overflow-hidden', dark ? 'border-z-border bg-black' : 'border-z-border bg-gray-50'), children: [_jsxs("div", { className: cn('w-64 border-r flex flex-col', dark ? 'border-z-border' : 'border-z-border'), children: [_jsx("div", { className: cn('px-4 py-3 border-b flex items-center justify-between', dark ? 'border-z-border' : 'border-z-border'), children: _jsx("span", { className: "text-[9px] font-black uppercase tracking-[0.2em] text-white", children: "Execution History" }) }), _jsx("div", { className: "flex-1 overflow-y-auto p-2 space-y-1", children: runs.map((r) => (_jsxs("button", { onClick: () => setActiveRunId(r._id), className: cn('w-full text-left px-3 py-2 border rounded-sm flex items-center justify-between transition-colors', activeRunId === r._id
546
+ ? 'border-z-border-strong bg-white/5'
547
+ : 'border-transparent hover:bg-white/[0.02]', dark ? '' : 'text-black'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [r.status === 'running' ? (_jsx(Loader2, { size: 12, className: "animate-spin text-amber-500" })) : r.status === 'failed' ? (_jsx(AlertCircle, { size: 12, className: "text-red-500" })) : (_jsx(CheckCircle2, { size: 12, className: "text-emerald-500" })), _jsx("span", { className: "text-[10px] font-mono text-gray-400", children: String(r._id).slice(-6) })] }), _jsx("span", { className: "text-[9px] text-gray-500", children: new Date(r.createdAt).toLocaleTimeString() })] }, r._id))) })] }), _jsxs("div", { className: "flex-1 flex flex-col", children: [_jsxs("div", { className: cn('px-5 py-3 border-b flex items-center justify-between', dark ? 'border-z-border' : 'border-z-border'), children: [_jsx("span", { className: "text-[9px] font-black uppercase tracking-[0.3em] text-z-secondary", children: "Run Logs" }), _jsx("button", { onClick: () => setShowLogs(false), className: "text-gray-600 hover:text-white transition-colors", children: _jsx(X, { size: 14 }) })] }), _jsxs("div", { className: "p-4 space-y-1.5 overflow-y-auto flex-1 font-mono", children: [runLogs
548
+ .filter((l) => l.runId === activeRunId || testRunning)
549
+ .map((log, i) => (_jsxs("div", { className: "flex items-start gap-2.5", children: [log.level === 'error' ? (_jsx(AlertCircle, { size: 11, className: "text-red-500 flex-shrink-0 mt-0.5" })) : log.level === 'success' ? (_jsx(CheckCircle2, { size: 11, className: "text-z-active-text flex-shrink-0 mt-0.5" })) : (_jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-600 flex-shrink-0 mt-1.5" })), _jsxs("span", { className: cn('text-[10px]', log.level === 'error'
550
+ ? 'text-red-400'
551
+ : log.level === 'success'
552
+ ? 'text-z-active-text'
553
+ : 'text-gray-300'), children: [_jsxs("span", { className: "text-gray-600 mr-2", children: ["[", new Date(log.timestamp || Date.now()).toLocaleTimeString(), "]"] }), log.nodeId && (_jsxs("span", { className: "text-indigo-400 mr-2", children: ["[", log.nodeId, "]"] })), log.msg] })] }, i))), testRunning && (_jsxs("div", { className: "flex items-center gap-2.5 mt-2", children: [_jsx(Loader2, { size: 11, className: "animate-spin text-z-secondary" }), _jsx("span", { className: "text-[10px] text-z-secondary", children: "Engine running\u2026" })] }))] })] })] })) }), _jsx(AnimatePresence, { children: showTestModal && (_jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm", children: _jsxs(motion.div, { initial: { opacity: 0, scale: 0.95, y: 10 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.95, y: 10 }, className: cn('w-full max-w-lg overflow-hidden flex flex-col border rounded-none shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur-md', dark ? 'bg-black/65 border-white/10' : 'bg-white/80 border-black/10'), children: [_jsxs("div", { className: cn('px-5 py-4 border-b flex items-center justify-between', dark ? 'border-white/10' : 'border-black/10'), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center", children: _jsx(Zap, { size: 14, className: "text-indigo-400" }) }), _jsxs("div", { children: [_jsx("h3", { className: cn('text-xs font-bold uppercase tracking-wider', dark ? 'text-white' : 'text-black'), children: "Inject Test Payload" }), _jsx("p", { className: "text-[10px] text-gray-500", children: "Provide JSON data to trigger the workflow." })] })] }), _jsx("button", { onClick: () => setShowTestModal(false), className: "p-1 hover:bg-white/10 rounded-sm text-gray-500 transition-colors", children: _jsx(X, { size: 16 }) })] }), _jsx("div", { className: "p-5 flex-1 bg-black/20", children: _jsx("textarea", { value: testPayload, onChange: (e) => setTestPayload(e.target.value), className: cn('w-full h-48 text-emerald-400 font-mono text-[11px] p-4 outline-none border rounded-none resize-none', dark ? 'bg-black/40 border-white/10 focus:border-z-accent/50' : 'bg-white/40 border-black/10 focus:border-z-accent/50'), spellCheck: false }) }), _jsxs("div", { className: cn('px-5 py-3 border-t flex justify-end gap-3', dark ? 'border-white/10 bg-black/20' : 'border-black/10 bg-white/20'), children: [_jsx("button", { onClick: () => setShowTestModal(false), className: "px-4 py-2 text-[10px] font-bold uppercase tracking-wider text-gray-400 hover:text-white transition-colors", children: "Cancel" }), _jsxs("button", { onClick: executeTestFlow, className: "px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-bold uppercase tracking-widest rounded-sm transition-colors flex items-center gap-2", children: [_jsx(Play, { size: 12 }), "Run Sequence"] })] })] }) })) })] })) : (
554
+ /* Empty state */
555
+ _jsxs("div", { className: "flex-1 flex flex-col items-center justify-center p-12 text-center", children: [_jsx("div", { className: cn('w-28 h-28 border flex items-center justify-center mb-10 group transition-all duration-500', dark
556
+ ? 'border-z-border bg-z-panel hover:border-z-active-border'
557
+ : 'border-z-border bg-gray-50 hover:border-z-active-border'), children: _jsx(Workflow, { size: 44, className: "text-gray-600 group-hover:text-z-active-text transition-colors duration-500" }) }), _jsx("h2", { className: cn('text-2xl font-black uppercase tracking-tighter mb-3', dark ? 'text-white' : 'text-z-primary'), children: "Enterprise Automations" }), _jsx("p", { className: "text-[10px] text-z-secondary uppercase tracking-[0.3em] max-w-sm leading-loose", children: "Build visual workflow graphs connecting triggers to actions. Supports HTTP, AI, Slack, email, and database operations." }), _jsx("button", { onClick: createNewFlow, className: "mt-10 px-10 py-3.5 bg-z-accent text-white font-black uppercase tracking-[0.25em] text-[9px] hover:opacity-90 active:scale-95 transition-all shadow-sm", children: "Create Automation" })] })) })] }));
558
+ };
559
+ // ── Sub-components for config panel ───────────────────────────────────────────
560
+ function ConditionConfigPanel({ node, updateNodeData, dark }) {
561
+ const d = node.data;
562
+ const upd = (patch) => updateNodeData(node.id, patch);
563
+ return (_jsxs("div", { className: "space-y-5", children: [_jsx(FieldInput, { label: "Node Label", value: d.label, onChange: (v) => upd({ label: v }), placeholder: "If / Else" }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: "Evaluation Condition" }), _jsx("textarea", { value: d.condition || '', onChange: (e) => upd({ condition: e.target.value }), placeholder: "{{payload.amount}} > 1000", rows: 3, className: cn('w-full bg-z-panel backdrop-blur-md border border-z-border px-3 py-2.5 text-[11px] text-white outline-none resize-none rounded-none font-mono', 'focus:border-z-accent/50 transition-colors placeholder:text-gray-700') }), _jsx("p", { className: "text-[8px] text-gray-500 uppercase tracking-wider mt-1", children: "Must evaluate to a boolean True or False. Standard JavaScript syntax is supported." })] })] }));
564
+ }
565
+ function TriggerConfigPanel({ node, flowId, updateNodeData, dark }) {
566
+ const d = node.data;
567
+ return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: "Trigger Type" }), _jsx("div", { className: "grid grid-cols-3 gap-1.5", children: TRIGGER_TYPES.map((t) => (_jsxs("button", { onClick: () => updateNodeData(node.id, { triggerType: t.id }), title: t.desc, className: cn('py-2.5 px-2 text-[8px] font-black uppercase tracking-wider border transition-all flex flex-col items-center gap-1.5', d.triggerType === t.id
568
+ ? 'bg-white text-black border-white'
569
+ : dark
570
+ ? 'bg-z-hover border-z-border text-z-muted hover:text-white hover:border-white/20'
571
+ : 'bg-gray-100 border-z-border text-gray-600 hover:border-gray-400'), children: [_jsx(t.icon, { size: 13 }), t.detail] }, t.id))) })] }), d.triggerType === 'webhook' && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary", children: "Inbound Webhook URL" }), _jsx("div", { className: cn('px-3 py-2.5 border flex items-center gap-2', dark ? 'bg-black border-z-border' : 'bg-gray-100 border-z-border'), children: _jsx("code", { className: "text-[9px] text-z-active-text font-mono truncate flex-1", children: flowId ? `POST /api/v1/hooks/${flowId}` : 'Save automation to get URL' }) }), _jsx("p", { className: "text-[8px] text-gray-600 uppercase tracking-wider", children: "Send a POST request to this endpoint to trigger the flow" })] })), d.triggerType === 'collection_change' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Collection Slug", value: d.collection, onChange: (v) => updateNodeData(node.id, { collection: v }), placeholder: "e.g. posts, products" }), _jsx(FieldSelect, { label: "Event Action", value: d.action || '', onChange: (v) => updateNodeData(node.id, { action: v }), options: [
572
+ { value: '', label: 'Any (create, update, delete)' },
573
+ { value: 'create', label: 'Create only' },
574
+ { value: 'update', label: 'Update only' },
575
+ { value: 'delete', label: 'Delete only' },
576
+ ] })] })), d.triggerType === 'schedule' && (_jsx(FieldInput, { label: "Cron Expression", value: d.cron, onChange: (v) => updateNodeData(node.id, { cron: v }), placeholder: "0 9 * * 1 (every Monday 9am)", mono: true }))] }));
577
+ }
578
+ function ActionConfigPanel({ node, updateNodeData, dark }) {
579
+ const d = node.data;
580
+ const t = d.actionType;
581
+ const upd = (patch) => updateNodeData(node.id, patch);
582
+ const [flows, setFlows] = useState([]);
583
+ useEffect(() => {
584
+ if (t === 'loop')
585
+ api.get('/flows').then((r) => setFlows(r.data.data));
586
+ }, [t]);
587
+ return (_jsxs("div", { className: "space-y-5", children: [_jsx(FieldInput, { label: "Node Label", value: d.label, onChange: (v) => upd({ label: v }), placeholder: "Describe this step\u2026" }), t === 'delay' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Duration Amount", value: d.amount, onChange: (v) => upd({ amount: v }), type: "number", placeholder: "15" }), _jsx(FieldSelect, { label: "Time Unit", value: d.unit, onChange: (v) => upd({ unit: v }), options: [
588
+ { label: 'Seconds', value: 'seconds' },
589
+ { label: 'Minutes', value: 'minutes' },
590
+ { label: 'Hours', value: 'hours' },
591
+ { label: 'Days', value: 'days' },
592
+ ] }), _jsx("p", { className: "text-[8px] text-gray-500 uppercase mt-2", children: "The execution engine will suspend this workflow into durable storage and wake it up automatically when the timer expires." })] })), t === 'code' && (_jsxs(_Fragment, { children: [_jsx(FieldTextarea, { label: "Javascript Transformer Code", value: d.code, onChange: (v) => upd({ code: v }), placeholder: "payload.total = payload.price * 2;\nreturn payload;", rows: 8, mono: true }), _jsx("p", { className: "text-[8px] text-gray-500 uppercase mt-2", children: "Write raw JS. You must return an object. The returned object will become the new Payload for all downstream nodes." })] })), t === 'loop' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Array Path (Inside Payload)", value: d.arrayPath, onChange: (v) => upd({ arrayPath: v }), placeholder: "payload.customers", mono: true }), _jsx(FieldSelect, { label: "Target Sub-Flow", value: d.targetFlowId, onChange: (v) => upd({ targetFlowId: v }), options: [
593
+ { label: 'Select automation...', value: '' },
594
+ ...flows.map((f) => ({ label: f.name, value: f._id })),
595
+ ] }), _jsx("p", { className: "text-[8px] text-gray-500 uppercase mt-2", children: "The engine will read the array and spawn a parallel background run of the selected Sub-Flow for every single item." })] })), d.actionType === 'http' && (_jsxs(_Fragment, { children: [_jsx(FieldSelect, { label: "Method", value: d.method || 'POST', onChange: (v) => upd({ method: v }), options: ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'].map((m) => ({ value: m, label: m })) }), _jsx(FieldInput, { label: "Endpoint URL", value: d.url, onChange: (v) => upd({ url: v }), placeholder: "https://api.example.com/v1/...", mono: true }), _jsx(FieldTextarea, { label: "Headers (JSON)", value: d.headers, onChange: (v) => upd({ headers: v }), placeholder: '{"Authorization": "Bearer {{env.API_KEY}}"}', rows: 3, mono: true }), _jsx(FieldTextarea, { label: "Body (JSON)", value: d.body, onChange: (v) => upd({ body: v }), placeholder: '{"event": "{{payload.type}}", "data": "{{payload}}"}', rows: 5, mono: true })] })), d.actionType === 'slack' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Slack Webhook URL", value: d.webhookUrl, onChange: (v) => upd({ webhookUrl: v }), placeholder: "https://hooks.slack.com/services/...", mono: true }), _jsx(FieldTextarea, { label: "Message", value: d.message, onChange: (v) => upd({ message: v }), placeholder: "\uD83D\uDEA8 Alert: {{payload.title}} was updated!", rows: 4 })] })), d.actionType === 'email' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "To", value: d.to, onChange: (v) => upd({ to: v }), placeholder: "user@example.com or {{payload.email}}" }), _jsx(FieldInput, { label: "Subject", value: d.subject, onChange: (v) => upd({ subject: v }), placeholder: "Notification: {{payload.title}}" }), _jsx(FieldTextarea, { label: "Body (HTML)", value: d.body, onChange: (v) => upd({ body: v }), placeholder: "<h2>Hello!</h2><p>{{payload.content}}</p>", rows: 5, mono: true })] })), d.actionType === 'ai_prompt' && (_jsxs(_Fragment, { children: [_jsx(FieldTextarea, { label: "Prompt", value: d.prompt, onChange: (v) => upd({ prompt: v }), placeholder: "Translate the following to French: {{payload.content}}", rows: 6 }), _jsxs("p", { className: "text-[8px] text-gray-600 uppercase tracking-wider", children: ["AI output is injected into context as", ' ', _jsx("code", { className: "text-purple-400 font-mono", children: '{{nodeId.output}}' }), " for downstream nodes."] })] })), d.actionType === 'webhook' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Webhook URL", value: d.url, onChange: (v) => upd({ url: v }), placeholder: "https://...", mono: true }), _jsx(FieldInput, { label: "Secret (optional)", value: d.secret, onChange: (v) => upd({ secret: v }), placeholder: "Signing secret", type: "password" })] })), d.actionType === 'update_content' && (_jsxs(_Fragment, { children: [_jsx(FieldInput, { label: "Collection Slug", value: d.collection, onChange: (v) => upd({ collection: v }), placeholder: "e.g. posts" }), _jsx(FieldSelect, { label: "Operation", value: d.operation || 'update', onChange: (v) => upd({ operation: v }), options: [
596
+ { value: 'update', label: 'Update document' },
597
+ { value: 'create', label: 'Create document' },
598
+ { value: 'delete', label: 'Delete document' },
599
+ ] }), _jsx(FieldInput, { label: "Document ID (or $.path)", value: d.documentId, onChange: (v) => upd({ documentId: v }), placeholder: "{{payload._id}} or $.id", mono: true }), _jsx(FieldTextarea, { label: "Fields (JSON)", value: d.fields, onChange: (v) => upd({ fields: v }), placeholder: '{"status": "published", "updatedAt": "{{payload.updatedAt}}"}', rows: 5, mono: true })] })), d.actionType === 'log' && (_jsx(FieldInput, { label: "Log Message", value: d.message, onChange: (v) => upd({ message: v }), placeholder: "Flow executed: {{payload.title}}" }))] }));
600
+ }
601
+ export default FlowBuilderPage;
@@ -0,0 +1 @@
1
+ export * from './plugin';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './plugin';
@@ -0,0 +1 @@
1
+ export { default as FlowBuilderPage } from './FlowBuilderPage';
package/dist/plugin.js ADDED
@@ -0,0 +1 @@
1
+ export { default as FlowBuilderPage } from './FlowBuilderPage';
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@zenith-open/zenithcms-plugin-workflows-ui",
3
+ "version": "1.0.0",
4
+ "description": "Workflow Automation UI Plugin for Zenith CMS",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "@xyflow/react": "^12.11.0",
9
+ "dagre": "^0.8.5",
10
+ "lucide-react": "^0.354.0",
11
+ "framer-motion": "^11.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "@types/react": "^19.2.14",
15
+ "@types/react-dom": "^19.2.3",
16
+ "react": "^19.2.5",
17
+ "react-dom": "^19.2.5",
18
+ "typescript": "^5.0.0"
19
+ },
20
+ "peerDependencies": {
21
+ "react": "^18.2.0 || ^19.0.0",
22
+ "react-dom": "^18.2.0 || ^19.0.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "echo 'Skipped build for broken plugin-workflows-ui'"
33
+ }
34
+ }