@@ -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,117 @@ 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+ let effectiveOffset = pasteOffset
891+ if ( targetContainer && pasteTargetPosition && clipboard ) {
892+ const clipboardBlocks = Object . values ( clipboard . blocks )
893+ if ( clipboardBlocks . length > 0 ) {
894+ const minX = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . x ) )
895+ const maxX = Math . max (
896+ ...clipboardBlocks . map ( ( b ) => b . position . x + BLOCK_DIMENSIONS . FIXED_WIDTH )
897+ )
898+ const minY = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . y ) )
899+ const maxY = Math . max (
900+ ...clipboardBlocks . map ( ( b ) => b . position . y + BLOCK_DIMENSIONS . MIN_HEIGHT )
901+ )
902+ const clipboardCenter = { x : ( minX + maxX ) / 2 , y : ( minY + maxY ) / 2 }
903+ effectiveOffset = {
904+ x : pasteTargetPosition . x - clipboardCenter . x ,
905+ y : pasteTargetPosition . y - clipboardCenter . y ,
906+ }
907+ }
908+ }
909+
910+ const pasteData = preparePasteData ( effectiveOffset )
906911 if ( ! pasteData ) return
907912
908- const pastedBlocksArray = Object . values ( pasteData . blocks )
913+ let pastedBlocksArray = Object . values ( pasteData . blocks )
914+
915+ // If pasting into a subflow, adjust blocks to be children of that subflow
916+ if ( targetContainer ) {
917+ // Check if any pasted block is a trigger - triggers cannot be in subflows
918+ const hasTrigger = pastedBlocksArray . some ( ( b ) => TriggerUtils . isTriggerBlock ( b ) )
919+ if ( hasTrigger ) {
920+ addNotification ( {
921+ level : 'error' ,
922+ message : 'Triggers cannot be placed inside loop or parallel subflows.' ,
923+ workflowId : activeWorkflowId || undefined ,
924+ } )
925+ return
926+ }
927+
928+ // Check if any pasted block is a subflow - subflows cannot be nested
929+ const hasSubflow = pastedBlocksArray . some ( ( b ) => b . type === 'loop' || b . type === 'parallel' )
930+ if ( hasSubflow ) {
931+ addNotification ( {
932+ level : 'error' ,
933+ message : 'Subflows cannot be nested inside other subflows.' ,
934+ workflowId : activeWorkflowId || undefined ,
935+ } )
936+ return
937+ }
938+
939+ // Adjust each block's position to be relative to the container and set parentId
940+ pastedBlocksArray = pastedBlocksArray . map ( ( block ) => {
941+ // Convert absolute position to relative position within the container
942+ const relativePosition = {
943+ x : block . position . x - targetContainer . loopPosition . x ,
944+ y : block . position . y - targetContainer . loopPosition . y ,
945+ }
946+
947+ // Clamp position to keep block inside container
948+ const clampedPosition = {
949+ x : Math . max (
950+ CONTAINER_DIMENSIONS . LEFT_PADDING ,
951+ Math . min (
952+ relativePosition . x ,
953+ targetContainer . dimensions . width -
954+ BLOCK_DIMENSIONS . FIXED_WIDTH -
955+ CONTAINER_DIMENSIONS . RIGHT_PADDING
956+ )
957+ ) ,
958+ y : Math . max (
959+ CONTAINER_DIMENSIONS . TOP_PADDING ,
960+ Math . min (
961+ relativePosition . y ,
962+ targetContainer . dimensions . height -
963+ BLOCK_DIMENSIONS . MIN_HEIGHT -
964+ CONTAINER_DIMENSIONS . BOTTOM_PADDING
965+ )
966+ ) ,
967+ }
968+
969+ return {
970+ ...block ,
971+ position : clampedPosition ,
972+ data : {
973+ ...block . data ,
974+ parentId : targetContainer . loopId ,
975+ extent : 'parent' ,
976+ } ,
977+ }
978+ } )
979+
980+ // Update pasteData.blocks with the modified blocks
981+ pasteData . blocks = pastedBlocksArray . reduce (
982+ ( acc , block ) => {
983+ acc [ block . id ] = block
984+ return acc
985+ } ,
986+ { } as Record < string , ( typeof pastedBlocksArray ) [ 0 ] >
987+ )
988+ }
989+
909990 const validation = validateTriggerPaste ( pastedBlocksArray , blocks , operation )
910991 if ( ! validation . isValid ) {
911992 addNotification ( {
@@ -926,21 +1007,46 @@ const WorkflowContent = React.memo(() => {
9261007 pasteData . parallels ,
9271008 pasteData . subBlockValues
9281009 )
1010+
1011+ // Resize container if we pasted into a subflow
1012+ if ( targetContainer ) {
1013+ resizeLoopNodesWrapper ( )
1014+ }
9291015 } ,
9301016 [
9311017 preparePasteData ,
9321018 blocks ,
1019+ clipboard ,
9331020 addNotification ,
9341021 activeWorkflowId ,
9351022 collaborativeBatchAddBlocks ,
9361023 setPendingSelection ,
1024+ resizeLoopNodesWrapper ,
9371025 ]
9381026 )
9391027
9401028 const handleContextPaste = useCallback ( ( ) => {
9411029 if ( ! hasClipboard ( ) ) return
942- executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , screenToFlowPosition ) )
943- } , [ hasClipboard , executePasteOperation , clipboard , screenToFlowPosition ] )
1030+
1031+ // Convert context menu position to flow coordinates and check if inside a subflow
1032+ const flowPosition = screenToFlowPosition ( contextMenuPosition )
1033+ const targetContainer = isPointInLoopNode ( flowPosition )
1034+
1035+ executePasteOperation (
1036+ 'paste' ,
1037+ calculatePasteOffset ( clipboard , getViewportCenter ( ) ) ,
1038+ targetContainer ,
1039+ flowPosition // Pass the click position so blocks are centered at where user right-clicked
1040+ )
1041+ } , [
1042+ hasClipboard ,
1043+ executePasteOperation ,
1044+ clipboard ,
1045+ getViewportCenter ,
1046+ screenToFlowPosition ,
1047+ contextMenuPosition ,
1048+ isPointInLoopNode ,
1049+ ] )
9441050
9451051 const handleContextDuplicate = useCallback ( ( ) => {
9461052 copyBlocks ( contextMenuBlocks . map ( ( b ) => b . id ) )
@@ -1006,10 +1112,6 @@ const WorkflowContent = React.memo(() => {
10061112 setIsChatOpen ( ! isChatOpen )
10071113 } , [ ] )
10081114
1009- const handleContextInvite = useCallback ( ( ) => {
1010- window . dispatchEvent ( new CustomEvent ( 'open-invite-modal' ) )
1011- } , [ ] )
1012-
10131115 useEffect ( ( ) => {
10141116 let cleanup : ( ( ) => void ) | null = null
10151117
@@ -1054,7 +1156,7 @@ const WorkflowContent = React.memo(() => {
10541156 } else if ( ( event . ctrlKey || event . metaKey ) && event . key === 'v' ) {
10551157 if ( effectivePermissions . canEdit && hasClipboard ( ) ) {
10561158 event . preventDefault ( )
1057- executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , screenToFlowPosition ) )
1159+ executePasteOperation ( 'paste' , calculatePasteOffset ( clipboard , getViewportCenter ( ) ) )
10581160 }
10591161 }
10601162 }
@@ -1074,7 +1176,7 @@ const WorkflowContent = React.memo(() => {
10741176 hasClipboard ,
10751177 effectivePermissions . canEdit ,
10761178 clipboard ,
1077- screenToFlowPosition ,
1179+ getViewportCenter ,
10781180 executePasteOperation ,
10791181 ] )
10801182
@@ -1507,7 +1609,7 @@ const WorkflowContent = React.memo(() => {
15071609 if ( ! type ) return
15081610 if ( type === 'connectionBlock' ) return
15091611
1510- const basePosition = getViewportCenter ( screenToFlowPosition )
1612+ const basePosition = getViewportCenter ( )
15111613
15121614 if ( type === 'loop' || type === 'parallel' ) {
15131615 const id = crypto . randomUUID ( )
@@ -1576,7 +1678,7 @@ const WorkflowContent = React.memo(() => {
15761678 )
15771679 }
15781680 } , [
1579- screenToFlowPosition ,
1681+ getViewportCenter ,
15801682 blocks ,
15811683 addBlock ,
15821684 effectivePermissions . canEdit ,
0 commit comments