Skip to content

Commit 9670d96

Browse files
fix(templates-page): loading issue due to loading extensive workflow block in preview for all listings (#2166)
* fix(templates-page): loading issue due to loading extensive workflow block in preview for all listings * add more properties
1 parent eb0d4cb commit 9670d96

File tree

5 files changed

+285
-6
lines changed

5 files changed

+285
-6
lines changed

apps/sim/app/templates/components/template-card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ function TemplateCardInner({
210210
isPannable={false}
211211
defaultZoom={0.8}
212212
fitPadding={0.2}
213+
lightweight
213214
/>
214215
) : (
215216
<div className='h-full w-full bg-[#2A2A2A]' />

apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ function TemplateCardInner({
211211
isPannable={false}
212212
defaultZoom={0.8}
213213
fitPadding={0.2}
214+
lightweight
214215
/>
215216
) : (
216217
<div className='h-full w-full bg-[#2A2A2A]' />
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use client'
2+
3+
import { memo, useMemo } from 'react'
4+
import { Handle, type NodeProps, Position } from 'reactflow'
5+
import { getBlock } from '@/blocks/registry'
6+
7+
interface WorkflowPreviewBlockData {
8+
type: string
9+
name: string
10+
isTrigger?: boolean
11+
horizontalHandles?: boolean
12+
enabled?: boolean
13+
}
14+
15+
/**
16+
* Lightweight block component for workflow previews.
17+
* Renders block header, dummy subblocks skeleton, and handles.
18+
* Respects horizontalHandles and enabled state from workflow.
19+
* No heavy hooks, store subscriptions, or interactive features.
20+
* Used in template cards and other preview contexts for performance.
21+
*/
22+
function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>) {
23+
const { type, name, isTrigger = false, horizontalHandles = false, enabled = true } = data
24+
25+
const blockConfig = getBlock(type)
26+
if (!blockConfig) {
27+
return null
28+
}
29+
30+
const IconComponent = blockConfig.icon
31+
// Hide input handle for triggers, starters, or blocks in trigger mode
32+
const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger
33+
34+
// Get visible subblocks from config (no fetching, just config structure)
35+
const visibleSubBlocks = useMemo(() => {
36+
if (!blockConfig.subBlocks) return []
37+
38+
return blockConfig.subBlocks.filter((subBlock) => {
39+
if (subBlock.hidden) return false
40+
if (subBlock.hideFromPreview) return false
41+
if (subBlock.mode === 'trigger') return false
42+
if (subBlock.mode === 'advanced') return false
43+
return true
44+
})
45+
}, [blockConfig.subBlocks])
46+
47+
const hasSubBlocks = visibleSubBlocks.length > 0
48+
const showErrorRow = !isStarterOrTrigger
49+
50+
// Handle styles based on orientation
51+
const horizontalHandleClass = '!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !rounded-[2px]'
52+
const verticalHandleClass = '!border-none !bg-[var(--surface-12)] !h-[7px] !w-5 !rounded-[2px]'
53+
54+
return (
55+
<div className='relative w-[250px] select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'>
56+
{/* Target handle - not shown for triggers/starters */}
57+
{!isStarterOrTrigger && (
58+
<Handle
59+
type='target'
60+
position={horizontalHandles ? Position.Left : Position.Top}
61+
id='target'
62+
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
63+
style={
64+
horizontalHandles
65+
? { left: '-7px', top: '24px' }
66+
: { top: '-7px', left: '50%', transform: 'translateX(-50%)' }
67+
}
68+
/>
69+
)}
70+
71+
{/* Header */}
72+
<div
73+
className={`flex items-center gap-[10px] p-[8px] ${hasSubBlocks || showErrorRow ? 'border-[var(--divider)] border-b' : ''}`}
74+
>
75+
<div
76+
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
77+
style={{ background: enabled ? blockConfig.bgColor : 'gray' }}
78+
>
79+
<IconComponent className='h-[16px] w-[16px] text-white' />
80+
</div>
81+
<span
82+
className={`truncate font-medium text-[16px] ${!enabled ? 'text-[#808080]' : ''}`}
83+
title={name}
84+
>
85+
{name}
86+
</span>
87+
</div>
88+
89+
{/* Subblocks skeleton */}
90+
{(hasSubBlocks || showErrorRow) && (
91+
<div className='flex flex-col gap-[8px] p-[8px]'>
92+
{visibleSubBlocks.slice(0, 4).map((subBlock) => (
93+
<div key={subBlock.id} className='flex items-center gap-[8px]'>
94+
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
95+
{subBlock.title ?? subBlock.id}
96+
</span>
97+
<span className='flex-1 truncate text-right text-[14px] text-[var(--white)]'>-</span>
98+
</div>
99+
))}
100+
{visibleSubBlocks.length > 4 && (
101+
<div className='flex items-center gap-[8px]'>
102+
<span className='text-[14px] text-[var(--text-tertiary)]'>
103+
+{visibleSubBlocks.length - 4} more
104+
</span>
105+
</div>
106+
)}
107+
{showErrorRow && (
108+
<div className='flex items-center gap-[8px]'>
109+
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
110+
error
111+
</span>
112+
</div>
113+
)}
114+
</div>
115+
)}
116+
117+
{/* Source handle */}
118+
<Handle
119+
type='source'
120+
position={horizontalHandles ? Position.Right : Position.Bottom}
121+
id='source'
122+
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
123+
style={
124+
horizontalHandles
125+
? { right: '-7px', top: '24px' }
126+
: { bottom: '-7px', left: '50%', transform: 'translateX(-50%)' }
127+
}
128+
/>
129+
</div>
130+
)
131+
}
132+
133+
export const WorkflowPreviewBlock = memo(WorkflowPreviewBlockInner)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client'
2+
3+
import { memo } from 'react'
4+
import { RepeatIcon, SplitIcon } from 'lucide-react'
5+
import { Handle, type NodeProps, Position } from 'reactflow'
6+
7+
interface WorkflowPreviewSubflowData {
8+
name: string
9+
width?: number
10+
height?: number
11+
kind: 'loop' | 'parallel'
12+
}
13+
14+
/**
15+
* Lightweight subflow component for workflow previews.
16+
* Matches the styling of the actual SubflowNodeComponent but without
17+
* hooks, store subscriptions, or interactive features.
18+
* Used in template cards and other preview contexts for performance.
19+
*/
20+
function WorkflowPreviewSubflowInner({ data }: NodeProps<WorkflowPreviewSubflowData>) {
21+
const { name, width = 500, height = 300, kind } = data
22+
23+
const isLoop = kind === 'loop'
24+
const BlockIcon = isLoop ? RepeatIcon : SplitIcon
25+
const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B'
26+
const blockName = name || (isLoop ? 'Loop' : 'Parallel')
27+
28+
// Handle IDs matching the actual subflow component
29+
const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source'
30+
const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source'
31+
32+
// Handle styles matching the actual subflow component
33+
const handleClass =
34+
'!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-[2px]'
35+
36+
return (
37+
<div
38+
className='relative select-none rounded-[8px] border border-[var(--divider)]'
39+
style={{
40+
width,
41+
height,
42+
}}
43+
>
44+
{/* Target handle on left (input to the subflow) */}
45+
<Handle
46+
type='target'
47+
position={Position.Left}
48+
id='target'
49+
className={handleClass}
50+
style={{ left: '-7px', top: '20px', transform: 'translateY(-50%)' }}
51+
/>
52+
53+
{/* Header - matches actual subflow header */}
54+
<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]'>
55+
<div
56+
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
57+
style={{ backgroundColor: blockIconBg }}
58+
>
59+
<BlockIcon className='h-[16px] w-[16px] text-white' />
60+
</div>
61+
<span className='font-medium text-[16px]' title={blockName}>
62+
{blockName}
63+
</span>
64+
</div>
65+
66+
{/* Start handle inside - connects to first block in subflow */}
67+
<div className='absolute top-[56px] left-[16px] flex items-center justify-center rounded-[8px] bg-[var(--surface-2)] px-[12px] py-[6px]'>
68+
<span className='font-medium text-[14px] text-white'>Start</span>
69+
<Handle
70+
type='source'
71+
position={Position.Right}
72+
id={startHandleId}
73+
className={handleClass}
74+
style={{ right: '-7px', top: '50%', transform: 'translateY(-50%)' }}
75+
/>
76+
</div>
77+
78+
{/* End source handle on right (output from the subflow) */}
79+
<Handle
80+
type='source'
81+
position={Position.Right}
82+
id={endHandleId}
83+
className={handleClass}
84+
style={{ right: '-7px', top: '20px', transform: 'translateY(-50%)' }}
85+
/>
86+
</div>
87+
)
88+
}
89+
90+
export const WorkflowPreviewSubflow = memo(WorkflowPreviewSubflowInner)

apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client'
22

33
import { useMemo } from 'react'
4-
import { cloneDeep } from 'lodash'
54
import ReactFlow, {
65
ConnectionLineType,
76
type Edge,
@@ -18,6 +17,8 @@ import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen
1817
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
1918
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
2019
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
20+
import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block'
21+
import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow'
2122
import { getBlock } from '@/blocks'
2223
import type { WorkflowState } from '@/stores/workflows/workflow/types'
2324

@@ -34,15 +35,29 @@ interface WorkflowPreviewProps {
3435
defaultZoom?: number
3536
fitPadding?: number
3637
onNodeClick?: (blockId: string, mousePosition: { x: number; y: number }) => void
38+
/** Use lightweight blocks for better performance in template cards */
39+
lightweight?: boolean
3740
}
3841

39-
// Define node types - the components now handle preview mode internally
40-
const nodeTypes: NodeTypes = {
42+
/**
43+
* Full node types with interactive WorkflowBlock for detailed previews
44+
*/
45+
const fullNodeTypes: NodeTypes = {
4146
workflowBlock: WorkflowBlock,
4247
noteBlock: NoteBlock,
4348
subflowNode: SubflowNodeComponent,
4449
}
4550

51+
/**
52+
* Lightweight node types for template cards and other high-volume previews.
53+
* Uses minimal components without hooks or store subscriptions.
54+
*/
55+
const lightweightNodeTypes: NodeTypes = {
56+
workflowBlock: WorkflowPreviewBlock,
57+
noteBlock: WorkflowPreviewBlock,
58+
subflowNode: WorkflowPreviewSubflow,
59+
}
60+
4661
// Define edge types
4762
const edgeTypes: EdgeTypes = {
4863
default: WorkflowEdge,
@@ -59,7 +74,10 @@ export function WorkflowPreview({
5974
defaultZoom = 0.8,
6075
fitPadding = 0.25,
6176
onNodeClick,
77+
lightweight = false,
6278
}: WorkflowPreviewProps) {
79+
// Use lightweight node types for better performance in template cards
80+
const nodeTypes = lightweight ? lightweightNodeTypes : fullNodeTypes
6381
// Check if the workflow state is valid
6482
const isValidWorkflowState = workflowState?.blocks && workflowState.edges
6583

@@ -130,6 +148,43 @@ export function WorkflowPreview({
130148

131149
const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks)
132150

151+
// Lightweight mode: create minimal node data for performance
152+
if (lightweight) {
153+
// Handle loops and parallels as subflow nodes
154+
if (block.type === 'loop' || block.type === 'parallel') {
155+
nodeArray.push({
156+
id: blockId,
157+
type: 'subflowNode',
158+
position: absolutePosition,
159+
draggable: false,
160+
data: {
161+
name: block.name,
162+
width: block.data?.width || 500,
163+
height: block.data?.height || 300,
164+
kind: block.type as 'loop' | 'parallel',
165+
},
166+
})
167+
return
168+
}
169+
170+
// Regular blocks
171+
nodeArray.push({
172+
id: blockId,
173+
type: 'workflowBlock',
174+
position: absolutePosition,
175+
draggable: false,
176+
data: {
177+
type: block.type,
178+
name: block.name,
179+
isTrigger: block.triggerMode === true,
180+
horizontalHandles: block.horizontalHandles ?? false,
181+
enabled: block.enabled ?? true,
182+
},
183+
})
184+
return
185+
}
186+
187+
// Full mode: create detailed node data for interactive previews
133188
if (block.type === 'loop') {
134189
nodeArray.push({
135190
id: block.id,
@@ -178,8 +233,6 @@ export function WorkflowPreview({
178233
return
179234
}
180235

181-
const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {}
182-
183236
const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'
184237

185238
nodeArray.push({
@@ -194,7 +247,7 @@ export function WorkflowPreview({
194247
blockState: block,
195248
canEdit: false,
196249
isPreview: true,
197-
subBlockValues: subBlocksClone,
250+
subBlockValues: block.subBlocks ?? {},
198251
},
199252
})
200253

@@ -242,6 +295,7 @@ export function WorkflowPreview({
242295
showSubBlocks,
243296
workflowState.blocks,
244297
isValidWorkflowState,
298+
lightweight,
245299
])
246300

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

0 commit comments

Comments
 (0)