11import { memo , useMemo } from 'react'
2- import { Badge , cn , handleKeyboardActivation } from '@sim/emcn'
3- import { RepeatIcon , SplitIcon } from 'lucide-react'
4- import { Handle , type NodeProps , Position , useReactFlow } from 'reactflow'
5- import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
6- import { type DiffStatus , hasDiffStatus } from '@/lib/workflows/diff/types'
2+ import { type SubflowNodeData , SubflowNodeView } from '@sim/workflow-renderer'
3+ import { type NodeProps , useReactFlow } from 'reactflow'
4+ import { hasDiffStatus } from '@/lib/workflows/diff/types'
75import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
86import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar'
97import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
108import { useLastRunPath } from '@/stores/execution'
119import { usePanelEditorStore } from '@/stores/panel'
1210
1311/**
14- * Data structure for subflow nodes (loop and parallel containers)
15- */
16- export interface SubflowNodeData {
17- width ?: number
18- height ?: number
19- parentId ?: string
20- extent ?: 'parent'
21- isPreview ?: boolean
22- /** Whether this subflow is selected in preview mode */
23- isPreviewSelected ?: boolean
24- kind : 'loop' | 'parallel'
25- name ?: string
26- /** Execution status passed by preview/snapshot views */
27- executionStatus ?: 'success' | 'error' | 'not-executed'
28- /** Whether the parent workflow is locked and should render as read-only */
29- isWorkflowLocked ?: boolean
30- }
31-
32- const HANDLE_STYLE = {
33- top : `${ HANDLE_POSITIONS . DEFAULT_Y_OFFSET } px` ,
34- transform : 'translateY(-50%)' ,
35- } as const
36-
37- /**
38- * Reusable class names for Handle components.
39- * Matches the styling pattern from workflow-block.tsx.
40- */
41- const getHandleClasses = ( position : 'left' | 'right' ) => {
42- const baseClasses = '!z-[10] !cursor-crosshair !border-none !transition-[colors] !duration-150'
43- const colorClasses = '!bg-[var(--workflow-edge)]'
44-
45- const positionClasses = {
46- left : '!left-[-8px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover-hover:!left-[-11px] hover-hover:!w-[10px] hover-hover:!rounded-l-full' ,
47- right :
48- '!right-[-8px] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none hover-hover:!right-[-11px] hover-hover:!w-[10px] hover-hover:!rounded-r-full' ,
49- }
50-
51- return cn ( baseClasses , colorClasses , positionClasses [ position ] )
52- }
53-
54- /**
55- * Subflow node component for loop and parallel execution containers.
56- * Renders a resizable container with a header displaying the block name and icon,
57- * handles for connections, and supports nested execution contexts.
12+ * Editor container for {@link SubflowNodeView}.
5813 *
59- * @param props - Node properties containing data and id
60- * @returns Rendered subflow node component
14+ * Resolves the subflow's enabled/locked/focus/diff/run state from the editor
15+ * stores, computes its nesting depth from the ReactFlow node tree, and renders
16+ * the pure view shared with the docs preview — injecting the editor-only
17+ * {@link ActionBar} through the view's `actionBar` slot.
6118 */
6219export const SubflowNodeComponent = memo ( ( { data, id, selected } : NodeProps < SubflowNodeData > ) => {
6320 const { getNodes } = useReactFlow ( )
@@ -66,7 +23,7 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
6623
6724 const currentWorkflow = useCurrentWorkflow ( )
6825 const currentBlock = currentWorkflow . getBlockById ( id )
69- const diffStatus : DiffStatus =
26+ const diffStatus =
7027 currentWorkflow . isDiffMode && currentBlock && hasDiffStatus ( currentBlock )
7128 ? currentBlock . is_diff
7229 : undefined
@@ -75,12 +32,10 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
7532 const isLocked = currentBlock ?. locked ?? false
7633 const isPreview = data ?. isPreview || false
7734
78- const setCurrentBlockId = usePanelEditorStore ( ( state ) => state . setCurrentBlockId )
7935 const currentBlockId = usePanelEditorStore ( ( state ) => state . currentBlockId )
36+ const setCurrentBlockId = usePanelEditorStore ( ( state ) => state . setCurrentBlockId )
8037 const isFocused = currentBlockId === id
8138
82- const isPreviewSelected = data ?. isPreviewSelected || false
83-
8439 const lastRunPath = useLastRunPath ( )
8540 const executionStatus = data . executionStatus
8641 const runPathStatus : 'success' | 'error' | undefined =
@@ -91,8 +46,8 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
9146 : lastRunPath . get ( id )
9247
9348 /**
94- * Calculate the nesting level of this subflow node based on its parent hierarchy.
95- * Used to apply appropriate styling for nested containers .
49+ * Nesting depth, walking the parent chain so the view can apply nested
50+ * container styling.
9651 */
9752 const nestingLevel = useMemo ( ( ) => {
9853 let level = 0
@@ -108,178 +63,21 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
10863 return level
10964 } , [ data ?. parentId , getNodes ] )
11065
111- const startHandleId = data . kind === 'loop' ? 'loop-start-source' : 'parallel-start-source'
112- const endHandleId = data . kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
113- const BlockIcon = data . kind === 'loop' ? RepeatIcon : SplitIcon
114- const blockIconBg = data . kind === 'loop' ? '#2FB3FF' : '#FEE12B'
115- const blockName = data . name || ( data . kind === 'loop' ? 'Loop' : 'Parallel' )
116-
117- /**
118- * Determine the ring styling based on subflow state priority:
119- * 1. Focused (selected in editor), selected (shift-click/box), or preview selected - blue ring
120- * 2. Diff status (version comparison) - green/orange ring
121- * 3. Run path status (execution result) - green/red ring
122- */
123- const isSelected = ! isPreview && selected
124- const hasRing =
125- isFocused ||
126- isSelected ||
127- isPreviewSelected ||
128- diffStatus === 'new' ||
129- diffStatus === 'edited' ||
130- ! ! runPathStatus
131-
132- /**
133- * Compute the ring color for the subflow selection indicator.
134- * Uses boxShadow (not CSS outline) to match the ring styling of regular workflow blocks.
135- * This works because ReactFlow renders child nodes as sibling divs at the viewport level
136- * (not as DOM children), so children at zIndex 1000 don't clip the parent's boxShadow.
137- */
138- const getRingColor = ( ) : string | undefined => {
139- if ( ! hasRing ) return undefined
140- if ( isFocused || isSelected || isPreviewSelected ) return 'var(--brand-secondary)'
141- if ( diffStatus === 'new' ) return 'var(--brand-accent)'
142- if ( diffStatus === 'edited' ) return 'var(--warning)'
143- if ( runPathStatus === 'success' ) {
144- return executionStatus ? 'var(--brand-accent)' : 'var(--border-success)'
145- }
146- if ( runPathStatus === 'error' ) return 'var(--text-error)'
147- return undefined
148- }
149- const ringColor = getRingColor ( )
150-
15166 return (
152- < div className = 'group pointer-events-none relative' >
153- < div
154- className = 'relative select-none rounded-lg border border-[var(--border-1)] transition-block-bg'
155- style = { {
156- width : data . width || 500 ,
157- height : data . height || 300 ,
158- overflow : 'visible' ,
159- pointerEvents : 'none' ,
160- ...( ringColor && {
161- boxShadow : `0 0 0 1.75px ${ ringColor } ` ,
162- } ) ,
163- } }
164- data-node-id = { id }
165- data-type = 'subflowNode'
166- data-nesting-level = { nestingLevel }
167- data-subflow-selected = { isFocused || isSelected || isPreviewSelected }
168- >
169- { ! isPreview && < ActionBar blockId = { id } blockType = { data . kind } disabled = { ! canEditWorkflow } /> }
170-
171- { /* Header Section */ }
172- < div
173- role = 'button'
174- tabIndex = { 0 }
175- aria-label = { `Select ${ blockName } ` }
176- onClick = { ( ) => setCurrentBlockId ( id ) }
177- onKeyDown = { ( event ) => handleKeyboardActivation ( event , ( ) => setCurrentBlockId ( id ) ) }
178- className = 'workflow-drag-handle flex cursor-grab items-center justify-between rounded-t-[8px] border-[var(--border)] border-b bg-[var(--surface-2)] py-2 pr-3 pl-2 [&:active]:cursor-grabbing'
179- style = { { pointerEvents : 'auto' } }
180- >
181- < div className = 'flex min-w-0 flex-1 items-center gap-2.5' >
182- < div
183- className = 'flex size-[24px] flex-shrink-0 items-center justify-center rounded-md'
184- style = { { backgroundColor : isEnabled ? blockIconBg : 'gray' } }
185- >
186- < BlockIcon className = 'size-[16px] text-white' />
187- </ div >
188- < span
189- className = { cn (
190- 'truncate font-medium text-md' ,
191- ! isEnabled && 'text-[var(--text-muted)]'
192- ) }
193- title = { blockName }
194- >
195- { blockName }
196- </ span >
197- </ div >
198- < div className = 'flex items-center gap-1' >
199- { ! isEnabled && < Badge variant = 'gray-secondary' > disabled</ Badge > }
200- { isLocked && < Badge variant = 'gray-secondary' > locked</ Badge > }
201- </ div >
202- </ div >
203-
204- { /*
205- * Subflow body background. Captures clicks to select the subflow in the
206- * panel editor, matching the header click behavior. Child nodes and edges
207- * are rendered as sibling divs at the viewport level by ReactFlow (not as
208- * DOM children), so enabling pointer events here doesn't block them.
209- */ }
210- < div
211- role = 'button'
212- tabIndex = { isPreview ? - 1 : 0 }
213- aria-label = { `Select ${ blockName } ` }
214- className = 'workflow-drag-handle absolute inset-0 top-[44px] cursor-grab rounded-b-[8px] [&:active]:cursor-grabbing'
215- style = { { pointerEvents : isPreview ? 'none' : 'auto' } }
216- onClick = { ( ) => setCurrentBlockId ( id ) }
217- onKeyDown = { ( event ) => handleKeyboardActivation ( event , ( ) => setCurrentBlockId ( id ) ) }
218- />
219-
220- { ! isPreview && canEditWorkflow && (
221- < div
222- role = 'separator'
223- aria-orientation = 'horizontal'
224- className = 'absolute right-[8px] bottom-2 z-20 flex size-[32px] cursor-se-resize items-center justify-center text-muted-foreground'
225- style = { { pointerEvents : 'auto' } }
226- />
227- ) }
228-
229- < div
230- className = 'relative h-[calc(100%-50px)] pt-4 pr-[80px] pb-4 pl-4'
231- data-dragarea = 'true'
232- style = { { pointerEvents : 'none' } }
233- >
234- { /* Subflow Start */ }
235- < div
236- className = 'absolute top-4 left-[16px] flex items-center justify-center rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)] px-3 py-1.5'
237- style = { { pointerEvents : isPreview ? 'none' : 'auto' } }
238- data-parent-id = { id }
239- data-node-role = { `${ data . kind } -start` }
240- data-extent = 'parent'
241- >
242- < span className = 'font-medium text-[var(--text-primary)] text-sm' > Start</ span >
243-
244- < Handle
245- type = 'source'
246- position = { Position . Right }
247- id = { startHandleId }
248- className = { getHandleClasses ( 'right' ) }
249- style = { {
250- top : '50%' ,
251- transform : 'translateY(-50%)' ,
252- pointerEvents : 'auto' ,
253- } }
254- data-parent-id = { id }
255- />
256- </ div >
257- </ div >
258-
259- { /* Input handle on left middle */ }
260- < Handle
261- type = 'target'
262- position = { Position . Left }
263- className = { getHandleClasses ( 'left' ) }
264- style = { {
265- ...HANDLE_STYLE ,
266- pointerEvents : 'auto' ,
267- } }
268- />
269-
270- { /* Output handle on right middle */ }
271- < Handle
272- type = 'source'
273- position = { Position . Right }
274- className = { getHandleClasses ( 'right' ) }
275- style = { {
276- ...HANDLE_STYLE ,
277- pointerEvents : 'auto' ,
278- } }
279- id = { endHandleId }
280- />
281- </ div >
282- </ div >
67+ < SubflowNodeView
68+ id = { id }
69+ data = { data }
70+ selected = { selected }
71+ isEnabled = { isEnabled }
72+ isLocked = { isLocked }
73+ isFocused = { isFocused }
74+ runPathStatus = { runPathStatus }
75+ diffStatus = { diffStatus }
76+ nestingLevel = { nestingLevel }
77+ canEditWorkflow = { canEditWorkflow }
78+ onSelect = { ( ) => setCurrentBlockId ( id ) }
79+ actionBar = { < ActionBar blockId = { id } blockType = { data . kind } disabled = { ! canEditWorkflow } /> }
80+ />
28381 )
28482} )
28583
0 commit comments