import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import WorkflowEditorComponent from './components/workflow-editor/WorkflowEditorComponent'; import WorkflowFormComponent from './components/workflow-form/WorkflowFormComponent'; import WorkflowExecutionsTab from './components/workflow-executions/WorkflowExecutionsTab'; import WorkflowDebugDialog from './components/workflow-debug-dialog/WorkflowDebugDialog'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useTranslation } from 'react-i18next'; import { Settings, Play, BarChart3, GitBranch, Download, Upload, Bug } from 'lucide-react'; import { backendClient } from '@/app/infra/http'; import { Workflow } from '@/app/infra/entities/api'; import { useWorkflowStore } from './store/useWorkflowStore'; import { toast } from 'sonner'; import EmojiPicker from '@/components/ui/emoji-picker'; export default function WorkflowDetailContent({ id }: { id: string }) { const isCreateMode = id === 'new'; const navigate = useNavigate(); const { t } = useTranslation(); const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData(); const { currentWorkflow, setCurrentWorkflow, fromWorkflowDefinition, toWorkflowDefinition, isDirty, setDirty, isSaving, setSaving, setLoading, reset, nodeTypes, setNodeTypes, } = useWorkflowStore(); const [activeTab, setActiveTab] = useState('editor'); const [workflow, setWorkflow] = useState(null); const [createStep, setCreateStep] = useState<'basic' | 'editor'>('basic'); const [basicInfo, setBasicInfo] = useState<{ name: string; description: string; emoji: string }>({ name: '', description: '', emoji: '🔄', }); const fileInputRef = useRef(null); const [isWebSocketConnected, setIsWebSocketConnected] = useState(false); // Set breadcrumb entity name useEffect(() => { if (isCreateMode) { setDetailEntityName(t('workflows.createWorkflow')); } else { const wf = workflows.find((w) => w.id === id); setDetailEntityName(wf?.name ?? id); } return () => setDetailEntityName(null); }, [id, isCreateMode, workflows, setDetailEntityName, t]); // Load node types useEffect(() => { if (nodeTypes.length === 0) { backendClient.getWorkflowNodeTypes().then((resp) => { setNodeTypes(resp.node_types, resp.categories); }).catch((err) => { console.error('Failed to load node types:', err); }); } }, [nodeTypes.length, setNodeTypes]); // Load workflow data useEffect(() => { if (isCreateMode) { reset(); setWorkflow(null); return; } setLoading(true); backendClient.getWorkflow(id).then((resp) => { setWorkflow(resp.workflow); setCurrentWorkflow(resp.workflow); fromWorkflowDefinition(resp.workflow.nodes || [], resp.workflow.edges || []); }).catch((err) => { console.error('Failed to load workflow:', err); toast.error(t('workflows.loadError')); }).finally(() => { setLoading(false); }); return () => { reset(); }; }, [id, isCreateMode]); // Save handler const handleSave = useCallback(async () => { if (isSaving) return; setSaving(true); try { const { nodes, edges } = toWorkflowDefinition(); if (isCreateMode) { const resp = await backendClient.createWorkflow({ name: basicInfo.name || t('workflows.newWorkflow'), description: basicInfo.description, emoji: basicInfo.emoji, nodes, edges, }); refreshWorkflows(); navigate(`/home/workflows?id=${encodeURIComponent(resp.uuid)}`); toast.success(t('workflows.createSuccess')); } else { await backendClient.updateWorkflow(id, { name: workflow?.name, emoji: workflow?.emoji, description: workflow?.description, nodes, edges, variables: workflow?.variables, settings: workflow?.settings, triggers: workflow?.triggers, }); setDirty(false); refreshWorkflows(); toast.success(t('workflows.saveSuccess')); } } catch (err) { console.error('Failed to save workflow:', err); toast.error(t('workflows.saveError')); } finally { setSaving(false); } }, [id, isCreateMode, workflow, isSaving, toWorkflowDefinition, refreshWorkflows, navigate, t, basicInfo]); // Export workflow handler const handleExport = useCallback(() => { const { nodes, edges } = toWorkflowDefinition(); const exportData = { name: workflow?.name || t('workflows.newWorkflow'), description: workflow?.description || '', emoji: workflow?.emoji || '🔄', nodes, edges, variables: workflow?.variables || {}, settings: workflow?.settings || {}, version: '1.0', exportedAt: new Date().toISOString(), }; 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 = `${workflow?.name || 'workflow'}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(t('workflows.exportSuccess')); }, [workflow, toWorkflowDefinition, t]); // Import workflow handler const handleImport = useCallback((file: File) => { const reader = new FileReader(); reader.onload = (e) => { try { const importData = JSON.parse(e.target?.result as string); // Validate imported data structure if (!importData.nodes || !Array.isArray(importData.nodes)) { throw new Error('Invalid workflow file: missing nodes'); } if (!importData.edges || !Array.isArray(importData.edges)) { throw new Error('Invalid workflow file: missing edges'); } // Validate each node has required fields const nodeIds = new Set(); for (const node of importData.nodes) { if (!node.id || !node.type) { throw new Error(`Invalid node: missing id or type`); } if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') { throw new Error(`Invalid node "${node.id}": missing or invalid position`); } nodeIds.add(node.id); } // Validate each edge has required fields and references existing nodes for (const edge of importData.edges) { if (!edge.id || !edge.source || !edge.target) { throw new Error(`Invalid edge: missing id, source, or target`); } if (!nodeIds.has(edge.source)) { throw new Error(`Edge "${edge.id}" references unknown source node "${edge.source}"`); } if (!nodeIds.has(edge.target)) { throw new Error(`Edge "${edge.id}" references unknown target node "${edge.target}"`); } } // Load nodes and edges into the store fromWorkflowDefinition(importData.nodes, importData.edges); // Update workflow metadata if available if (workflow && (importData.name || importData.description || importData.emoji)) { setWorkflow({ ...workflow, name: importData.name || workflow.name, description: importData.description || workflow.description, emoji: importData.emoji || workflow.emoji, variables: importData.variables || workflow.variables, settings: importData.settings || workflow.settings, }); } setDirty(true); toast.success(t('workflows.importSuccess')); } catch (error) { console.error('Failed to import workflow:', error); toast.error(t('workflows.importError')); } }; reader.readAsText(file); }, [workflow, fromWorkflowDefinition, setDirty, t]); // Handle file input change const handleFileChange = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { handleImport(file); // Reset file input e.target.value = ''; } }, [handleImport]); // Publish handler const handlePublish = useCallback(async () => { if (!workflow?.uuid) return; try { await backendClient.publishWorkflow(workflow.uuid); toast.success(t('workflows.publishSuccess')); refreshWorkflows(); } catch (err) { console.error('Failed to publish workflow:', err); toast.error(t('workflows.publishError')); } }, [workflow, refreshWorkflows, t]); // Delete handler const handleDelete = useCallback(async () => { if (!workflow?.uuid) return; try { await backendClient.deleteWorkflow(workflow.uuid); refreshWorkflows(); navigate('/home/workflows'); toast.success(t('workflows.deleteSuccess')); } catch (err) { console.error('Failed to delete workflow:', err); toast.error(t('workflows.deleteError')); } }, [workflow, refreshWorkflows, navigate, t]); // ==================== Create Mode ==================== if (isCreateMode && createStep === 'basic') { return (

{t('workflows.createWorkflow')}

{t('workflows.basicInfo')} {t('workflows.basicInfoDesc')}
setBasicInfo({ ...basicInfo, emoji })} /> setBasicInfo({ ...basicInfo, name: e.target.value })} placeholder={t('workflows.namePlaceholder')} className="flex-1" />