@@ -77,6 +77,60 @@ const LazyOAuthRequiredModal = lazy(() =>
7777
7878const logger = createLogger ( 'Workflow' )
7979
80+ const DEFAULT_PASTE_OFFSET = { x : 50 , y : 50 }
81+
82+ /**
83+ * Calculates the offset to paste blocks at viewport center
84+ */
85+ function calculatePasteOffset (
86+ clipboard : {
87+ blocks : Record < string , { position : { x : number ; y : number } ; type : string ; height ?: number } >
88+ } | null ,
89+ screenToFlowPosition : ( pos : { x : number ; y : number } ) => { x : number ; y : number }
90+ ) : { x : number ; y : number } {
91+ if ( ! clipboard ) return DEFAULT_PASTE_OFFSET
92+
93+ const clipboardBlocks = Object . values ( clipboard . blocks )
94+ if ( clipboardBlocks . length === 0 ) return DEFAULT_PASTE_OFFSET
95+
96+ // Calculate bounding box using proper dimensions
97+ const minX = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . x ) )
98+ const maxX = Math . max (
99+ ...clipboardBlocks . map ( ( b ) => {
100+ const width =
101+ b . type === 'loop' || b . type === 'parallel'
102+ ? CONTAINER_DIMENSIONS . DEFAULT_WIDTH
103+ : BLOCK_DIMENSIONS . FIXED_WIDTH
104+ return b . position . x + width
105+ } )
106+ )
107+ const minY = Math . min ( ...clipboardBlocks . map ( ( b ) => b . position . y ) )
108+ const maxY = Math . max (
109+ ...clipboardBlocks . map ( ( b ) => {
110+ const height =
111+ b . type === 'loop' || b . type === 'parallel'
112+ ? CONTAINER_DIMENSIONS . DEFAULT_HEIGHT
113+ : Math . max ( b . height || BLOCK_DIMENSIONS . MIN_HEIGHT , BLOCK_DIMENSIONS . MIN_HEIGHT )
114+ return b . position . y + height
115+ } )
116+ )
117+ const clipboardCenter = { x : ( minX + maxX ) / 2 , y : ( minY + maxY ) / 2 }
118+
119+ const flowContainer = document . querySelector ( '.react-flow' )
120+ if ( ! flowContainer ) return DEFAULT_PASTE_OFFSET
121+
122+ const rect = flowContainer . getBoundingClientRect ( )
123+ const viewportCenter = screenToFlowPosition ( {
124+ x : rect . width / 2 ,
125+ y : rect . height / 2 ,
126+ } )
127+
128+ return {
129+ x : viewportCenter . x - clipboardCenter . x ,
130+ y : viewportCenter . y - clipboardCenter . y ,
131+ }
132+ }
133+
80134/** Custom node types for ReactFlow. */
81135const nodeTypes : NodeTypes = {
82136 workflowBlock : WorkflowBlock ,
@@ -155,6 +209,7 @@ const WorkflowContent = React.memo(() => {
155209 copyBlocks,
156210 preparePasteData,
157211 hasClipboard,
212+ clipboard,
158213 } = useWorkflowRegistry (
159214 useShallow ( ( state ) => ( {
160215 workflows : state . workflows ,
@@ -164,6 +219,7 @@ const WorkflowContent = React.memo(() => {
164219 copyBlocks : state . copyBlocks ,
165220 preparePasteData : state . preparePasteData ,
166221 hasClipboard : state . hasClipboard ,
222+ clipboard : state . clipboard ,
167223 } ) )
168224 )
169225
@@ -557,7 +613,11 @@ const WorkflowContent = React.memo(() => {
557613 } else if ( ( event . ctrlKey || event . metaKey ) && event . key === 'v' ) {
558614 if ( effectivePermissions . canEdit && hasClipboard ( ) ) {
559615 event . preventDefault ( )
560- const pasteData = preparePasteData ( )
616+
617+ // Calculate offset to paste blocks at viewport center
618+ const pasteOffset = calculatePasteOffset ( clipboard , screenToFlowPosition )
619+
620+ const pasteData = preparePasteData ( pasteOffset )
561621 if ( pasteData ) {
562622 const pastedBlocks = Object . values ( pasteData . blocks )
563623 const hasTriggerInPaste = pastedBlocks . some ( ( block ) =>
@@ -608,6 +668,8 @@ const WorkflowContent = React.memo(() => {
608668 blocks ,
609669 addNotification ,
610670 activeWorkflowId ,
671+ clipboard ,
672+ screenToFlowPosition ,
611673 ] )
612674
613675 /**
0 commit comments