Skip to content

Commit 0a56955

Browse files
committed
fix(templates-page): loading issue due to loading extensive workflow block in preview for all listings
1 parent 84c2335 commit 0a56955

File tree

5 files changed

+268
-6
lines changed

5 files changed

+268
-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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
}
12+
13+
/**
14+
* Lightweight block component for workflow previews.
15+
* Renders block header, dummy subblocks skeleton, and horizontal handles.
16+
* No hooks, store subscriptions, or interactive features.
17+
* Used in template cards and other preview contexts for performance.
18+
*/
19+
function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>) {
20+
const { type, name, isTrigger = false } = data
21+
22+
const blockConfig = getBlock(type)
23+
if (!blockConfig) {
24+
return null
25+
}
26+
27+
const IconComponent = blockConfig.icon
28+
// Hide input handle for triggers, starters, or blocks in trigger mode
29+
const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger
30+
31+
// Get visible subblocks from config (no fetching, just config structure)
32+
const visibleSubBlocks = useMemo(() => {
33+
if (!blockConfig.subBlocks) return []
34+
35+
return blockConfig.subBlocks.filter((subBlock) => {
36+
if (subBlock.hidden) return false
37+
if (subBlock.hideFromPreview) return false
38+
if (subBlock.mode === 'trigger') return false
39+
if (subBlock.mode === 'advanced') return false
40+
return true
41+
})
42+
}, [blockConfig.subBlocks])
43+
44+
const hasSubBlocks = visibleSubBlocks.length > 0
45+
const showErrorRow = !isStarterOrTrigger
46+
47+
// Handle styles - always horizontal
48+
const handleClass = '!border-none !bg-[var(--surface-12)] !h-5 !w-[7px] !rounded-[2px]'
49+
50+
return (
51+
<div className='relative w-[250px] select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'>
52+
{/* Target handle - not shown for triggers/starters */}
53+
{!isStarterOrTrigger && (
54+
<Handle
55+
type='target'
56+
position={Position.Left}
57+
id='target'
58+
className={handleClass}
59+
style={{ left: '-7px', top: '24px' }}
60+
/>
61+
)}
62+
63+
{/* Header */}
64+
<div
65+
className={`flex items-center gap-[10px] p-[8px] ${hasSubBlocks || showErrorRow ? 'border-[var(--divider)] border-b' : ''}`}
66+
>
67+
<div
68+
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
69+
style={{ background: blockConfig.bgColor }}
70+
>
71+
<IconComponent className='h-[16px] w-[16px] text-white' />
72+
</div>
73+
<span className='truncate font-medium text-[16px]' title={name}>
74+
{name}
75+
</span>
76+
</div>
77+
78+
{/* Subblocks skeleton */}
79+
{(hasSubBlocks || showErrorRow) && (
80+
<div className='flex flex-col gap-[8px] p-[8px]'>
81+
{visibleSubBlocks.slice(0, 4).map((subBlock) => (
82+
<div key={subBlock.id} className='flex items-center gap-[8px]'>
83+
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
84+
{subBlock.title ?? subBlock.id}
85+
</span>
86+
<span className='flex-1 truncate text-right text-[14px] text-[var(--white)]'>-</span>
87+
</div>
88+
))}
89+
{visibleSubBlocks.length > 4 && (
90+
<div className='flex items-center gap-[8px]'>
91+
<span className='text-[14px] text-[var(--text-tertiary)]'>
92+
+{visibleSubBlocks.length - 4} more
93+
</span>
94+
</div>
95+
)}
96+
{showErrorRow && (
97+
<div className='flex items-center gap-[8px]'>
98+
<span className='min-w-0 truncate text-[14px] text-[var(--text-tertiary)] capitalize'>
99+
error
100+
</span>
101+
</div>
102+
)}
103+
</div>
104+
)}
105+
106+
{/* Source handle */}
107+
<Handle
108+
type='source'
109+
position={Position.Right}
110+
id='source'
111+
className={handleClass}
112+
style={{ right: '-7px', top: '24px' }}
113+
/>
114+
</div>
115+
)
116+
}
117+
118+
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: 58 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,41 @@ 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+
},
181+
})
182+
return
183+
}
184+
185+
// Full mode: create detailed node data for interactive previews
133186
if (block.type === 'loop') {
134187
nodeArray.push({
135188
id: block.id,
@@ -178,8 +231,6 @@ export function WorkflowPreview({
178231
return
179232
}
180233

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

185236
nodeArray.push({
@@ -194,7 +245,7 @@ export function WorkflowPreview({
194245
blockState: block,
195246
canEdit: false,
196247
isPreview: true,
197-
subBlockValues: subBlocksClone,
248+
subBlockValues: block.subBlocks ?? {},
198249
},
199250
})
200251

@@ -242,6 +293,7 @@ export function WorkflowPreview({
242293
showSubBlocks,
243294
workflowState.blocks,
244295
isValidWorkflowState,
296+
lightweight,
245297
])
246298

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

0 commit comments

Comments
 (0)