mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
committed by
GitHub
parent
eb0d4cbd57
commit
9670d96eca
@@ -210,6 +210,7 @@ function TemplateCardInner({
|
||||
isPannable={false}
|
||||
defaultZoom={0.8}
|
||||
fitPadding={0.2}
|
||||
lightweight
|
||||
/>
|
||||
) : (
|
||||
<div className='h-full w-full bg-[#2A2A2A]' />
|
||||
|
||||
@@ -211,6 +211,7 @@ function TemplateCardInner({
|
||||
isPannable={false}
|
||||
defaultZoom={0.8}
|
||||
fitPadding={0.2}
|
||||
lightweight
|
||||
/>
|
||||
) : (
|
||||
<div className='h-full w-full bg-[#2A2A2A]' />
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user