diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 67b8bf71d9..337325b0a7 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -210,6 +210,7 @@ function TemplateCardInner({ isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index d0f0bead47..41ae0ba933 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -211,6 +211,7 @@ function TemplateCardInner({ isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx new file mode 100644 index 0000000000..5a8cfec4c5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx @@ -0,0 +1,133 @@ +'use client' + +import { memo, useMemo } from 'react' +import { Handle, type NodeProps, Position } from 'reactflow' +import { getBlock } from '@/blocks/registry' + +interface WorkflowPreviewBlockData { + type: string + name: string + isTrigger?: boolean + horizontalHandles?: boolean + enabled?: boolean +} + +/** + * Lightweight block component for workflow previews. + * Renders block header, dummy subblocks skeleton, and handles. + * Respects horizontalHandles and enabled state from workflow. + * No heavy hooks, store subscriptions, or interactive features. + * Used in template cards and other preview contexts for performance. + */ +function WorkflowPreviewBlockInner({ data }: NodeProps) { + const { type, name, isTrigger = false, horizontalHandles = false, enabled = true } = data + + const blockConfig = getBlock(type) + if (!blockConfig) { + return null + } + + const IconComponent = blockConfig.icon + // Hide input handle for triggers, starters, or blocks in trigger mode + const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger + + // Get visible subblocks from config (no fetching, just config structure) + const visibleSubBlocks = useMemo(() => { + if (!blockConfig.subBlocks) return [] + + return blockConfig.subBlocks.filter((subBlock) => { + if (subBlock.hidden) return false + if (subBlock.hideFromPreview) return false + if (subBlock.mode === 'trigger') return false + if (subBlock.mode === 'advanced') return false + return true + }) + }, [blockConfig.subBlocks]) + + const hasSubBlocks = visibleSubBlocks.length > 0 + const showErrorRow = !isStarterOrTrigger + + // Handle styles based on orientation + const horizontalHandleClass = '!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !rounded-[2px]' + const verticalHandleClass = '!border-none !bg-[var(--surface-12)] !h-[7px] !w-5 !rounded-[2px]' + + return ( +
+ {/* Target handle - not shown for triggers/starters */} + {!isStarterOrTrigger && ( + + )} + + {/* Header */} +
+
+ +
+ + {name} + +
+ + {/* Subblocks skeleton */} + {(hasSubBlocks || showErrorRow) && ( +
+ {visibleSubBlocks.slice(0, 4).map((subBlock) => ( +
+ + {subBlock.title ?? subBlock.id} + + - +
+ ))} + {visibleSubBlocks.length > 4 && ( +
+ + +{visibleSubBlocks.length - 4} more + +
+ )} + {showErrorRow && ( +
+ + error + +
+ )} +
+ )} + + {/* Source handle */} + +
+ ) +} + +export const WorkflowPreviewBlock = memo(WorkflowPreviewBlockInner) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx new file mode 100644 index 0000000000..d58d04b5df --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx @@ -0,0 +1,90 @@ +'use client' + +import { memo } from 'react' +import { RepeatIcon, SplitIcon } from 'lucide-react' +import { Handle, type NodeProps, Position } from 'reactflow' + +interface WorkflowPreviewSubflowData { + name: string + width?: number + height?: number + kind: 'loop' | 'parallel' +} + +/** + * Lightweight subflow component for workflow previews. + * Matches the styling of the actual SubflowNodeComponent but without + * hooks, store subscriptions, or interactive features. + * Used in template cards and other preview contexts for performance. + */ +function WorkflowPreviewSubflowInner({ data }: NodeProps) { + const { name, width = 500, height = 300, kind } = data + + const isLoop = kind === 'loop' + const BlockIcon = isLoop ? RepeatIcon : SplitIcon + const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B' + const blockName = name || (isLoop ? 'Loop' : 'Parallel') + + // Handle IDs matching the actual subflow component + const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source' + const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source' + + // Handle styles matching the actual subflow component + const handleClass = + '!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-[2px]' + + return ( +
+ {/* Target handle on left (input to the subflow) */} + + + {/* Header - matches actual subflow header */} +
+
+ +
+ + {blockName} + +
+ + {/* Start handle inside - connects to first block in subflow */} +
+ Start + +
+ + {/* End source handle on right (output from the subflow) */} + +
+ ) +} + +export const WorkflowPreviewSubflow = memo(WorkflowPreviewSubflowInner) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx index ca4674596f..32b605f440 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx @@ -1,7 +1,6 @@ 'use client' import { useMemo } from 'react' -import { cloneDeep } from 'lodash' import ReactFlow, { ConnectionLineType, type Edge, @@ -18,6 +17,8 @@ import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' +import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block' +import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow' import { getBlock } from '@/blocks' import type { WorkflowState } from '@/stores/workflows/workflow/types' @@ -34,15 +35,29 @@ interface WorkflowPreviewProps { defaultZoom?: number fitPadding?: number onNodeClick?: (blockId: string, mousePosition: { x: number; y: number }) => void + /** Use lightweight blocks for better performance in template cards */ + lightweight?: boolean } -// Define node types - the components now handle preview mode internally -const nodeTypes: NodeTypes = { +/** + * Full node types with interactive WorkflowBlock for detailed previews + */ +const fullNodeTypes: NodeTypes = { workflowBlock: WorkflowBlock, noteBlock: NoteBlock, subflowNode: SubflowNodeComponent, } +/** + * Lightweight node types for template cards and other high-volume previews. + * Uses minimal components without hooks or store subscriptions. + */ +const lightweightNodeTypes: NodeTypes = { + workflowBlock: WorkflowPreviewBlock, + noteBlock: WorkflowPreviewBlock, + subflowNode: WorkflowPreviewSubflow, +} + // Define edge types const edgeTypes: EdgeTypes = { default: WorkflowEdge, @@ -59,7 +74,10 @@ export function WorkflowPreview({ defaultZoom = 0.8, fitPadding = 0.25, onNodeClick, + lightweight = false, }: WorkflowPreviewProps) { + // Use lightweight node types for better performance in template cards + const nodeTypes = lightweight ? lightweightNodeTypes : fullNodeTypes // Check if the workflow state is valid const isValidWorkflowState = workflowState?.blocks && workflowState.edges @@ -130,6 +148,43 @@ export function WorkflowPreview({ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks) + // Lightweight mode: create minimal node data for performance + if (lightweight) { + // Handle loops and parallels as subflow nodes + if (block.type === 'loop' || block.type === 'parallel') { + nodeArray.push({ + id: blockId, + type: 'subflowNode', + position: absolutePosition, + draggable: false, + data: { + name: block.name, + width: block.data?.width || 500, + height: block.data?.height || 300, + kind: block.type as 'loop' | 'parallel', + }, + }) + return + } + + // Regular blocks + nodeArray.push({ + id: blockId, + type: 'workflowBlock', + position: absolutePosition, + draggable: false, + data: { + type: block.type, + name: block.name, + isTrigger: block.triggerMode === true, + horizontalHandles: block.horizontalHandles ?? false, + enabled: block.enabled ?? true, + }, + }) + return + } + + // Full mode: create detailed node data for interactive previews if (block.type === 'loop') { nodeArray.push({ id: block.id, @@ -178,8 +233,6 @@ export function WorkflowPreview({ return } - const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {} - const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' nodeArray.push({ @@ -194,7 +247,7 @@ export function WorkflowPreview({ blockState: block, canEdit: false, isPreview: true, - subBlockValues: subBlocksClone, + subBlockValues: block.subBlocks ?? {}, }, }) @@ -242,6 +295,7 @@ export function WorkflowPreview({ showSubBlocks, workflowState.blocks, isValidWorkflowState, + lightweight, ]) const edges: Edge[] = useMemo(() => {