diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index f3abf994a2e..f5f71a0c97f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -6258,6 +6258,48 @@ describe('useCanvasOperations', () => { connections: {}, }; + it('centers a pasted node in the viewport instead of using the last click', async () => { + const nodes = [createTestNode({ name: 'Pasted', position: [100, 100] })]; + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation( + (importedNodes, connections) => + createTestWorkflowObject({ nodes: importedNodes, connections }), + ); + + const canvasOperations = useCanvasOperations(); + canvasOperations.lastClickPosition.value = [900, 900]; + const result = await canvasOperations.importWorkflowData( + { nodes, connections: {} }, + 'paste', + { viewport: { xMin: 0, yMin: 0, xMax: 1000, yMax: 1000 }, trackEvents: false }, + ); + + expect(result.nodes?.[0].position).toEqual([464, 464]); + }); + + it('centers the bounding box when pasting nodes with different leftmost and topmost nodes', async () => { + const nodes = [ + createTestNode({ name: 'Top', position: [400, 0] }), + createTestNode({ name: 'Left', position: [0, 200] }), + ]; + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation( + (importedNodes, connections) => + createTestWorkflowObject({ nodes: importedNodes, connections }), + ); + + const canvasOperations = useCanvasOperations(); + canvasOperations.lastClickPosition.value = [900, 900]; + const result = await canvasOperations.importWorkflowData( + { nodes, connections: {} }, + 'paste', + { viewport: { xMin: 0, yMin: 0, xMax: 1000, yMax: 1000 }, trackEvents: false }, + ); + + expect(result.nodes?.map((node) => node.position)).toEqual([ + [656, 352], + [256, 552], + ]); + }); + it.each(['paste', 'file', 'url'] as const)( 'strips empty groups before importing from %s when the feature is disabled', async (source) => { diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index fa42c866655..4307b866be0 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -3378,18 +3378,23 @@ export function useCanvasOperations() { // Fix the node position as it could be totally offscreen // and the pasted nodes would so not be directly visible to // the user + const groupSize = + workflowData.nodes && workflowData.nodes.length > 1 + ? getNodesGroupSize(workflowData.nodes) + : DEFAULT_NODE_SIZE; + const pastePosition: XYPosition = + source === 'paste' && viewport + ? [ + (viewport.xMin + viewport.xMax - groupSize[0]) / 2, + (viewport.yMin + viewport.yMax - groupSize[1]) / 2, + ] + : lastClickPosition.value; workflowHelpers.updateNodePositions( workflowData, - NodeViewUtils.getNewNodePosition( - workflowDocumentStore.value.allNodes, - lastClickPosition.value, - { - ...(workflowData.nodes && workflowData.nodes.length > 1 - ? { size: getNodesGroupSize(workflowData.nodes) } - : {}), - viewport, - }, - ), + NodeViewUtils.getNewNodePosition(workflowDocumentStore.value.allNodes, pastePosition, { + size: groupSize, + viewport, + }), ); if (ownsImportBulk) { diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts index fecfe1344ff..e612022f39d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts @@ -61,6 +61,22 @@ describe('useWorkflowHelpers', () => { workflowsStore.workflowId = ''; }); + describe('updateNodePositions', () => { + it('places the bounding-box top-left at the requested position', () => { + const nodes = [ + createTestNode({ name: 'Top', position: [400, 0] }), + createTestNode({ name: 'Left', position: [0, 200] }), + ]; + + useWorkflowHelpers().updateNodePositions({ nodes, connections: {} }, [256, 352]); + + expect(nodes.map((node) => node.position)).toEqual([ + [656, 352], + [256, 552], + ]); + }); + }); + describe('getNodeParametersWithResolvedExpressions', () => { it('should correctly detect and resolve expressions in a regular node ', async () => { const nodeParameters = { diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index be1a4d5fbc3..271ae4314f7 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -738,32 +738,22 @@ export function useWorkflowHelpers() { } } - // Updates the position of all the nodes that the top-left node - // is at the given position + // Updates all node positions so the group starts at the given position function updateNodePositions( workflowData: WorkflowData | WorkflowDataUpdate, position: XYPosition, ): void { - if (workflowData.nodes === undefined) { + if (!workflowData.nodes?.length) { return; } - // Find most top-left node - const minPosition = [99999999, 99999999]; + const minPosition = [Infinity, Infinity]; for (const node of workflowData.nodes) { - if (node.position[1] < minPosition[1]) { - minPosition[0] = node.position[0]; - minPosition[1] = node.position[1]; - } else if (node.position[1] === minPosition[1]) { - if (node.position[0] < minPosition[0]) { - minPosition[0] = node.position[0]; - minPosition[1] = node.position[1]; - } - } + minPosition[0] = Math.min(minPosition[0], node.position[0]); + minPosition[1] = Math.min(minPosition[1], node.position[1]); } - // Update the position on all nodes so that the - // most top-left one is at given position + // Shift the group's bounding-box top-left to the given position const offsetPosition = [position[0] - minPosition[0], position[1] - minPosition[1]]; for (const node of workflowData.nodes) { node.position[0] += offsetPosition[0];