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
This commit is contained in:
Vikhyath Mondreti
2025-12-02 19:47:48 -08:00
committed by GitHub
parent eb0d4cbd57
commit 9670d96eca
5 changed files with 285 additions and 6 deletions

View File

@@ -210,6 +210,7 @@ function TemplateCardInner({
isPannable={false}
defaultZoom={0.8}
fitPadding={0.2}
lightweight
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />

View File

@@ -211,6 +211,7 @@ function TemplateCardInner({
isPannable={false}
defaultZoom={0.8}
fitPadding={0.2}
lightweight
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(() => {