feat(editor): Update grid size to 16px for better alignment (#16869)

This commit is contained in:
Alex Grozav
2025-07-02 13:19:26 +03:00
committed by GitHub
parent 657e5a3b3a
commit 7ebde66eed
19 changed files with 315 additions and 199 deletions

View File

@@ -74,8 +74,8 @@ export function createCanvasGraphNode({
id = '1',
type = 'default',
label = 'Node',
position = { x: 100, y: 100 },
dimensions = { width: 100, height: 100 },
position = { x: 96, y: 96 },
dimensions = { width: 96, height: 96 },
data,
...rest
}: Partial<

View File

@@ -3,7 +3,7 @@ import { reactive, computed, toRefs } from 'vue';
import type { ActionTypeDescription, SimplifiedNodeType } from '@/Interface';
import { WEBHOOK_NODE_TYPE, DRAG_EVENT_DATA_KEY } from '@/constants';
import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils';
import { DEFAULT_NODE_SIZE, getNewNodePosition } from '@/utils/nodeViewUtils';
import NodeIcon from '@/components/NodeIcon.vue';
import { useViewStacks } from '../composables/useViewStacks';
@@ -76,7 +76,10 @@ function onDragOver(event: DragEvent): void {
return;
}
const [x, y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
const [x, y] = getNewNodePosition(
[],
[event.pageX - DEFAULT_NODE_SIZE[0] / 2, event.pageY - DEFAULT_NODE_SIZE[1] / 2],
);
state.draggablePosition = { x, y };
}

View File

@@ -127,7 +127,7 @@ describe('Canvas', () => {
[
{
id: '1',
position: { x: 120, y: 120 },
position: { x: 112, y: 112 },
},
],
],

View File

@@ -2,7 +2,7 @@
exports[`CanvasBackground > should render the background with the correct gap 1`] = `
"<svg class="vue-flow__background vue-flow__container" style="height: 100%; width: 100%;" data-test-id="canvas-background">
<pattern id="pattern-vue-flow-0" x="0" y="0" width="20" height="20" patternTransform="translate(-11,-11)" patternUnits="userSpaceOnUse">
<pattern id="pattern-vue-flow-0" x="0" y="0" width="16" height="16" patternTransform="translate(-9,-9)" patternUnits="userSpaceOnUse">
<circle cx="0.5" cy="0.5" r="0.5" fill="#aaa"></circle>
<!---->
</pattern>

View File

@@ -111,8 +111,8 @@ describe('CanvasNode', () => {
const inputHandles = getAllByTestId('canvas-node-input-handle');
expect(inputHandles[1]).toHaveStyle('left: 40px');
expect(inputHandles[2]).toHaveStyle('left: 160px');
expect(inputHandles[3]).toHaveStyle('left: 200px');
expect(inputHandles[2]).toHaveStyle('left: 168px');
expect(inputHandles[3]).toHaveStyle('left: 232px');
});
});

View File

@@ -34,7 +34,7 @@ import type { EventBus } from '@n8n/utils/event-bus';
import { createEventBus } from '@n8n/utils/event-bus';
import isEqual from 'lodash/isEqual';
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
import { GRID_SIZE } from '@/utils/nodeViewUtils';
import { CONFIGURATION_NODE_OFFSET, GRID_SIZE } from '@/utils/nodeViewUtils';
type Props = NodeProps<CanvasNodeData> & {
readOnly?: boolean;
@@ -186,7 +186,7 @@ const createEndpointMappingFn =
connectingHandle.value?.handleId === handleId;
const offsetValue =
position === Position.Bottom
? `${GRID_SIZE * (2 + index * 2)}px`
? `${GRID_SIZE * 2 * (1 + index * 2) + CONFIGURATION_NODE_OFFSET}px`
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
return {

View File

@@ -35,13 +35,12 @@ describe('CanvasNodeDefault', () => {
describe('inputs and outputs', () => {
it.each([
[1, 1, '100px'],
[3, 1, '100px'],
[4, 1, '140px'],
[1, 1, '100px'],
[1, 3, '100px'],
[1, 4, '140px'],
[4, 4, '140px'],
[1, 1, '96px'],
[1, 3, '128px'],
[1, 4, '160px'],
[3, 1, '128px'],
[4, 1, '160px'],
[4, 4, '160px'],
])(
'should adjust height css variable based on the number of inputs and outputs (%i inputs, %i outputs)',
(inputCount, outputCount, expected) => {
@@ -205,7 +204,7 @@ describe('CanvasNodeDefault', () => {
[
'1 required',
[{ type: NodeConnectionTypes.AiLanguageModel, index: 0, required: true }],
'240px',
'272px',
],
[
'2 required, 1 optional',
@@ -214,7 +213,7 @@ describe('CanvasNodeDefault', () => {
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
],
'240px',
'272px',
],
[
'2 required, 2 optional',
@@ -224,7 +223,7 @@ describe('CanvasNodeDefault', () => {
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
],
'240px',
'272px',
],
[
'1 required, 4 optional',
@@ -235,7 +234,7 @@ describe('CanvasNodeDefault', () => {
{ type: NodeConnectionTypes.AiMemory, index: 0 },
{ type: NodeConnectionTypes.AiMemory, index: 0 },
],
'280px',
'336px',
],
])(
'should adjust width css variable based on the number of non-main inputs (%s)',

View File

@@ -234,7 +234,8 @@ function onActivate(event: MouseEvent) {
&.configuration {
.icon {
margin-left: calc((var(--canvas-node--height) - var(--node-icon-size)) / 2);
// 4px represents calc(var(--handle--indicator--width) - configuration node offset) / 2)
margin-left: calc((var(--canvas-node--height) - var(--node-icon-size) - 4px) / 2);
}
&:not(.running) {

View File

@@ -4,7 +4,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
<div
class="node configurable"
data-test-id="canvas-configurable-node"
style="--canvas-node--width: 240px; --canvas-node--height: 100px; --node-icon-size: 40px;"
style="--canvas-node--width: 272px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
@@ -49,7 +49,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
<div
class="node configurable configuration"
data-test-id="canvas-configurable-node"
style="--canvas-node--width: 240px; --canvas-node--height: 75px; --node-icon-size: 30px;"
style="--canvas-node--width: 272px; --canvas-node--height: 80px; --node-icon-size: 30px;"
>
<!--v-if-->
<div
@@ -139,7 +139,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
<div
class="node"
data-test-id="canvas-default-node"
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
style="--canvas-node--width: 96px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
@@ -184,7 +184,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
<div
class="node trigger"
data-test-id="canvas-trigger-node"
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
style="--canvas-node--width: 96px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div

View File

@@ -3,26 +3,26 @@
exports[`useCanvasLayout > should layout a basic workflow 1`] = `
{
"boundingBox": {
"height": 100,
"width": 540,
"height": 96,
"width": 544,
"x": 0,
"y": 0,
},
"nodes": [
{
"id": "node1",
"x": 100,
"y": 100,
"x": 96,
"y": 96,
},
{
"id": "node2",
"x": 320,
"y": 100,
"y": 96,
},
{
"id": "node3",
"x": 540,
"y": 100,
"x": 544,
"y": 96,
},
],
}
@@ -31,26 +31,26 @@ exports[`useCanvasLayout > should layout a basic workflow 1`] = `
exports[`useCanvasLayout > should layout a basic workflow with selected nodes 1`] = `
{
"boundingBox": {
"height": 100,
"width": 540,
"height": 96,
"width": 544,
"x": 0,
"y": 0,
},
"nodes": [
{
"id": "node1",
"x": 100,
"y": 100,
"x": 96,
"y": 96,
},
{
"id": "node2",
"x": 320,
"y": 100,
"y": 96,
},
{
"id": "node3",
"x": 540,
"y": 100,
"x": 544,
"y": 96,
},
],
}
@@ -59,16 +59,16 @@ exports[`useCanvasLayout > should layout a basic workflow with selected nodes 1`
exports[`useCanvasLayout > should layout a workflow with AI nodes 1`] = `
{
"boundingBox": {
"height": 540,
"width": 820,
"height": 544,
"width": 832,
"x": 0,
"y": 220,
"y": 224,
},
"nodes": [
{
"id": "node1",
"x": 100,
"y": 100,
"x": 96,
"y": 96,
},
{
"id": "aiTool1",
@@ -77,28 +77,28 @@ exports[`useCanvasLayout > should layout a workflow with AI nodes 1`] = `
},
{
"id": "aiTool2",
"x": 460,
"x": 464,
"y": 320,
},
{
"id": "aiTool3",
"x": 600,
"y": 540,
"x": 608,
"y": 544,
},
{
"id": "aiAgent",
"x": 460,
"y": 100,
"x": 464,
"y": 96,
},
{
"id": "configurableAiTool",
"x": 600,
"x": 608,
"y": 320,
},
{
"id": "node2",
"x": 820,
"y": 100,
"x": 832,
"y": 96,
},
],
}
@@ -107,8 +107,8 @@ exports[`useCanvasLayout > should layout a workflow with AI nodes 1`] = `
exports[`useCanvasLayout > should layout a workflow with sticky notes 1`] = `
{
"boundingBox": {
"height": 100,
"width": 760,
"height": 96,
"width": 768,
"x": 0,
"y": 0,
},
@@ -120,22 +120,22 @@ exports[`useCanvasLayout > should layout a workflow with sticky notes 1`] = `
},
{
"id": "node2",
"x": 220,
"x": 224,
"y": 0,
},
{
"id": "node3",
"x": 440,
"x": 448,
"y": 0,
},
{
"id": "node4",
"x": 660,
"x": 672,
"y": 0,
},
{
"id": "sticky",
"x": 130,
"x": 134,
"y": -240,
},
],
@@ -145,7 +145,7 @@ exports[`useCanvasLayout > should layout a workflow with sticky notes 1`] = `
exports[`useCanvasLayout > should not reorder nodes vertically as it affects execution order 1`] = `
{
"boundingBox": {
"height": 300,
"height": 288,
"width": 320,
"x": 0,
"y": 0,
@@ -154,17 +154,17 @@ exports[`useCanvasLayout > should not reorder nodes vertically as it affects exe
{
"id": "node1",
"x": 0,
"y": -100,
"y": -112,
},
{
"id": "node3",
"x": 220,
"y": -200,
"x": 224,
"y": -208,
},
{
"id": "node2",
"x": 220,
"y": 0,
"x": 224,
"y": -16,
},
],
}

