@@ -23,11 +23,7 @@ import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/type
2323
2424const logger = createLogger ( 'PreviewWorkflow' )
2525
26- /**
27- * Gets block dimensions for preview purposes.
28- * For containers, uses stored dimensions or defaults.
29- * For regular blocks, uses stored height or estimates based on type.
30- */
26+ /** Gets block dimensions, using stored values or defaults. */
3127function getPreviewBlockDimensions ( block : BlockState ) : { width : number ; height : number } {
3228 if ( block . type === 'loop' || block . type === 'parallel' ) {
3329 return {
@@ -50,10 +46,7 @@ function getPreviewBlockDimensions(block: BlockState): { width: number; height:
5046 return estimateBlockDimensions ( block . type )
5147}
5248
53- /**
54- * Calculates container dimensions based on child block positions and sizes.
55- * Mirrors the logic from useNodeUtilities.calculateLoopDimensions.
56- */
49+ /** Calculates container dimensions from child block positions. */
5750function calculateContainerDimensions (
5851 containerId : string ,
5952 blocks : Record < string , BlockState >
@@ -91,12 +84,7 @@ function calculateContainerDimensions(
9184 return { width, height }
9285}
9386
94- /**
95- * Finds the leftmost block ID from a workflow state.
96- * Excludes subflow containers (loop/parallel) from consideration.
97- * @param workflowState - The workflow state to search
98- * @returns The ID of the leftmost block, or null if no blocks exist
99- */
87+ /** Finds the leftmost block ID, excluding subflow containers. */
10088export function getLeftmostBlockId ( workflowState : WorkflowState | null | undefined ) : string | null {
10189 if ( ! workflowState ?. blocks ) return null
10290
@@ -118,7 +106,7 @@ export function getLeftmostBlockId(workflowState: WorkflowState | null | undefin
118106/** Execution status for edges/nodes in the preview */
119107type ExecutionStatus = 'success' | 'error' | 'not-executed'
120108
121- /** Calculates absolute position for blocks , handling nested subflows */
109+ /** Calculates absolute position, handling nested subflows. */
122110function calculateAbsolutePosition (
123111 block : BlockState ,
124112 blocks : Record < string , BlockState >
@@ -164,10 +152,7 @@ interface PreviewWorkflowProps {
164152 lightweight ?: boolean
165153}
166154
167- /**
168- * Preview node types using minimal components without hooks or store subscriptions.
169- * This prevents interaction issues while allowing canvas panning and node clicking.
170- */
155+ /** Preview node types using minimal, hook-free components. */
171156const previewNodeTypes : NodeTypes = {
172157 workflowBlock : PreviewBlock ,
173158 noteBlock : PreviewBlock ,
@@ -185,11 +170,7 @@ interface FitViewOnChangeProps {
185170 containerRef : React . RefObject < HTMLDivElement | null >
186171}
187172
188- /**
189- * Helper component that calls fitView when the set of nodes changes or when the container resizes.
190- * Only triggers on actual node additions/removals, not on selection changes.
191- * Must be rendered inside ReactFlowProvider.
192- */
173+ /** Calls fitView on node changes or container resize. */
193174function FitViewOnChange ( { nodeIds, fitPadding, containerRef } : FitViewOnChangeProps ) {
194175 const { fitView } = useReactFlow ( )
195176 const lastNodeIdsRef = useRef < string | null > ( null )
@@ -229,16 +210,7 @@ function FitViewOnChange({ nodeIds, fitPadding, containerRef }: FitViewOnChangeP
229210 return null
230211}
231212
232- /**
233- * Readonly workflow component for visualizing workflow state.
234- * Renders blocks, subflows, and edges with execution status highlighting.
235- *
236- * @remarks
237- * - Supports panning and node click interactions
238- * - Shows execution path via green edges for successful paths
239- * - Error edges display red by default, green when error path was taken
240- * - Fits view automatically when nodes change or container resizes
241- */
213+ /** Readonly workflow visualization with execution status highlighting. */
242214export function PreviewWorkflow ( {
243215 workflowState,
244216 className,
@@ -300,49 +272,58 @@ export function PreviewWorkflow({
300272 return map
301273 } , [ workflowState . blocks , isValidWorkflowState ] )
302274
303- /** Derives subflow execution status from child blocks */
275+ /** Maps base block IDs to execution data, handling parallel iteration variants (blockId₍n₎). */
276+ const blockExecutionMap = useMemo ( ( ) => {
277+ if ( ! executedBlocks ) return new Map < string , { status : string } > ( )
278+
279+ const map = new Map < string , { status : string } > ( )
280+ for ( const [ key , value ] of Object . entries ( executedBlocks ) ) {
281+ // Extract base ID (remove iteration suffix like ₍0₎)
282+ const baseId = key . includes ( '₍' ) ? key . split ( '₍' ) [ 0 ] : key
283+ // Keep first match or error status (error takes precedence)
284+ const existing = map . get ( baseId )
285+ if ( ! existing || value . status === 'error' ) {
286+ map . set ( baseId , value )
287+ }
288+ }
289+ return map
290+ } , [ executedBlocks ] )
291+
292+ /** Derives subflow status from children. Error takes precedence. */
304293 const getSubflowExecutionStatus = useMemo ( ( ) => {
305294 return ( subflowId : string ) : ExecutionStatus | undefined => {
306- if ( ! executedBlocks ) return undefined
307-
308295 const childIds = subflowChildrenMap . get ( subflowId )
309296 if ( ! childIds ?. length ) return undefined
310297
311- const childStatuses = childIds . map ( ( id ) => executedBlocks [ id ] ) . filter ( Boolean )
312- if ( childStatuses . length === 0 ) return undefined
298+ const executedChildren = childIds
299+ . map ( ( id ) => blockExecutionMap . get ( id ) )
300+ . filter ( ( status ) : status is { status : string } => Boolean ( status ) )
313301
314- if ( childStatuses . some ( ( s ) => s . status === 'error' ) ) return 'error'
315- if ( childStatuses . some ( ( s ) => s . status === 'success ' ) ) return 'success '
316- return 'not-executed '
302+ if ( executedChildren . length === 0 ) return undefined
303+ if ( executedChildren . some ( ( s ) => s . status === 'error ' ) ) return 'error '
304+ return 'success '
317305 }
318- } , [ executedBlocks , subflowChildrenMap ] )
306+ } , [ subflowChildrenMap , blockExecutionMap ] )
319307
320- /** Gets execution status for any block, deriving subflow status from children */
308+ /** Gets block status. Subflows derive status from children. */
321309 const getBlockExecutionStatus = useMemo ( ( ) => {
322310 return ( blockId : string ) : { status : string ; executed : boolean } | undefined => {
323- if ( ! executedBlocks ) return undefined
324-
325- const directStatus = executedBlocks [ blockId ]
311+ const directStatus = blockExecutionMap . get ( blockId )
326312 if ( directStatus ) {
327313 return { status : directStatus . status , executed : true }
328314 }
329315
330316 const block = workflowState . blocks ?. [ blockId ]
331- if ( block && ( block . type === 'loop' || block . type === 'parallel' ) ) {
317+ if ( block ? .type === 'loop' || block ? .type === 'parallel' ) {
332318 const subflowStatus = getSubflowExecutionStatus ( blockId )
333319 if ( subflowStatus ) {
334320 return { status : subflowStatus , executed : true }
335321 }
336-
337- const incomingEdge = workflowState . edges ?. find ( ( e ) => e . target === blockId )
338- if ( incomingEdge && executedBlocks [ incomingEdge . source ] ?. status === 'success' ) {
339- return { status : 'not-executed' , executed : true }
340- }
341322 }
342323
343324 return undefined
344325 }
345- } , [ executedBlocks , workflowState . blocks , workflowState . edges , getSubflowExecutionStatus ] )
326+ } , [ workflowState . blocks , getSubflowExecutionStatus , blockExecutionMap ] )
346327
347328 const edgesStructure = useMemo ( ( ) => {
348329 if ( ! isValidWorkflowState ) return { count : 0 , ids : '' }
@@ -444,48 +425,29 @@ export function PreviewWorkflow({
444425 const edges : Edge [ ] = useMemo ( ( ) => {
445426 if ( ! isValidWorkflowState ) return [ ]
446427
447- /**
448- * Determines edge execution status for visualization.
449- * Error edges turn green when taken (source errored, target executed).
450- * Normal edges turn green when both source succeeded and target executed.
451- */
428+ /** Edge is green if target executed and source condition met by edge type. */
452429 const getEdgeExecutionStatus = ( edge : {
453430 source : string
454431 target : string
455432 sourceHandle ?: string | null
456433 } ) : ExecutionStatus | undefined => {
457- if ( ! executedBlocks ) return undefined
434+ if ( blockExecutionMap . size === 0 ) return undefined
458435
459- const sourceStatus = getBlockExecutionStatus ( edge . source )
460436 const targetStatus = getBlockExecutionStatus ( edge . target )
461- const isErrorEdge = edge . sourceHandle === 'error'
462-
463- if ( isErrorEdge ) {
464- return sourceStatus ?. status === 'error' && targetStatus ?. executed
465- ? 'success'
466- : 'not-executed'
467- }
437+ if ( ! targetStatus ?. executed ) return 'not-executed'
468438
469- const isSubflowStartEdge =
470- edge . sourceHandle === 'loop-start-source' || edge . sourceHandle === 'parallel-start-source'
439+ const sourceStatus = getBlockExecutionStatus ( edge . source )
440+ const { sourceHandle } = edge
471441
472- if ( isSubflowStartEdge ) {
473- const incomingEdge = workflowState . edges ?. find ( ( e ) => e . target === edge . source )
474- const incomingSucceeded = incomingEdge
475- ? executedBlocks [ incomingEdge . source ] ?. status === 'success'
476- : false
477- return incomingSucceeded ? 'success' : 'not-executed'
442+ if ( sourceHandle === 'error' ) {
443+ return sourceStatus ?. status === 'error' ? 'success' : 'not-executed'
478444 }
479445
480- const targetBlock = workflowState . blocks ?. [ edge . target ]
481- const targetIsSubflow =
482- targetBlock && ( targetBlock . type === 'loop' || targetBlock . type === 'parallel' )
483-
484- if ( sourceStatus ?. status === 'success' && ( targetStatus ?. executed || targetIsSubflow ) ) {
446+ if ( sourceHandle === 'loop-start-source' || sourceHandle === 'parallel-start-source' ) {
485447 return 'success'
486448 }
487449
488- return 'not-executed'
450+ return sourceStatus ?. status === 'success' ? 'success' : 'not-executed'
489451 }
490452
491453 return ( workflowState . edges || [ ] ) . map ( ( edge ) => {
@@ -507,9 +469,8 @@ export function PreviewWorkflow({
507469 } , [
508470 edgesStructure ,
509471 workflowState . edges ,
510- workflowState . blocks ,
511472 isValidWorkflowState ,
512- executedBlocks ,
473+ blockExecutionMap ,
513474 getBlockExecutionStatus ,
514475 ] )
515476
0 commit comments