From ff4fee0cb8a9eb283a610e35ca6fd3bc9f6e253a Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 10 Feb 2025 19:42:37 -0800 Subject: [PATCH] Added loop block to workflow canvas; need to fix height calculation --- .../components/custom-edge/custom-edge.tsx | 6 +- .../workflow-canvas/workflow-canvas.tsx | 94 +++++++++++++++---- .../workflow-loop/workflow-loop.tsx | 71 ++++++++++++++ 3 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 app/w/[id]/components/workflow-loop/workflow-loop.tsx diff --git a/app/w/[id]/components/custom-edge/custom-edge.tsx b/app/w/[id]/components/custom-edge/custom-edge.tsx index f07bca46a..73429527c 100644 --- a/app/w/[id]/components/custom-edge/custom-edge.tsx +++ b/app/w/[id]/components/custom-edge/custom-edge.tsx @@ -20,13 +20,14 @@ export const CustomEdge = (props: EdgeProps) => { const isSelected = props.id === props.data?.selectedEdgeId return ( - + { x={labelX - 12} y={labelY - 12} className="overflow-visible" + style={{ zIndex: 999 }} >
{ e.stopPropagation() props.data?.onDelete?.(props.id) diff --git a/app/w/[id]/components/workflow-canvas/workflow-canvas.tsx b/app/w/[id]/components/workflow-canvas/workflow-canvas.tsx index 846b8c320..b0c98fccd 100644 --- a/app/w/[id]/components/workflow-canvas/workflow-canvas.tsx +++ b/app/w/[id]/components/workflow-canvas/workflow-canvas.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import ReactFlow, { Background, ConnectionLineType, @@ -16,6 +16,7 @@ import { NotificationList } from '@/app/w/components/notifications/notifications import { getBlock } from '../../../../../blocks' import { useWorkflowExecution } from '../../../hooks/use-workflow-execution' import { CustomEdge } from '../custom-edge/custom-edge' +import { createLoopNode, getRelativeLoopPosition } from '../workflow-loop/workflow-loop' import { WorkflowNode } from '../workflow-node/workflow-node' // Define custom node and edge types for ReactFlow @@ -45,31 +46,90 @@ export function WorkflowCanvas() { } = useWorkflowStore() const { addNotification } = useNotificationStore() const { project, setViewport } = useReactFlow() + const { loops } = useWorkflowStore() - // Transform blocks into ReactFlow node format - const nodes = Object.values(blocks).map((block) => ({ - id: block.id, - type: 'workflowBlock', - position: block.position, - selected: block.id === selectedBlockId, - dragHandle: '.workflow-drag-handle', - data: { - type: block.type, - config: getBlock(block.type), - name: block.name, - }, - })) + // Transform blocks and loops into ReactFlow node format + const nodes = useMemo(() => { + const nodeArray: any[] = [] - // Handle node position updates during drag operations + // Add loop group nodes first + Object.entries(loops).forEach(([loopId, loop]) => { + const loopNode = createLoopNode({ loopId, loop, blocks }) + if (loopNode) { + nodeArray.push(loopNode) + } + }) + + // Add block nodes with relative positions if they're in a loop + Object.entries(blocks).forEach(([blockId, block]) => { + // Skip loop position entries that don't have proper block structure + if (!block.type || !block.name) { + console.log('Skipping invalid block:', blockId, block) + return + } + + // Get block configuration + const blockConfig = getBlock(block.type) + if (!blockConfig) { + console.error(`No configuration found for block type: ${block.type}`) + return + } + + // Find if block belongs to any loop + const parentLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(block.id)) + + let position = block.position + if (parentLoop) { + const [loopId] = parentLoop + const loopNode = nodeArray.find((node) => node.id === `loop-${loopId}`) + if (loopNode) { + position = getRelativeLoopPosition(block.position, loopNode.position) + } + } + + nodeArray.push({ + id: block.id, + type: 'workflowBlock', + position, + parentId: parentLoop ? `loop-${parentLoop[0]}` : undefined, + dragHandle: '.workflow-drag-handle', + selected: block.id === selectedBlockId, + data: { + type: block.type, + config: blockConfig, + name: block.name, + }, + }) + }) + + return nodeArray + }, [blocks, loops, selectedBlockId]) + + // Update node position handler const onNodesChange = useCallback( (changes: any) => { changes.forEach((change: any) => { if (change.type === 'position' && change.position) { - updateBlockPosition(change.id, change.position) + const node = nodes.find((n) => n.id === change.id) + if (!node) return + + // If node is part of a loop, convert position back to absolute + if (node.parentId) { + const loopNode = nodes.find((n) => n.id === node.parentId) + if (loopNode) { + const absolutePosition = { + x: change.position.x + loopNode.position.x, + y: change.position.y + loopNode.position.y, + } + updateBlockPosition(change.id, absolutePosition) + } + } else { + updateBlockPosition(change.id, change.position) + } } }) }, - [updateBlockPosition] + [nodes, updateBlockPosition] ) // Handle edge removal and updates diff --git a/app/w/[id]/components/workflow-loop/workflow-loop.tsx b/app/w/[id]/components/workflow-loop/workflow-loop.tsx new file mode 100644 index 000000000..18bda5c36 --- /dev/null +++ b/app/w/[id]/components/workflow-loop/workflow-loop.tsx @@ -0,0 +1,71 @@ +import { Loop } from '@/stores/workflow/types' + +interface WorkflowLoopProps { + loopId: string + loop: Loop + blocks: Record +} + +// Pure calculation function - no hooks +function calculateLoopBounds(loop: Loop, blocks: Record) { + // Get all blocks in this loop + const loopBlocks = loop.nodes.map((id) => blocks[id]) + if (!loopBlocks.length) return null + + // Calculate bounds of all blocks in loop + const bound = loopBlocks.reduce( + (acc, block) => { + acc.minX = Math.min(acc.minX, block.position.x) + acc.minY = Math.min(acc.minY, block.position.y) + acc.maxX = Math.max(acc.maxX, block.position.x + (block.isWide ? 480 : 320)) + acc.maxY = Math.max(acc.maxY, block.position.y + 200) + return acc + }, + { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + ) + + // Add padding around the group + const PADDING = 50 + return { + x: bound.minX - PADDING, + y: bound.minY - PADDING, + width: bound.maxX - bound.minX + PADDING * 2, + height: bound.maxY - bound.minY + PADDING * 2, + } +} + +// Helper function to create loop node +export function createLoopNode({ loopId, loop, blocks }: WorkflowLoopProps) { + const loopBounds = calculateLoopBounds(loop, blocks) + if (!loopBounds) return null + + return { + id: `loop-${loopId}`, + type: 'group', + position: { x: loopBounds.x, y: loopBounds.y }, + style: { + backgroundColor: 'rgb(247, 247, 248)', + border: '1px solid rgb(203, 213, 225)', + borderRadius: '12px', + width: loopBounds.width, + height: loopBounds.height, + pointerEvents: 'none', + zIndex: -1, + isolation: 'isolate', + }, + data: { + label: 'Loop', + }, + } +} + +// Helper function to calculate relative position for child blocks +export function getRelativeLoopPosition( + blockPosition: { x: number; y: number }, + loopBounds: { x: number; y: number } +) { + return { + x: blockPosition.x - loopBounds.x, + y: blockPosition.y - loopBounds.y, + } +}