@@ -66,7 +66,6 @@ import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
6666import { useAutoConnect , useSnapToGridSize } from '@/hooks/queries/general-settings'
6767import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
6868import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
69- import { usePermissionConfig } from '@/hooks/use-permission-config'
7069import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
7170import { useCanvasModeStore } from '@/stores/canvas-mode'
7271import { useChatStore } from '@/stores/chat/store'
@@ -99,34 +98,14 @@ const logger = createLogger('Workflow')
9998
10099const DEFAULT_PASTE_OFFSET = { x : 50 , y : 50 }
101100
102- /**
103- * Gets the center of the current viewport in flow coordinates
104- */
105- function getViewportCenter (
106- screenToFlowPosition : ( pos : { x : number ; y : number } ) => { x : number ; y : number }
107- ) : { x : number ; y : number } {
108- const flowContainer = document . querySelector ( '.react-flow' )
109- if ( ! flowContainer ) {
110- return screenToFlowPosition ( {
111- x : window . innerWidth / 2 ,
112- y : window . innerHeight / 2 ,
113- } )
114- }
115- const rect = flowContainer . getBoundingClientRect ( )
116- return screenToFlowPosition ( {
117- x : rect . width / 2 ,
118- y : rect . height / 2 ,
119- } )
120- }
121-
122101/**
123102 * Calculates the offset to paste blocks at viewport center
124103 */
125104function calculatePasteOffset (
126105 clipboard : {
127106 blocks : Record < string , { position : { x : number ; y : number } ; type : string ; height ?: number } >
128107 } | null ,
129- screenToFlowPosition : ( pos : { x : number ; y : number } ) => { x : number ; y : number }
108+ viewportCenter : { x : number ; y : number }
130109) : { x : number ; y : number } {
131110 if ( ! clipboard ) return DEFAULT_PASTE_OFFSET
132111
@@ -155,8 +134,6 @@ function calculatePasteOffset(
155134 )
156135 const clipboardCenter = { x : ( minX + maxX ) / 2 , y : ( minY + maxY ) / 2 }
157136
158- const viewportCenter = getViewportCenter ( screenToFlowPosition )
159-
160137 return {
161138 x : viewportCenter . x - clipboardCenter . x ,
162139 y : viewportCenter . y - clipboardCenter . y ,
@@ -266,7 +243,7 @@ const WorkflowContent = React.memo(() => {
266243 const router = useRouter ( )
267244 const reactFlowInstance = useReactFlow ( )
268245 const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
269- const { fitViewToBounds } = useCanvasViewport ( reactFlowInstance )
246+ const { fitViewToBounds, getViewportCenter } = useCanvasViewport ( reactFlowInstance )
270247 const { emitCursorUpdate } = useSocket ( )
271248
272249 const workspaceId = params . workspaceId as string
@@ -338,8 +315,6 @@ const WorkflowContent = React.memo(() => {
338315 const isVariablesOpen = useVariablesStore ( ( state ) => state . isOpen )
339316 const isChatOpen = useChatStore ( ( state ) => state . isChatOpen )
340317
341- // Permission config for invitation control
342- const { isInvitationsDisabled } = usePermissionConfig ( )
343318 const snapGrid : [ number , number ] = useMemo (
344319 ( ) => [ snapToGridSize , snapToGridSize ] ,
345320 [ snapToGridSize ]
@@ -901,11 +876,125 @@ const WorkflowContent = React.memo(() => {
901876 * Consolidates shared logic for context paste, duplicate, and keyboard paste.
902877 */
903878 const executePasteOperation = useCallback (
904- ( operation : 'paste' | 'duplicate' , pasteOffset : { x : number ; y : number } ) => {
905- const pasteData = preparePasteData ( pasteOffset )
879+ (
880+ operation : 'paste' | 'duplicate' ,
881+ pasteOffset : { x : number ; y : number } ,
882+ targetContainer ?: {
883+ loopId : string
884+ loopPosition : { x : number ; y : number }
885+ dimensions : { width : number ; height : number }
886+ } | null ,
887+ pasteTargetPosition ?: { x : number ; y : number }
888+ ) => {
889+ // For context menu paste into a subflow, calculate offset to center blocks at click position
890+ // Skip click-position centering if blocks came from inside a subflow (relative coordinates)
891+ let effectiveOffset = pasteOffset
892+ if ( targetContainer && pasteTargetPosition && clipboard ) {
893+ const clipboardBlocks = Object . values ( clipboard . blocks )
894+ // Only use click-position centering for top-level blocks (absolute coordinates)
895+ // Blocks with parentId have relative positions that can't be mixed with absolute click position
896+ const hasNestedBlocks = clipboardBlocks . some ( ( b ) => b . data ?. parentId )
897+ if ( clipboardBlocks . length > 0 && ! hasNestedBlocks ) {
898+ const minX = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . x ) )
899+ const maxX = Math . max (
900+ ...clipboardBlocks . map ( ( b ) => b . position . x + BLOCK_DIMENSIONS . FIXED_WIDTH )
901+ )
902+ const minY = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . y ) )
903+ const maxY = Math . max (
904+ ...clipboardBlocks . map ( ( b ) => b . position . y + BLOCK_DIMENSIONS . MIN_HEIGHT )
905+ )
906+ const clipboardCenter = { x : ( minX + maxX ) / 2 , y : ( minY + maxY ) / 2 }
907+ effectiveOffset = {
908+ x : pasteTargetPosition . x - clipboardCenter . x ,
909+ y : pasteTargetPosition . y - clipboardCenter . y ,
910+ }
911+ }
912+ }
913+
914+ const pasteData = preparePasteData ( effectiveOffset )
906915 if ( ! pasteData ) return
907916
908- const pastedBlocksArray = Object . values ( pasteData . blocks )
917+ let pastedBlocksArray = Object . values ( pasteData . blocks )
918+
919+ // If pasting into a subflow, adjust blocks to be children of that subflow
920+ if ( targetContainer ) {
921+ // Check if any pasted block is a trigger - triggers cannot be in subflows
922+ const hasTrigger = pastedBlocksArray . some ( ( b ) => TriggerUtils . isTriggerBlock ( b ) )
923+ if ( hasTrigger ) {
924+ addNotification ( {
925+ level : 'error' ,
926+ message : 'Triggers cannot be placed inside loop or parallel subflows.' ,
927+ workflowId : activeWorkflowId || undefined ,
928+ } )
929+ return
930+ }
931+
932+ // Check if any pasted block is a subflow - subflows cannot be nested
933+ const hasSubflow = pastedBlocksArray . some ( ( b ) => b . type === 'loop' || b . type === 'parallel' )
934+ if ( hasSubflow ) {
935+ addNotification ( {
936+ level : 'error' ,
937+ message : 'Subflows cannot be nested inside other subflows.' ,
938+ workflowId : activeWorkflowId || undefined ,
939+ } )
940+ return
941+ }
942+
943+ // Adjust each block's position to be relative to the container and set parentId
944+ pastedBlocksArray = pastedBlocksArray . map ( ( block ) => {
945+ // For blocks already nested (have parentId), positions are already relative - use as-is
946+ // For top-level blocks, convert absolute position to relative by subtracting container position
947+ const wasNested = Boolean ( block . data ?. parentId )
948+ const relativePosition = wasNested
949+ ? { x : block . position . x , y : block . position . y }
950+ : {
951+ x : block . position . x - targetContainer . loopPosition . x ,
952+ y : block . position . y - targetContainer . loopPosition . y ,
953+ }
954+
955+ // Clamp position to keep block inside container (below header)
956+ const clampedPosition = {
957+ x : Math . max (
958+ CONTAINER_DIMENSIONS . LEFT_PADDING ,
959+ Math . min (
960+ relativePosition . x ,
961+ targetContainer . dimensions . width -
962+ BLOCK_DIMENSIONS . FIXED_WIDTH -
963+ CONTAINER_DIMENSIONS . RIGHT_PADDING
964+ )
965+ ) ,
966+ y : Math . max (
967+ CONTAINER_DIMENSIONS . HEADER_HEIGHT + CONTAINER_DIMENSIONS . TOP_PADDING ,
968+ Math . min (
969+ relativePosition . y ,
970+ targetContainer . dimensions . height -
971+ BLOCK_DIMENSIONS . MIN_HEIGHT -
972+ CONTAINER_DIMENSIONS . BOTTOM_PADDING
973+ )
974+ ) ,
975+ }
976+
977+ return {
978+ ...block ,
979+ position : clampedPosition ,
980+ data : {
981+ ...block . data ,
982+ parentId : targetContainer . loopId ,
983+ extent : 'parent' ,
984+ } ,
985+ }
986+ } )
987+
988+ // Update pasteData.blocks with the modified blocks
989+ pasteData . blocks = pastedBlocksArray . reduce (
990+ ( acc , block ) => {
991+ acc [ block . id ] = block
992+ return acc
993+ } ,
994+ { } as Record < string , ( typeof pastedBlocksArray ) [ 0 ] >
995+ )
996+ }
997+
909998 const validation = validateTriggerPaste ( pastedBlocksArray , blocks , operation )
910999 if ( ! validation . isValid ) {
9111000 addNotification ( {
@@ -926,21 +1015,46 @@ const WorkflowContent = React.memo(() => {
9261015 pasteData . parallels ,
9271016 pasteData . subBlockValues
9281017 )
1018+
1019+ // Resize container if we pasted into a subflow
1020+ if ( targetContainer ) {
1021+ resizeLoopNodesWrapper ( )
1022+ }
9291023 } ,
9301024 [
9311025 preparePasteData ,
9321026 blocks ,
1027+ clipboard ,
9331028 addNotification ,
9341029 activeWorkflowId ,
9351030 collaborativeBatchAddBlocks ,
9361031 setPendingSelection ,
1032+ resizeLoopNodesWrapper ,
9371033 ]
9381034 )
9391035
9401036 const handleContextPaste = useCallback ( ( ) => {
9411037 if ( ! hasClipboard ( ) ) return
942- executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , screenToFlowPosition ) )
943- } , [ hasClipboard , executePasteOperation , clipboard , screenToFlowPosition ] )
1038+
1039+ // Convert context menu position to flow coordinates and check if inside a subflow
1040+ const flowPosition = screenToFlowPosition ( contextMenuPosition )
1041+ const targetContainer = isPointInLoopNode ( flowPosition )
1042+
1043+ executePasteOperation (
1044+ 'paste' ,
1045+ calculatePasteOffset ( clipboard , getViewportCenter ( ) ) ,
1046+ targetContainer ,
1047+ flowPosition // Pass the click position so blocks are centered at where user right-clicked
1048+ )
1049+ } , [
1050+ hasClipboard ,
1051+ executePasteOperation ,
1052+ clipboard ,
1053+ getViewportCenter ,
1054+ screenToFlowPosition ,
1055+ contextMenuPosition ,
1056+ isPointInLoopNode ,
1057+ ] )
9441058
9451059 const handleContextDuplicate = useCallback ( ( ) => {
9461060 copyBlocks ( contextMenuBlocks . map ( ( b ) => b . id ) )
@@ -1006,10 +1120,6 @@ const WorkflowContent = React.memo(() => {
10061120 setIsChatOpen ( ! isChatOpen )
10071121 } , [ ] )
10081122
1009- const handleContextInvite = useCallback ( ( ) => {
1010- window . dispatchEvent ( new CustomEvent ( 'open-invite-modal' ) )
1011- } , [ ] )
1012-
10131123 useEffect ( ( ) => {
10141124 let cleanup : ( ( ) => void ) | null = null
10151125
@@ -1054,7 +1164,7 @@ const WorkflowContent = React.memo(() => {
10541164 } else if ( ( event . ctrlKey || event . metaKey ) && event . key === 'v' ) {
10551165 if ( effectivePermissions . canEdit && hasClipboard ( ) ) {
10561166 event . preventDefault ( )
1057- executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , screenToFlowPosition ) )
1167+ executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , getViewportCenter ( ) ) )
10581168 }
10591169 }
10601170 }
@@ -1074,7 +1184,7 @@ const WorkflowContent = React.memo(() => {
10741184 hasClipboard ,
10751185 effectivePermissions . canEdit ,
10761186 clipboard ,
1077- screenToFlowPosition ,
1187+ getViewportCenter ,
10781188 executePasteOperation ,
10791189 ] )
10801190
@@ -1507,7 +1617,7 @@ const WorkflowContent = React.memo(() => {
15071617 if ( ! type ) return
15081618 if ( type === 'connectionBlock' ) return
15091619
1510- const basePosition = getViewportCenter ( screenToFlowPosition )
1620+ const basePosition = getViewportCenter ( )
15111621
15121622 if ( type === 'loop' || type === 'parallel' ) {
15131623 const id = crypto . randomUUID ( )
@@ -1576,7 +1686,7 @@ const WorkflowContent = React.memo(() => {
15761686 )
15771687 }
15781688 } , [
1579- screenToFlowPosition ,
1689+ getViewportCenter ,
15801690 blocks ,
15811691 addBlock ,
15821692 effectivePermissions . canEdit ,
0 commit comments