View File

@@ -55,6 +55,7 @@ describe('useCanvasLayout', () => {
const { layout } = createTestSetup(nodes, connections);
const result = layout('all');
expect(result).toMatchSnapshot();
expect(matchesGrid(result)).toBe(true);
});
@@ -153,8 +154,8 @@ describe('useCanvasLayout', () => {
test('should not reorder nodes vertically as it affects execution order', () => {
const nodes = [
createCanvasGraphNode({ id: 'node1', position: { x: 0, y: 0 } }),
createCanvasGraphNode({ id: 'node2', position: { x: 400, y: 200 } }),
createCanvasGraphNode({ id: 'node3', position: { x: 400, y: -200 } }),
createCanvasGraphNode({ id: 'node2', position: { x: 400, y: 208 } }),
createCanvasGraphNode({ id: 'node3', position: { x: 400, y: -208 } }),
];
const connections: Array<[string, string]> = [

View File

@@ -9,7 +9,7 @@ import {
type CanvasNodeData,
} from '../types';
import { isPresent } from '../utils/typesUtils';
import { GRID_SIZE, NODE_SIZE } from '../utils/nodeViewUtils';
import { DEFAULT_NODE_SIZE, GRID_SIZE } from '../utils/nodeViewUtils';
export type CanvasLayoutOptions = { id?: string };
export type CanvasLayoutTarget = 'selection' | 'all';
@@ -40,12 +40,12 @@ export type CanvasLayoutEvent = {
export type CanvasNodeDictionary = Record<string, GraphNode<CanvasNodeData>>;
const NODE_X_SPACING = GRID_SIZE * 6;
const NODE_Y_SPACING = GRID_SIZE * 5;
const NODE_X_SPACING = GRID_SIZE * 8;
const NODE_Y_SPACING = GRID_SIZE * 6;
const SUBGRAPH_SPACING = GRID_SIZE * 8;
const AI_X_SPACING = GRID_SIZE * 2;
const AI_Y_SPACING = GRID_SIZE * 6;
const STICKY_BOTTOM_PADDING = GRID_SIZE * 3;
const AI_X_SPACING = GRID_SIZE * 3;
const AI_Y_SPACING = GRID_SIZE * 8;
const STICKY_BOTTOM_PADDING = GRID_SIZE * 4;
export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
const {
@@ -113,7 +113,10 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
function createDagreSubGraph({
nodeIds,
parent,
}: { nodeIds: string[]; parent: dagre.graphlib.Graph }) {
}: {
nodeIds: string[];
parent: dagre.graphlib.Graph;
}) {
const subGraph = new dagre.graphlib.Graph();
subGraph.setGraph({
rankdir: 'LR',
@@ -165,7 +168,10 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
function createAiSubGraph({
parent,
nodeIds,
}: { parent: dagre.graphlib.Graph; nodeIds: string[] }) {
}: {
parent: dagre.graphlib.Graph;
nodeIds: string[];
}) {
const subGraph = new dagre.graphlib.Graph();
subGraph.setGraph({
rankdir: 'TB',
@@ -449,7 +455,7 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
const aiGraphBoundingBox = compositeBoundingBox(
aiNodes.map((nodeId) => boundingBoxByNodeId[nodeId]).filter(isPresent),
);
const aiNodeVerticalCorrection = aiGraphBoundingBox.height / 2 - NODE_SIZE / 2;
const aiNodeVerticalCorrection = aiGraphBoundingBox.height / 2 - DEFAULT_NODE_SIZE[0] / 2;
aiGraphBoundingBox.y += aiNodeVerticalCorrection;
const hasConflictingNodes = Object.entries(boundingBoxByNodeId)

View File

@@ -200,12 +200,12 @@ describe('useCanvasOperations', () => {
{
type: 'type',
typeVersion: 1,
position: [20, 20],
position: [32, 32],
},
mockNodeTypeDescription({ name: 'type' }),
);
expect(result.position).toEqual([20, 20]);
expect(result.position).toEqual([32, 32]);
});
it('should not assign credentials when multiple credentials are available', () => {
@@ -274,13 +274,13 @@ describe('useCanvasOperations', () => {
describe('resolveNodePosition', () => {
it('should return the node position if it is already set', () => {
const node = createTestNode({ position: [100, 100] });
const node = createTestNode({ position: [112, 112] });
const nodeTypeDescription = mockNodeTypeDescription();
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition(node, nodeTypeDescription);
expect(position).toEqual([100, 100]);
expect(position).toEqual([112, 112]);
});
it('should place the node at the last cancelled connection position', () => {
@@ -302,7 +302,7 @@ describe('useCanvasOperations', () => {
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([200, 160]);
expect(position).toEqual([208, 160]);
expect(uiStore.lastCancelledConnectionPosition).toBeUndefined();
});
@@ -316,7 +316,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
uiStore.lastInteractedWithNode = createTestNode({
position: [100, 100],
position: [112, 112],
type: 'test',
typeVersion: 1,
});
@@ -327,7 +327,7 @@ describe('useCanvasOperations', () => {
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([320, 100]);
expect(position).toEqual([320, 112]);
});
it('should place the node below the last interacted with node if it has non-main outputs', () => {
@@ -340,7 +340,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
uiStore.lastInteractedWithNode = createTestNode({
position: [100, 100],
position: [96, 96],
type: 'test',
typeVersion: 1,
});
@@ -358,7 +358,7 @@ describe('useCanvasOperations', () => {
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([460, 100]);
expect(position).toEqual([448, 96]);
});
it('should place the node at the last clicked position if no other position is set', () => {
@@ -367,16 +367,14 @@ describe('useCanvasOperations', () => {
const node = createTestNode({ id: '0' });
const nodeTypeDescription = mockNodeTypeDescription();
workflowsStore.workflowTriggerNodes = [
createTestNode({ id: 'trigger', position: [100, 100] }),
];
workflowsStore.workflowTriggerNodes = [createTestNode({ id: 'trigger', position: [96, 96] })];
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
lastClickPosition.value = [300, 300];
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([300, 300]);
expect(position).toEqual([304, 304]); // Snapped to grid
});
it('should place the trigger node at the root if it is the first trigger node', () => {
@@ -532,8 +530,8 @@ describe('useCanvasOperations', () => {
it('records history for multiple node position updates when tracking is enabled', () => {
const historyStore = useHistoryStore();
const events = [
{ id: 'node1', position: { x: 100, y: 100 } },
{ id: 'node2', position: { x: 200, y: 200 } },
{ id: 'node1', position: { x: 96, y: 96 } },
{ id: 'node2', position: { x: 208, y: 208 } },
];
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
@@ -548,8 +546,8 @@ describe('useCanvasOperations', () => {
it('updates positions for multiple nodes', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const events = [
{ id: 'node1', position: { x: 100, y: 100 } },
{ id: 'node2', position: { x: 200, y: 200 } },
{ id: 'node1', position: { x: 96, y: 96 } },
{ id: 'node2', position: { x: 208, y: 208 } },
];
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
workflowsStore.getNodeById
@@ -570,13 +568,13 @@ describe('useCanvasOperations', () => {
updateNodesPosition(events);
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
});
it('does not record history when trackHistory is false', () => {
const historyStore = useHistoryStore();
const events = [{ id: 'node1', position: { x: 100, y: 100 } }];
const events = [{ id: 'node1', position: { x: 96, y: 96 } }];
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
@@ -596,10 +594,10 @@ describe('useCanvasOperations', () => {
target: 'all',
result: {
nodes: [
{ id: 'node1', x: 100, y: 100 },
{ id: 'node2', x: 200, y: 200 },
{ id: 'node1', x: 96, y: 96 },
{ id: 'node2', x: 208, y: 208 },
],
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
boundingBox: { height: 96, width: 96, x: 0, y: 0 },
},
};
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
@@ -619,10 +617,10 @@ describe('useCanvasOperations', () => {
target: 'all',
result: {
nodes: [
{ id: 'node1', x: 100, y: 100 },
{ id: 'node2', x: 200, y: 200 },
{ id: 'node1', x: 96, y: 96 },
{ id: 'node2', x: 208, y: 208 },
],
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
boundingBox: { height: 96, width: 96, x: 0, y: 0 },
},
};
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
@@ -644,8 +642,8 @@ describe('useCanvasOperations', () => {
tidyUp(event);
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
});
it('should send a "User tidied up workflow" telemetry event', () => {
@@ -654,10 +652,10 @@ describe('useCanvasOperations', () => {
target: 'all',
result: {
nodes: [
{ id: 'node1', x: 100, y: 100 },
{ id: 'node2', x: 200, y: 200 },
{ id: 'node1', x: 96, y: 96 },
{ id: 'node2', x: 208, y: 208 },
],
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
boundingBox: { height: 96, width: 96, x: 0, y: 0 },
},
};
@@ -738,8 +736,8 @@ describe('useCanvasOperations', () => {
const nodeTypesStore = useNodeTypesStore();
const nodeTypeName = 'type';
const nodes = [
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
mockNode({ name: 'Node 1', type: nodeTypeName, position: [32, 32] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 256] }),
];
workflowsStore.getCurrentWorkflow.mockReturnValue(
@@ -758,14 +756,14 @@ describe('useCanvasOperations', () => {
name: nodes[0].name,
type: nodeTypeName,
typeVersion: 1,
position: [40, 40],
position: [32, 32],
parameters: {},
});
expect(workflowsStore.addNode.mock.calls[1][0]).toMatchObject({
name: nodes[1].name,
type: nodeTypeName,
typeVersion: 1,
position: [100, 240],
position: [96, 256],
parameters: {},
});
});
@@ -775,8 +773,8 @@ describe('useCanvasOperations', () => {
const nodeTypesStore = mockedStore(useNodeTypesStore);
const nodeTypeName = 'type';
const nodes = [
mockNode({ name: 'Node 1', type: nodeTypeName, position: [120, 120] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [180, 320] }),
mockNode({ name: 'Node 1', type: nodeTypeName, position: [128, 128] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [192, 320] }),
];
workflowsStore.getCurrentWorkflow.mockReturnValue(
@@ -804,9 +802,9 @@ describe('useCanvasOperations', () => {
const nodeTypesStore = useNodeTypesStore();
const nodeTypeName = 'type';
const nodes = [
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [100, 240] }),
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [32, 32] }),
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [32, 32] }),
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [96, 256] }),
];
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
@@ -842,7 +840,7 @@ describe('useCanvasOperations', () => {
const nodeTypeName = 'type';
const nodes = [
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 240] }),
];
workflowsStore.getCurrentWorkflow.mockReturnValue(
@@ -3007,8 +3005,8 @@ describe('useCanvasOperations', () => {
// Create three nodes in a sequence: A -> B -> C
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
const nodeTypeDescription = mockNodeTypeDescription({
name: nodeA.type,
@@ -3077,8 +3075,8 @@ describe('useCanvasOperations', () => {
// Create three nodes in a sequence: A -> B -> C
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
const nodeTypeDescription = mockNodeTypeDescription({
name: nodeA.type,
@@ -3144,8 +3142,8 @@ describe('useCanvasOperations', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
// Create nodes: B -> C (no incoming to B)
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
workflowsStore.workflow.nodes = [nodeB, nodeC];
workflowsStore.workflow.connections = {
@@ -3173,7 +3171,7 @@ describe('useCanvasOperations', () => {
// Create nodes: A -> B (no outgoing from B)
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
workflowsStore.workflow.nodes = [nodeA, nodeB];
workflowsStore.workflow.connections = {
@@ -3220,7 +3218,7 @@ describe('useCanvasOperations', () => {
const nodeB: IWorkflowTemplateNode = createTestNode({
id: 'Y',
name: 'Node Y',
position: [180, 80],
position: [192, 80],
});
const workflow: IWorkflowTemplate['workflow'] = {

View File

@@ -71,6 +71,7 @@ import {
} from '@/utils/canvasUtils';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import {
GRID_SIZE,
CONFIGURABLE_NODE_SIZE,
CONFIGURATION_NODE_SIZE,
DEFAULT_NODE_SIZE,
@@ -692,7 +693,7 @@ export function useCanvasOperations() {
// When we're adding multiple nodes, increment the X position for the next one
insertPosition = [
lastAddedNode.position[0] + NodeViewUtils.NODE_SIZE * 2 + NodeViewUtils.GRID_SIZE,
lastAddedNode.position[0] + DEFAULT_NODE_SIZE[0] * 2 + GRID_SIZE,
lastAddedNode.position[1],
];
}
@@ -1108,8 +1109,8 @@ export function useCanvasOperations() {
if (lastInteractedWithNodeMainOutputs.length > 1) {
const yOffsetValues = generateOffsets(
lastInteractedWithNodeMainOutputs.length,
NodeViewUtils.NODE_SIZE,
NodeViewUtils.GRID_SIZE,
DEFAULT_NODE_SIZE[1],
GRID_SIZE,
);
yOffset = yOffsetValues[connectionIndex];

View File

@@ -116,7 +116,7 @@ export const EXPRESSIONS_DOCS_URL = `https://${DOCS_DOMAIN}/code-examples/expres
export const N8N_PRICING_PAGE_URL = 'https://n8n.io/pricing';
export const N8N_MAIN_GITHUB_REPO_URL = 'https://github.com/n8n-io/n8n';
export const NODE_MIN_INPUT_ITEMS_COUNT = 5;
export const NODE_MIN_INPUT_ITEMS_COUNT = 4;
// node types
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';

View File

@@ -102,6 +102,7 @@ import { updateCurrentUserSettings } from '@/api/users';
import { useExecutingNode } from '@/composables/useExecutingNode';
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
import { isChatNode } from '@/utils/aiUtils';
import { snapPositionToGrid } from '@/utils/nodeViewUtils';
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '',
@@ -1290,6 +1291,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
node.type = getCredentialOnlyNodeTypeName(node.extendsCredential);
}
if (node.position) {
node.position = snapPositionToGrid(node.position);
}
if (!nodeMetadata.value[node.name]) {
nodeMetadata.value[node.name] = { pristine: true };
}

View File

@@ -992,7 +992,7 @@ describe('insertSpacersBetweenEndpoints', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([{ index: 0, required: true }, null, null, null, null]);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
});
it('should not insert spacers when there are at least min endpoints count', () => {
@@ -1012,14 +1012,14 @@ describe('insertSpacersBetweenEndpoints', () => {
const endpoints = [{ index: 0, required: false }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([null, null, null, null, { index: 0, required: false }]);
expect(result).toEqual([null, null, null, { index: 0, required: false }]);
});
it('should handle no endpoints', () => {
const endpoints: Array<{ index: number; required: boolean }> = [];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([null, null, null, null, null]);
expect(result).toEqual([null, null, null, null]);
});
it('should handle required endpoints greater than NODE_MIN_INPUT_ITEMS_COUNT', () => {
@@ -1040,7 +1040,6 @@ describe('insertSpacersBetweenEndpoints', () => {
{ index: 0, required: true },
{ index: 1, required: true },
null,
null,
{ index: 2 },
]);
});
@@ -1049,6 +1048,6 @@ describe('insertSpacersBetweenEndpoints', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([{ index: 0, required: true }, null, null, null, null]);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
});
});

View File

@@ -7,8 +7,11 @@ import {
generateOffsets,
getGenericHints,
getNewNodePosition,
NODE_SIZE,
updateViewportToContainNodes,
DEFAULT_NODE_SIZE,
snapPositionToGrid,
calculateNodeSize,
GRID_SIZE,
} from './nodeViewUtils';
import type { INode, INodeTypeDescription, INodeExecutionData, Workflow } from 'n8n-workflow';
import type { INodeUi, XYPosition } from '@/Interface';
@@ -203,96 +206,112 @@ describe('getGenericHints', () => {
describe('generateOffsets', () => {
it('should return correct offsets for 0 nodes', () => {
const result = generateOffsets(0, 100, 20);
const result = generateOffsets(0, 96, GRID_SIZE);
expect(result).toEqual([]);
});
it('should return correct offsets for 1 node', () => {
const result = generateOffsets(1, 100, 20);
const result = generateOffsets(1, 96, GRID_SIZE);
expect(result).toEqual([0]);
});
it('should return correct offsets for 2 nodes', () => {
const result = generateOffsets(2, 100, 20);
expect(result).toEqual([-100, 100]);
const result = generateOffsets(2, 96, GRID_SIZE);
expect(result).toEqual([-96, 96]);
});
it('should return correct offsets for 3 nodes', () => {
const result = generateOffsets(3, 100, 20);
expect(result).toEqual([-120, 0, 120]);
const result = generateOffsets(3, 96, GRID_SIZE);
expect(result).toEqual([-112, 0, 112]);
});
it('should return correct offsets for 4 nodes', () => {
const result = generateOffsets(4, 100, 20);
expect(result).toEqual([-220, -100, 100, 220]);
const result = generateOffsets(4, 96, GRID_SIZE);
expect(result).toEqual([-208, -96, 96, 208]);
});
it('should return correct offsets for large node count', () => {
const result = generateOffsets(10, 100, 20);
expect(result).toEqual([-580, -460, -340, -220, -100, 100, 220, 340, 460, 580]);
const result = generateOffsets(10, 96, GRID_SIZE);
expect(result).toEqual([-544, -432, -320, -208, -96, 96, 208, 320, 432, 544]);
});
});
describe('snapPositionToGrid', () => {
it('should snap position to grid', () => {
const position: XYPosition = [105, 115];
const snappedPosition = snapPositionToGrid(position);
expect(snappedPosition).toEqual([112, 128]);
});
it('should not change position if already on grid', () => {
const position: XYPosition = [96, 96];
const snappedPosition = snapPositionToGrid(position);
expect(snappedPosition).toEqual([96, 96]);
});
it('should handle negative positions', () => {
const position: XYPosition = [-15, -25];
const snappedPosition = snapPositionToGrid(position);
expect(snappedPosition).toEqual([-16, -32]);
});
});
describe('getNewNodePosition', () => {
it('should return the new position when there are no conflicts', () => {
const nodes: INodeUi[] = [];
const newPosition: XYPosition = [100, 100];
const newPosition: XYPosition = [96, 96];
const result = getNewNodePosition(nodes, newPosition);
expect(result).toEqual([100, 100]);
expect(result).toEqual([96, 96]);
});
it('should adjust the position to the closest grid size', () => {
const nodes: INodeUi[] = [];
const newPosition: XYPosition = [105, 115];
const result = getNewNodePosition(nodes, newPosition);
expect(result).toEqual([120, 120]);
expect(result).toEqual([112, 128]);
});
it('should move the position to avoid conflicts', () => {
const nodes: INodeUi[] = [
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
];
const newPosition: XYPosition = [100, 100];
const nodes: INodeUi[] = [createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE })];
const newPosition: XYPosition = [96, 96];
const result = getNewNodePosition(nodes, newPosition);
expect(result).toEqual([220, 220]);
expect(result).toEqual([240, 240]);
});
it('should skip nodes in the conflict allowlist', () => {
const nodes: INodeUi[] = [
createTestNode({ id: '1', position: [100, 100], type: STICKY_NODE_TYPE }),
createTestNode({ id: '1', position: [96, 96], type: STICKY_NODE_TYPE }),
];
const newPosition: XYPosition = [100, 100];
const newPosition: XYPosition = [96, 96];
const result = getNewNodePosition(nodes, newPosition);
expect(result).toEqual([100, 100]);
expect(result).toEqual([96, 96]);
});
it('should use the provided move position to resolve conflicts', () => {
const nodes: INodeUi[] = [
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
];
const newPosition: XYPosition = [100, 100];
const movePosition: XYPosition = [50, 50];
const nodes: INodeUi[] = [createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE })];
const newPosition: XYPosition = [96, 96];
const movePosition: XYPosition = [48, 48];
const result = getNewNodePosition(nodes, newPosition, {
offset: movePosition,
});
expect(result).toEqual([220, 220]);
expect(result).toEqual([240, 240]);
});
it('should handle multiple conflicts correctly', () => {
const nodes: INodeUi[] = [
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
createTestNode({ id: '2', position: [140, 140], type: SET_NODE_TYPE }),
createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE }),
createTestNode({ id: '2', position: [144, 144], type: SET_NODE_TYPE }),
];
const newPosition: XYPosition = [100, 100];
const newPosition: XYPosition = [96, 96];
const result = getNewNodePosition(nodes, newPosition);
expect(result).toEqual([280, 280]);
expect(result).toEqual([288, 288]);
});
});
const testNodes: INode[] = [
createTestNode({ id: 'a', position: [0, 0] }),
createTestNode({ id: 'b', position: [100, 50] }),
createTestNode({ id: 'c', position: [50, 100] }),
createTestNode({ id: 'b', position: [96, 50] }),
createTestNode({ id: 'c', position: [50, 96] }),
createTestNode({ id: 'd', position: [-20, -10] }),
];
@@ -379,15 +398,15 @@ describe('getBottomMostNode', () => {
describe('getNodesGroupSize', () => {
it('calculates the group size correctly', () => {
const [width, height] = getNodesGroupSize(testNodes);
expect(width).toBe(Math.abs(100 - -20) + NODE_SIZE);
expect(height).toBe(Math.abs(-10 - 100) + NODE_SIZE);
expect(width).toBe(Math.abs(96 - -20) + DEFAULT_NODE_SIZE[0]);
expect(height).toBe(Math.abs(-10 - 96) + DEFAULT_NODE_SIZE[1]);
});
it('should handle a single node', () => {
const single = [testNodes[0]];
const [w, h] = getNodesGroupSize(single);
expect(w).toBe(NODE_SIZE);
expect(h).toBe(NODE_SIZE);
expect(w).toBe(DEFAULT_NODE_SIZE[0]);
expect(h).toBe(DEFAULT_NODE_SIZE[1]);
});
it('should handle nodes with equal positions', () => {
@@ -396,12 +415,12 @@ describe('getNodesGroupSize', () => {
createTestNode({ id: 'y', position: [10, 20] }),
];
const [we, he] = getNodesGroupSize(equalNodes);
expect(we).toBe(NODE_SIZE);
expect(he).toBe(NODE_SIZE);
expect(we).toBe(DEFAULT_NODE_SIZE[0]);
expect(he).toBe(DEFAULT_NODE_SIZE[1]);
});
});
describe(updateViewportToContainNodes, () => {
describe('updateViewportToContainNodes', () => {
it('should return the same viewport if given node is already in the viewport', () => {
const result = updateViewportToContainNodes(
{ x: 0, y: 0, zoom: 2 },
@@ -446,6 +465,82 @@ describe(updateViewportToContainNodes, () => {
});
});
describe('calculateNodeSize', () => {
it('should return configuration node size when isConfiguration is true and isConfigurable is false', () => {
const result = calculateNodeSize(
true, // isConfiguration
false, // isConfigurable
1,
1,
0,
);
// width = GRID_SIZE * 5 = 16 * 5 = 80
// height = GRID_SIZE * 5 = 16 * 5 = 80
expect(result).toEqual({ width: 80, height: 80 });
});
it('should return configurable node size when isConfigurable is true and isConfiguration is false', () => {
const nonMainInputCount = 5;
const mainInputCount = 3;
const mainOutputCount = 2;
// width = max(4, 5) * 2 * 16 * 2 = 5 * 2 * 16 * 2 + offset = 336
// height = DEFAULT_NODE_SIZE[1] + max(0, max(3,2,1) - 2) * 16 * 2
// maxVerticalHandles = 3
// height = 96 + (3 - 2) * 32 = 96 + 32 = 128
expect(
calculateNodeSize(false, true, mainInputCount, mainOutputCount, nonMainInputCount),
).toEqual({ width: 336, height: 128 });
});
it('should return configurable configuration node size when both isConfigurable and isConfiguration are true', () => {
const nonMainInputCount = 2;
// width = max(4, 2) * 2 * 16 * 2 = 4 * 2 * 16 * 2 + offset = 272
// height = CONFIGURATION_NODE_SIZE[1] = 16 * 5 = 80
expect(calculateNodeSize(true, true, 1, 1, nonMainInputCount)).toEqual({
width: 272,
height: 80,
});
});
it('should return default node size when neither isConfigurable nor isConfiguration are true', () => {
const mainInputCount = 3;
const mainOutputCount = 2;
// width = 96
// maxVerticalHandles = 3
// height = 96 + (3 - 2) * 32 = 128
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0)).toEqual({
width: 96,
height: 128,
});
});
it('should calculate height based on the max of mainInputCount and mainOutputCount', () => {
const mainInputCount = 6;
const mainOutputCount = 4;
// maxVerticalHandles = 6
// height = 96 + (6 - 2) * 32 = 96 + 128 = 224
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0).height).toBe(224);
});
it('should respect the minimum width for configurable nodes', () => {
const nonMainInputCount = 2; // less than NODE_MIN_INPUT_ITEMS_COUNT
// width = 4 * 2 * 16 * 2 + offset = 272
// height = default path, mainInputCount = 1, mainOutputCount = 1
// maxVerticalHandles = 1
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
expect(calculateNodeSize(false, true, 1, 1, nonMainInputCount)).toEqual({
width: 272,
height: 96,
});
});
it('should handle edge case when mainInputCount and mainOutputCount are 0', () => {
// maxVerticalHandles = max(0,0,1) = 1
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
expect(calculateNodeSize(false, false, 0, 0, 0).height).toBe(96);
});
});
function createTestGraphNode(data: Partial<GraphNode> = {}): GraphNode {
return {
computedPosition: { z: 0, ...(data.position ?? { x: 0, y: 0 }) },

View File

@@ -32,17 +32,15 @@ import {
* Canvas constants and functions
*/
export const GRID_SIZE = 20;
export const GRID_SIZE = 16;
export const NODE_SIZE = GRID_SIZE * 5;
export const DEFAULT_NODE_SIZE: [number, number] = [GRID_SIZE * 5, GRID_SIZE * 5];
export const CONFIGURATION_NODE_SIZE: [number, number] = [GRID_SIZE * 4, GRID_SIZE * 4];
export const CONFIGURABLE_NODE_SIZE: [number, number] = [GRID_SIZE * 12, GRID_SIZE * 5];
export const DEFAULT_START_POSITION_X = GRID_SIZE * 9;
export const DEFAULT_START_POSITION_Y = GRID_SIZE * 12;
export const DEFAULT_NODE_SIZE: [number, number] = [GRID_SIZE * 6, GRID_SIZE * 6];
export const CONFIGURATION_NODE_SIZE: [number, number] = [GRID_SIZE * 5, GRID_SIZE * 5];
export const CONFIGURABLE_NODE_SIZE: [number, number] = [GRID_SIZE * 16, GRID_SIZE * 6];
export const DEFAULT_START_POSITION_X = GRID_SIZE * 11;
export const DEFAULT_START_POSITION_Y = GRID_SIZE * 15;
export const HEADER_HEIGHT = 65;
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = GRID_SIZE * 15;
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
export const PUSH_NODES_OFFSET = DEFAULT_NODE_SIZE[0] * 2 + GRID_SIZE;
export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
xMin: -Infinity,
yMin: -Infinity,
@@ -50,6 +48,10 @@ export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
yMax: Infinity,
};
// The top-center of the configuration node is not a multiple of GRID_SIZE,
// therefore we need to offset non-main inputs to align with the nodes top-center
export const CONFIGURATION_NODE_OFFSET = (CONFIGURATION_NODE_SIZE[0] / 2) % GRID_SIZE;
/**
* Utility functions for returning nodes found at the edges of a group
*/
@@ -110,8 +112,10 @@ export const getNodesGroupSize = (nodes: INodeUi[]): [number, number] => {
const rightMostNode = getRightMostNode(nodes);
const bottomMostNode = getBottomMostNode(nodes);
const width = Math.abs(rightMostNode.position[0] - leftMostNode.position[0]) + NODE_SIZE;
const height = Math.abs(bottomMostNode.position[1] - topMostNode.position[1]) + NODE_SIZE;
const width =
Math.abs(rightMostNode.position[0] - leftMostNode.position[0]) + DEFAULT_NODE_SIZE[0];
const height =
Math.abs(bottomMostNode.position[1] - topMostNode.position[1]) + DEFAULT_NODE_SIZE[1];
return [width, height];
};
@@ -155,6 +159,13 @@ const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): num
return inputNumber2;
};
export function snapPositionToGrid(position: XYPosition): XYPosition {
return [
closestNumberDivisibleBy(position[0], GRID_SIZE),
closestNumberDivisibleBy(position[1], GRID_SIZE),
];
}
/**
* Returns the new position for a node based on the given position and the nodes in the workflow
*/
@@ -173,13 +184,8 @@ export const getNewNodePosition = (
normalize?: boolean;
} = {},
): XYPosition => {
const resolvedOffset = [...offset];
resolvedOffset[0] = closestNumberDivisibleBy(resolvedOffset[0], GRID_SIZE);
resolvedOffset[1] = closestNumberDivisibleBy(resolvedOffset[1], GRID_SIZE);
const resolvedPosition: XYPosition = [...initialPosition];
resolvedPosition[0] = closestNumberDivisibleBy(resolvedPosition[0], GRID_SIZE);
resolvedPosition[1] = closestNumberDivisibleBy(resolvedPosition[1], GRID_SIZE);
const resolvedOffset = snapPositionToGrid(offset);
const resolvedPosition: XYPosition = snapPositionToGrid(initialPosition);
if (normalize) {
let conflictFound = false;
@@ -290,7 +296,7 @@ export const getNodesWithNormalizedPosition = <T extends { position: XYPosition
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
nodes.forEach((node) => {
node.position[0] += diffX + NODE_SIZE * 2;
node.position[0] += diffX + DEFAULT_NODE_SIZE[0] * 2;
node.position[1] += diffY;
});
}
@@ -610,18 +616,20 @@ export function calculateNodeSize(
nonMainInputCount: number,
): { width: number; height: number } {
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
const height = 100 + Math.max(0, maxVerticalHandles - 3) * GRID_SIZE * 2;
const height = DEFAULT_NODE_SIZE[1] + Math.max(0, maxVerticalHandles - 2) * GRID_SIZE * 2;
if (isConfigurable) {
return {
width: (Math.max(NODE_MIN_INPUT_ITEMS_COUNT - 1, nonMainInputCount) * 2 + 4) * GRID_SIZE,
height: isConfiguration ? 75 : height,
width:
Math.max(NODE_MIN_INPUT_ITEMS_COUNT, nonMainInputCount) * GRID_SIZE * 4 +
CONFIGURATION_NODE_OFFSET * 2,
height: isConfiguration ? CONFIGURATION_NODE_SIZE[1] : height,
};
}
if (isConfiguration) {
return { width: GRID_SIZE * 4, height: GRID_SIZE * 4 };
return { width: CONFIGURATION_NODE_SIZE[0], height: CONFIGURATION_NODE_SIZE[1] };
}
return { width: 100, height };
return { width: DEFAULT_NODE_SIZE[0], height };
}