@@ -93,17 +93,13 @@ const edgeTypes: EdgeTypes = {
9393/** ReactFlow configuration constants. */
9494const defaultEdgeOptions = { type : 'custom' }
9595
96- /** Tailwind classes for ReactFlow internal element styling */
9796const reactFlowStyles = [
98- // Z-index layering
9997 '[&_.react-flow__edges]:!z-0' ,
10098 '[&_.react-flow__node]:!z-[21]' ,
10199 '[&_.react-flow__handle]:!z-[30]' ,
102100 '[&_.react-flow__edge-labels]:!z-[60]' ,
103- // Light mode: transparent pane to show dots
104101 '[&_.react-flow__pane]:!bg-transparent' ,
105102 '[&_.react-flow__renderer]:!bg-transparent' ,
106- // Dark mode: solid background, hide dots
107103 'dark:[&_.react-flow__pane]:!bg-[var(--bg)]' ,
108104 'dark:[&_.react-flow__renderer]:!bg-[var(--bg)]' ,
109105 'dark:[&_.react-flow__background]:hidden' ,
@@ -151,12 +147,23 @@ const WorkflowContent = React.memo(() => {
151147
152148 const addNotification = useNotificationStore ( ( state ) => state . addNotification )
153149
154- const { workflows, activeWorkflowId, hydration, setActiveWorkflow } = useWorkflowRegistry (
150+ const {
151+ workflows,
152+ activeWorkflowId,
153+ hydration,
154+ setActiveWorkflow,
155+ copyBlocks,
156+ preparePasteData,
157+ hasClipboard,
158+ } = useWorkflowRegistry (
155159 useShallow ( ( state ) => ( {
156160 workflows : state . workflows ,
157161 activeWorkflowId : state . activeWorkflowId ,
158162 hydration : state . hydration ,
159163 setActiveWorkflow : state . setActiveWorkflow ,
164+ copyBlocks : state . copyBlocks ,
165+ preparePasteData : state . preparePasteData ,
166+ hasClipboard : state . hasClipboard ,
160167 } ) )
161168 )
162169
@@ -340,12 +347,20 @@ const WorkflowContent = React.memo(() => {
340347 collaborativeAddEdge : addEdge ,
341348 collaborativeRemoveBlock : removeBlock ,
342349 collaborativeRemoveEdge : removeEdge ,
343- collaborativeUpdateBlockPosition ,
350+ collaborativeBatchUpdatePositions ,
344351 collaborativeUpdateParentId : updateParentId ,
352+ collaborativePasteBlocks,
345353 undo,
346354 redo,
347355 } = useCollaborativeWorkflow ( )
348356
357+ const updateBlockPosition = useCallback (
358+ ( id : string , position : { x : number ; y : number } ) => {
359+ collaborativeBatchUpdatePositions ( [ { id, position } ] )
360+ } ,
361+ [ collaborativeBatchUpdatePositions ]
362+ )
363+
349364 const { activeBlockIds, pendingBlocks, isDebugging } = useExecutionStore (
350365 useShallow ( ( state ) => ( {
351366 activeBlockIds : state . activeBlockIds ,
@@ -419,7 +434,7 @@ const WorkflowContent = React.memo(() => {
419434 const result = updateNodeParentUtil (
420435 nodeId ,
421436 newParentId ,
422- collaborativeUpdateBlockPosition ,
437+ updateBlockPosition ,
423438 updateParentId ,
424439 ( ) => resizeLoopNodesWrapper ( )
425440 )
@@ -443,7 +458,7 @@ const WorkflowContent = React.memo(() => {
443458 } ,
444459 [
445460 getNodes ,
446- collaborativeUpdateBlockPosition ,
461+ updateBlockPosition ,
447462 updateParentId ,
448463 blocks ,
449464 edgesForDisplay ,
@@ -495,6 +510,20 @@ const WorkflowContent = React.memo(() => {
495510 ) {
496511 event . preventDefault ( )
497512 redo ( )
513+ } else if ( ( event . ctrlKey || event . metaKey ) && event . key === 'c' ) {
514+ const selectedNodes = getNodes ( ) . filter ( ( node ) => node . selected )
515+ if ( selectedNodes . length > 0 ) {
516+ event . preventDefault ( )
517+ copyBlocks ( selectedNodes . map ( ( node ) => node . id ) )
518+ }
519+ } else if ( ( event . ctrlKey || event . metaKey ) && event . key === 'v' ) {
520+ if ( effectivePermissions . canEdit && hasClipboard ( ) ) {
521+ event . preventDefault ( )
522+ const pasteData = preparePasteData ( )
523+ if ( pasteData ) {
524+ collaborativePasteBlocks ( pasteData )
525+ }
526+ }
498527 }
499528 }
500529
@@ -504,7 +533,17 @@ const WorkflowContent = React.memo(() => {
504533 window . removeEventListener ( 'keydown' , handleKeyDown )
505534 if ( cleanup ) cleanup ( )
506535 }
507- } , [ debouncedAutoLayout , undo , redo ] )
536+ } , [
537+ debouncedAutoLayout ,
538+ undo ,
539+ redo ,
540+ getNodes ,
541+ copyBlocks ,
542+ preparePasteData ,
543+ collaborativePasteBlocks ,
544+ hasClipboard ,
545+ effectivePermissions . canEdit ,
546+ ] )
508547
509548 /**
510549 * Removes all edges connected to a block, skipping individual edge recording for undo/redo.
@@ -1673,21 +1712,12 @@ const WorkflowContent = React.memo(() => {
16731712 missingParentId : parentId ,
16741713 } )
16751714
1676- // Fix the node by removing its parent reference and calculating absolute position
16771715 const absolutePosition = getNodeAbsolutePosition ( id )
1678-
1679- // Update the node to remove parent reference and use absolute position
1680- collaborativeUpdateBlockPosition ( id , absolutePosition )
1716+ updateBlockPosition ( id , absolutePosition )
16811717 updateParentId ( id , '' , 'parent' )
16821718 }
16831719 } )
1684- } , [
1685- blocks ,
1686- collaborativeUpdateBlockPosition ,
1687- updateParentId ,
1688- getNodeAbsolutePosition ,
1689- isWorkflowReady ,
1690- ] )
1720+ } , [ blocks , updateBlockPosition , updateParentId , getNodeAbsolutePosition , isWorkflowReady ] )
16911721
16921722 /** Handles edge removal changes. */
16931723 const onEdgesChange = useCallback (
@@ -2095,9 +2125,7 @@ const WorkflowContent = React.memo(() => {
20952125 }
20962126 }
20972127
2098- // Emit collaborative position update for the final position
2099- // This ensures other users see the smooth final position
2100- collaborativeUpdateBlockPosition ( node . id , finalPosition , true )
2128+ updateBlockPosition ( node . id , finalPosition )
21012129
21022130 // Record single move entry on drag end to avoid micro-moves
21032131 const start = getDragStartPosition ( )
@@ -2218,7 +2246,7 @@ const WorkflowContent = React.memo(() => {
22182246 dragStartParentId ,
22192247 potentialParentId ,
22202248 updateNodeParent ,
2221- collaborativeUpdateBlockPosition ,
2249+ updateBlockPosition ,
22222250 addEdge ,
22232251 tryCreateAutoConnectEdge ,
22242252 blocks ,
@@ -2232,7 +2260,45 @@ const WorkflowContent = React.memo(() => {
22322260 ]
22332261 )
22342262
2235- /** Clears edge selection and panel state when clicking empty canvas. */
2263+ const onSelectionDragStop = useCallback (
2264+ ( _event : React . MouseEvent , nodes : any [ ] ) => {
2265+ if ( nodes . length === 0 ) return
2266+
2267+ const positionUpdates = nodes . map ( ( node ) => {
2268+ const currentBlock = blocks [ node . id ]
2269+ const currentParentId = currentBlock ?. data ?. parentId
2270+ let finalPosition = node . position
2271+
2272+ if ( currentParentId ) {
2273+ const parentNode = getNodes ( ) . find ( ( n ) => n . id === currentParentId )
2274+ if ( parentNode ) {
2275+ const containerDimensions = {
2276+ width : parentNode . data ?. width || CONTAINER_DIMENSIONS . DEFAULT_WIDTH ,
2277+ height : parentNode . data ?. height || CONTAINER_DIMENSIONS . DEFAULT_HEIGHT ,
2278+ }
2279+ const blockDimensions = {
2280+ width : BLOCK_DIMENSIONS . FIXED_WIDTH ,
2281+ height : Math . max (
2282+ currentBlock ?. height || BLOCK_DIMENSIONS . MIN_HEIGHT ,
2283+ BLOCK_DIMENSIONS . MIN_HEIGHT
2284+ ) ,
2285+ }
2286+ finalPosition = clampPositionToContainer (
2287+ node . position ,
2288+ containerDimensions ,
2289+ blockDimensions
2290+ )
2291+ }
2292+ }
2293+
2294+ return { id : node . id , position : finalPosition }
2295+ } )
2296+
2297+ collaborativeBatchUpdatePositions ( positionUpdates )
2298+ } ,
2299+ [ blocks , getNodes , collaborativeBatchUpdatePositions ]
2300+ )
2301+
22362302 const onPaneClick = useCallback ( ( ) => {
22372303 setSelectedEdgeInfo ( null )
22382304 usePanelEditorStore . getState ( ) . clearCurrentBlock ( )
@@ -2390,15 +2456,14 @@ const WorkflowContent = React.memo(() => {
23902456 proOptions = { reactFlowProOptions }
23912457 connectionLineStyle = { connectionLineStyle }
23922458 connectionLineType = { ConnectionLineType . SmoothStep }
2393- onNodeClick = { ( e , _node ) => {
2394- e . stopPropagation ( )
2395- } }
23962459 onPaneClick = { onPaneClick }
23972460 onEdgeClick = { onEdgeClick }
23982461 onPointerMove = { handleCanvasPointerMove }
23992462 onPointerLeave = { handleCanvasPointerLeave }
24002463 elementsSelectable = { true }
2401- selectNodesOnDrag = { false }
2464+ selectionOnDrag = { true }
2465+ panOnDrag = { [ 1 , 2 ] }
2466+ multiSelectionKeyCode = { [ 'Meta' , 'Control' ] }
24022467 nodesConnectable = { effectivePermissions . canEdit }
24032468 nodesDraggable = { effectivePermissions . canEdit }
24042469 draggable = { false }
@@ -2408,6 +2473,7 @@ const WorkflowContent = React.memo(() => {
24082473 className = { `workflow-container h-full bg-[var(--bg)] transition-opacity duration-150 ${ reactFlowStyles } ${ isCanvasReady ? 'opacity-100' : 'opacity-0' } ` }
24092474 onNodeDrag = { effectivePermissions . canEdit ? onNodeDrag : undefined }
24102475 onNodeDragStop = { effectivePermissions . canEdit ? onNodeDragStop : undefined }
2476+ onSelectionDragStop = { effectivePermissions . canEdit ? onSelectionDragStop : undefined }
24112477 onNodeDragStart = { effectivePermissions . canEdit ? onNodeDragStart : undefined }
24122478 snapToGrid = { snapToGrid }
24132479 snapGrid = { snapGrid }
0 commit comments