Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,118 @@
'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
}

/**
* Lightweight block component for workflow previews.
* Renders block header, dummy subblocks skeleton, and horizontal handles.
* No 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 } = 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 - always horizontal
const handleClass = '!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !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={Position.Left}
id='target'
className={handleClass}
style={{ left: '-7px', top: '24px' }}
/>
)}

{/* 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: blockConfig.bgColor }}
>
<IconComponent className='h-[16px] w-[16px] text-white' />
</div>
<span className='truncate font-medium text-[16px]' 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={Position.Right}
id='source'
className={handleClass}
style={{ right: '-7px', top: '24px' }}
/>
</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,41 @@ 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,
},
})
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 +231,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 +245,7 @@ export function WorkflowPreview({
blockState: block,
canEdit: false,
isPreview: true,
subBlockValues: subBlocksClone,
subBlockValues: block.subBlocks ?? {},
},
})

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

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