mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix: Center pasted nodes in the canvas viewport (#39995)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
This commit is contained in:
parent
902b039c4d
commit
bd9e1d8947
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user