'use client'; import React, { useCallback, useRef, DragEvent, useMemo, useState, useEffect } from 'react'; import { ReactFlow, MiniMap, Background, Connection, Node, ReactFlowProvider, useReactFlow, OnSelectionChangeParams, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useHotkeys } from 'react-hotkeys-hook'; import { useNodesStore } from '@/stores/nodes-store'; import useFlowStore, { useFlowStoreActions } from '@/stores/flow-store'; import { useEditorStore } from '@/stores/editor-store'; import { useDesignStore } from '@/stores/design-store'; import { useSidebarStore } from '@/stores/sidebar-store'; import { useTemplateStore } from '@/stores/template-store'; import * as Tooltip from '@radix-ui/react-tooltip'; import { SendToBack } from 'lucide-react'; import AnimatedMessageEdge from '@/components/core/designer/edges/animated-messages/edge'; import { getNodeId } from '@/utils/react-flow'; import FlowControls from '@/components/core/designer/common/FlowControls'; import TemplateSelector from '@/modals/template-selector'; import SettingsModal from '@/modals/settings-modal'; import AutoLayoutModal from '@/modals/auto-layout-modal'; function FlowContent({ onAutoLayoutClick }: { onAutoLayoutClick: () => void }) { const reactFlowWrapper = useRef(null); const reactFlowInstance = useReactFlow(); const { screenToFlowPosition, getNodes } = useReactFlow(); const [lastSelection, setLastSelection] = useState(null); const [lastCopiedSelection, setLastCopiedSelection] = useState(null); const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null); const [cursorPosition, setCursorPosition] = useState<{ x: number; y: number } | null>(null); const { nodes: applicationNodes } = useNodesStore(); const { nodes: allNodes, edges } = useFlowStore(); const { addNode, onNodesChange, onConnect, duplicateNode, setReactFlowInstance, undo, redo, exportFlow, importFlow, setEdges } = useFlowStoreActions(); useEffect(() => { setReactFlowInstance(reactFlowInstance); }, [reactFlowInstance]); const { selectedNode, setSelectedNode } = useEditorStore(); const { currentDesign, updateDesignName } = useDesignStore(); const { toggleSidebar, showMinimap, showComments, selectedComponent, setSelectedComponent } = useSidebarStore(); const { setShowTemplateSelector } = useTemplateStore(); // Filter nodes based on comment visibility const nodes = React.useMemo(() => { if (showComments) { return allNodes; } // Hide comment thread nodes when showComments is false return allNodes.filter(node => node.type !== 'commentThread'); }, [allNodes, showComments]); function handleCopy(e: KeyboardEvent) { // const multipleSelection = lastSelection?.nodes // ? lastSelection?.nodes.length > 0 // : false; e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); if (window.getSelection()?.toString().length === 0 && lastSelection) { setLastCopiedSelection(lastSelection); } } function handlePaste(e: KeyboardEvent) { if (true) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); if ( window.getSelection()?.toString().length === 0 && lastCopiedSelection ) { // For each node int he array call the store to add the node for (const node of lastCopiedSelection?.nodes ?? []) { duplicateNode(node.id); } } } } function handleOpenDesigns(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); openFromFile(); } const openFromFile = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.ecstudio'; fileInput.onchange = (e) => { const target = e.target as HTMLInputElement; const file = target?.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target?.result as string); importFlow(data); // Update the current design name based on metadata (root level or nested) or filename const designName = data.name || data.metadata?.name || file.name.replace('.ecstudio', ''); if (currentDesign?.id) { updateDesignName(currentDesign.id, designName); } } catch (error) { console.error('Error parsing file:', error); alert('Error: Invalid file format. Please select a valid .ecstudio file.'); } } reader.readAsText(file); } } fileInput.click(); } function handleUndo(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); undo(); } function handleRedo(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); redo(); } function handleToggleSidebar(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); toggleSidebar(); } function handleSelectAll(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); // Select all nodes const nodeUpdates = nodes.map(node => ({ id: node.id, type: 'select' as const, selected: true })); onNodesChange(nodeUpdates); } function handleSaveToFile(e: KeyboardEvent) { e.preventDefault(); (e as unknown as Event).stopImmediatePropagation(); const data = exportFlow(); // Check if the browser supports the File System Access API if ('showSaveFilePicker' in window) { // Modern browsers with File System Access API const saveFileWithPicker = async () => { try { const fileHandle = await (window as any).showSaveFilePicker({ suggestedName: `${currentDesign?.name || 'design'}.ecstudio`, types: [{ description: 'EventCatalog Studio files', accept: { 'application/json': ['.ecstudio'] } }] }); const writable = await fileHandle.createWritable(); await writable.write(JSON.stringify(data, null, 2)); await writable.close(); } catch (error) { // User cancelled or other error if (error instanceof Error && error.name !== 'AbortError') { console.error('Error saving file:', error); } } }; saveFileWithPicker(); } else { // Fallback for browsers without File System Access API 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 = `${currentDesign?.name || 'design'}.ecstudio`; a.click(); URL.revokeObjectURL(url); } } // @ts-ignore useHotkeys('mod+c', handleCopy); // @ts-ignore useHotkeys('mod+v', handlePaste); // @ts-ignore useHotkeys('mod+o', handleOpenDesigns); // @ts-ignore useHotkeys('mod+z', handleUndo); // @ts-ignore useHotkeys('mod+shift+z', handleRedo); // @ts-ignore useHotkeys('mod+b', handleToggleSidebar); // @ts-ignore useHotkeys('mod+a', handleSelectAll); // @ts-ignore useHotkeys('mod+s', handleSaveToFile); // @ts-ignore useHotkeys('mod+n', () => setShowTemplateSelector(true)); const nodeTypes = applicationNodes.reduce((acc: any, node: any) => { acc[node.type] = node.component; return acc; }, {}); const edgeTypes = useMemo(() => ({ animatedMessage: AnimatedMessageEdge, }), []); const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); const onDrop = useCallback( (event: DragEvent) => { event.preventDefault(); const dataString = event.dataTransfer.getData('application/reactflow'); if (!dataString) return; const { nodeType, label, data } = JSON.parse(dataString); const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); const newNode: Node = { id: getNodeId(nodeType), type: nodeType, position, data: data ?? { label }, }; addNode(newNode); }, [screenToFlowPosition, getNodes] ); const handleNodesChange = useCallback((changes: any) => { onNodesChange(changes); }, [onNodesChange]); const handleConnect = useCallback((connection: Connection) => { onConnect(connection); }, [onConnect]); const onEdgesChange = useCallback((changes: any) => { // Handle edge changes if needed }, []); const onSelectionChange = useCallback( (flow: OnSelectionChangeParams): void => { const isGroupSelection = flow.nodes.length > 1; setLastSelection(flow); // if (!isGroupSelection) { // setSelectedNode(flow.nodes[0]); // } }, [], ); const onPaneClick = useCallback((event: React.MouseEvent) => { const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); // If a component is selected, place it at click position if (selectedComponent) { const newNode: Node = { id: getNodeId(selectedComponent.nodeType), type: selectedComponent.nodeType, position, data: { ...selectedComponent.data, label: selectedComponent.label }, }; addNode(newNode); setSelectedComponent(null); // Clear selection after placing return; } if(selectedNode) { setSelectedNode(null); reactFlowInstance?.fitView({ duration: 1000 }); } }, [setSelectedNode, reactFlowInstance, selectedNode, selectedComponent, setSelectedComponent, screenToFlowPosition, addNode]); const handleContextMenu = useCallback((event: React.MouseEvent) => { event.preventDefault(); const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); setContextMenuPosition(position); }, [screenToFlowPosition]); const handleMouseMove = useCallback((event: React.MouseEvent) => { if (selectedComponent) { setCursorPosition({ x: event.clientX, y: event.clientY }); } else { setCursorPosition(null); } }, [selectedComponent]); const handleAutoLayoutClick = () => { onAutoLayoutClick(); }; return (
{/* Auto Layout Button - Top Left */}
Reorder Diagram
{/* */} {showMinimap && } {/* */} {/* Cursor Preview */} {selectedComponent && cursorPosition && (
{selectedComponent.iconComponent && ( )} {selectedComponent.label}
)}
); } export default function App() { const [showAutoLayoutDialog, setShowAutoLayoutDialog] = useState(false); const { autoLayout } = useFlowStoreActions(); const handleAutoLayoutClick = () => { setShowAutoLayoutDialog(true); }; return ( setShowAutoLayoutDialog(false)} onConfirm={autoLayout} /> ); }