11'use client'
22
3- import { memo , useMemo } from 'react'
3+ import { type CSSProperties , memo , useMemo } from 'react'
44import { Handle , type NodeProps , Position } from 'reactflow'
55import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
66import {
@@ -23,6 +23,27 @@ interface SubBlockValueEntry {
2323 value : unknown
2424}
2525
26+ /**
27+ * Handle style constants for preview blocks.
28+ * Extracted to avoid recreating style objects on each render.
29+ */
30+ const HANDLE_STYLES = {
31+ horizontal : '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]' ,
32+ vertical : '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]' ,
33+ right :
34+ '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' ,
35+ error :
36+ '!z-[10] !border-none !bg-[var(--text-error)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' ,
37+ } as const
38+
39+ /** Reusable style object for error handles positioned at bottom-right */
40+ const ERROR_HANDLE_STYLE : CSSProperties = {
41+ right : '-7px' ,
42+ top : 'auto' ,
43+ bottom : `${ HANDLE_POSITIONS . ERROR_BOTTOM_OFFSET } px` ,
44+ transform : 'translateY(50%)' ,
45+ }
46+
2647interface WorkflowPreviewBlockData {
2748 type : string
2849 name : string
@@ -379,9 +400,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
379400 ? routerRows . length > 0 || shouldShowDefaultHandles
380401 : hasSubBlocks || shouldShowDefaultHandles
381402
382- const horizontalHandleClass = '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]'
383- const verticalHandleClass = '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]'
384-
385403 const hasError = executionStatus === 'error'
386404 const hasSuccess = executionStatus === 'success'
387405
@@ -406,7 +424,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
406424 type = 'target'
407425 position = { horizontalHandles ? Position . Left : Position . Top }
408426 id = 'target'
409- className = { horizontalHandles ? horizontalHandleClass : verticalHandleClass }
427+ className = { horizontalHandles ? HANDLE_STYLES . horizontal : HANDLE_STYLES . vertical }
410428 style = {
411429 horizontalHandles
412430 ? { left : '-7px' , top : `${ HANDLE_POSITIONS . DEFAULT_Y_OFFSET } px` }
@@ -479,27 +497,103 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
479497 </ div >
480498 ) }
481499
482- { /* Source handle */ }
483- < Handle
484- type = 'source'
485- position = { horizontalHandles ? Position . Right : Position . Bottom }
486- id = 'source'
487- className = { horizontalHandles ? horizontalHandleClass : verticalHandleClass }
488- style = {
489- horizontalHandles
490- ? { right : '-7px' , top : `${ HANDLE_POSITIONS . DEFAULT_Y_OFFSET } px` }
491- : { bottom : '-7px' , left : '50%' , transform : 'translateX(-50%)' }
492- }
493- />
500+ { /* Condition block handles */ }
501+ { type === 'condition' && (
502+ < >
503+ { conditionRows . map ( ( cond , condIndex ) => {
504+ const topOffset =
505+ HANDLE_POSITIONS . CONDITION_START_Y + condIndex * HANDLE_POSITIONS . CONDITION_ROW_HEIGHT
506+ return (
507+ < Handle
508+ key = { `handle-${ cond . id } ` }
509+ type = 'source'
510+ position = { Position . Right }
511+ id = { `condition-${ cond . id } ` }
512+ className = { HANDLE_STYLES . right }
513+ style = { { top : `${ topOffset } px` , right : '-7px' , transform : 'translateY(-50%)' } }
514+ />
515+ )
516+ } ) }
517+ < Handle
518+ type = 'source'
519+ position = { Position . Right }
520+ id = 'error'
521+ className = { HANDLE_STYLES . error }
522+ style = { ERROR_HANDLE_STYLE }
523+ />
524+ </ >
525+ ) }
526+
527+ { /* Router block handles */ }
528+ { type === 'router_v2' && (
529+ < >
530+ { routerRows . map ( ( route , routeIndex ) => {
531+ // +1 row offset for context row at the top
532+ const topOffset =
533+ HANDLE_POSITIONS . CONDITION_START_Y +
534+ ( routeIndex + 1 ) * HANDLE_POSITIONS . CONDITION_ROW_HEIGHT
535+ return (
536+ < Handle
537+ key = { `handle-${ route . id } ` }
538+ type = 'source'
539+ position = { Position . Right }
540+ id = { `router-${ route . id } ` }
541+ className = { HANDLE_STYLES . right }
542+ style = { { top : `${ topOffset } px` , right : '-7px' , transform : 'translateY(-50%)' } }
543+ />
544+ )
545+ } ) }
546+ < Handle
547+ type = 'source'
548+ position = { Position . Right }
549+ id = 'error'
550+ className = { HANDLE_STYLES . error }
551+ style = { ERROR_HANDLE_STYLE }
552+ />
553+ </ >
554+ ) }
555+
556+ { /* Source and error handles for non-condition/router blocks */ }
557+ { type !== 'condition' && type !== 'router_v2' && type !== 'response' && (
558+ < >
559+ < Handle
560+ type = 'source'
561+ position = { horizontalHandles ? Position . Right : Position . Bottom }
562+ id = 'source'
563+ className = { horizontalHandles ? HANDLE_STYLES . right : HANDLE_STYLES . vertical }
564+ style = {
565+ horizontalHandles
566+ ? { right : '-7px' , top : `${ HANDLE_POSITIONS . DEFAULT_Y_OFFSET } px` }
567+ : { bottom : '-7px' , left : '50%' , transform : 'translateX(-50%)' }
568+ }
569+ />
570+ { shouldShowDefaultHandles && (
571+ < Handle
572+ type = 'source'
573+ position = { Position . Right }
574+ id = 'error'
575+ className = { HANDLE_STYLES . error }
576+ style = { ERROR_HANDLE_STYLE }
577+ />
578+ ) }
579+ </ >
580+ ) }
494581 </ div >
495582 )
496583}
497584
585+ /**
586+ * Custom comparison function for React.memo optimization.
587+ * Uses fast-path primitive comparison before shallow comparing subBlockValues.
588+ * @param prevProps - Previous render props
589+ * @param nextProps - Next render props
590+ * @returns True if render should be skipped (props are equal)
591+ */
498592function shouldSkipPreviewBlockRender (
499593 prevProps : NodeProps < WorkflowPreviewBlockData > ,
500594 nextProps : NodeProps < WorkflowPreviewBlockData >
501595) : boolean {
502- // Check primitive props first (fast path)
596+ // Fast path: check primitive props first
503597 if (
504598 prevProps . id !== nextProps . id ||
505599 prevProps . data . type !== nextProps . data . type ||
@@ -513,33 +607,34 @@ function shouldSkipPreviewBlockRender(
513607 return false
514608 }
515609
516- // Compare subBlockValues by reference first
517610 const prevValues = prevProps . data . subBlockValues
518611 const nextValues = nextProps . data . subBlockValues
519612
520- if ( prevValues === nextValues ) {
521- return true
522- }
613+ // Reference equality check
614+ if ( prevValues === nextValues ) return true
615+ if ( ! prevValues || ! nextValues ) return false
523616
524- if ( ! prevValues || ! nextValues ) {
525- return false
526- }
527-
528- // Shallow compare keys and values
617+ // Shallow comparison of subBlockValues
529618 const prevKeys = Object . keys ( prevValues )
530619 const nextKeys = Object . keys ( nextValues )
531620
532- if ( prevKeys . length !== nextKeys . length ) {
533- return false
534- }
621+ if ( prevKeys . length !== nextKeys . length ) return false
535622
536623 for ( const key of prevKeys ) {
537- if ( prevValues [ key ] !== nextValues [ key ] ) {
538- return false
539- }
624+ if ( prevValues [ key ] !== nextValues [ key ] ) return false
540625 }
541626
542627 return true
543628}
544629
630+ /**
631+ * Preview block component for workflow visualization in readonly contexts.
632+ * Optimized for rendering without hooks or store subscriptions.
633+ *
634+ * @remarks
635+ * - Renders block header, subblock values, and connection handles
636+ * - Supports condition, router, and standard block types
637+ * - Shows error handles for non-trigger blocks
638+ * - Displays execution status via colored ring overlays
639+ */
545640export const WorkflowPreviewBlock = memo ( WorkflowPreviewBlockInner , shouldSkipPreviewBlockRender )
0 commit comments