Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/sim/app/templates/components/template-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ function TemplateCardInner({
isPannable={false}
defaultZoom={0.8}
fitPadding={0.2}
lightweight
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ function TemplateCardInner({
isPannable={false}
defaultZoom={0.8}
fitPadding={0.2}
lightweight
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WorkflowPreviewBlockData>) {
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 (
<div className='relative w-[250px] select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'>
{/* Target handle - not shown for triggers/starters */}
{!isStarterOrTrigger && (
<Handle
type='target'
position={horizontalHandles ? Position.Left : Position.Top}
id='target'
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
style={
horizontalHandles
? { left: '-7px', top: '24px' }
: { top: '-7px', left: '50%', transform: 'translateX(-50%)' }
}
/>
)}

{/* Header */}
<div
className={`flex items-center gap-[10px] p-[8px] ${hasSubBlocks || showErrorRow ? 'border-[var(--divider)] border-b' : ''}`}
>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: enabled ? blockConfig.bgColor : 'gray' }}
>
<IconComponent className='h-[16px] w-[16px] text-white' />
</div>
<span
className={`truncate font-medium text-[16px] ${!enabled ? 'text-[#808080]' : ''}`}
title={name}
>
{name}
</span>
</div>

{/* Subblocks skeleton */}
{(hasSubBlocks || showErrorRow) && (
<div className='flex flex-col gap-[8px] p-[8px]'>
{visibleSubBlocks.slice(0, 4).map((subBlock) => (
<div key={subBlock.id} className='flex items-center gap-[8px]'>
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
{subBlock.title ?? subBlock.id}
</span>
<span className='flex-1 truncate text-right text-[14px] text-[var(--white)]'>-</span>
</div>
))}
{visibleSubBlocks.length > 4 && (
<div className='flex items-center gap-[8px]'>
<span className='text-[14px] text-[var(--text-tertiary)]'>
+{visibleSubBlocks.length - 4} more
</span>
</div>
)}
{showErrorRow && (
<div className='flex items-center gap-[8px]'>
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
error
</span>
</div>
)}
</div>
)}

{/* Source handle */}
<Handle
type='source'
position={horizontalHandles ? Position.Right : Position.Bottom}
id='source'
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
style={
horizontalHandles
? { right: '-7px', top: '24px' }
: { bottom: '-7px', left: '50%', transform: 'translateX(-50%)' }
}
/>
</div>
)
}

export const WorkflowPreviewBlock = memo(WorkflowPreviewBlockInner)
Original file line number Diff line number Diff line change
@@ -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<WorkflowPreviewSubflowData>) {
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 (
<div
className='relative select-none rounded-[8px] border border-[var(--divider)]'
style={{
width,
height,
}}
>
{/* Target handle on left (input to the subflow) */}
<Handle
type='target'
position={Position.Left}
id='target'
className={handleClass}
style={{ left: '-7px', top: '20px', transform: 'translateY(-50%)' }}
/>

{/* Header - matches actual subflow header */}
<div className='flex items-center gap-[10px] rounded-t-[8px] border-[var(--divider)] border-b bg-[var(--surface-2)] py-[8px] pr-[12px] pl-[8px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: blockIconBg }}
>
<BlockIcon className='h-[16px] w-[16px] text-white' />
</div>
<span className='font-medium text-[16px]' title={blockName}>
{blockName}
</span>
</div>

{/* Start handle inside - connects to first block in subflow */}
<div className='absolute top-[56px] left-[16px] flex items-center justify-center rounded-[8px] bg-[var(--surface-2)] px-[12px] py-[6px]'>
<span className='font-medium text-[14px] text-white'>Start</span>
<Handle
type='source'
position={Position.Right}
id={startHandleId}
className={handleClass}
style={{ right: '-7px', top: '50%', transform: 'translateY(-50%)' }}
/>
</div>

{/* End source handle on right (output from the subflow) */}
<Handle
type='source'
position={Position.Right}
id={endHandleId}
className={handleClass}
style={{ right: '-7px', top: '20px', transform: 'translateY(-50%)' }}
/>
</div>
)
}

export const WorkflowPreviewSubflow = memo(WorkflowPreviewSubflowInner)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client'

import { useMemo } from 'react'
import { cloneDeep } from 'lodash'
import ReactFlow, {
ConnectionLineType,
type Edge,
Expand All @@ -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'

Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -178,8 +233,6 @@ export function WorkflowPreview({
return
}

const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {}

const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'

nodeArray.push({
Expand All @@ -194,7 +247,7 @@ export function WorkflowPreview({
blockState: block,
canEdit: false,
isPreview: true,
subBlockValues: subBlocksClone,
subBlockValues: block.subBlocks ?? {},
},
})

Expand Down Expand Up @@ -242,6 +295,7 @@ export function WorkflowPreview({
showSubBlocks,
workflowState.blocks,
isValidWorkflowState,
lightweight,
])

const edges: Edge[] = useMemo(() => {
Expand Down