mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Update grid size to 16px for better alignment (#16869)
This commit is contained in:
@@ -74,8 +74,8 @@ export function createCanvasGraphNode({
|
|||||||
id = '1',
|
id = '1',
|
||||||
type = 'default',
|
type = 'default',
|
||||||
label = 'Node',
|
label = 'Node',
|
||||||
position = { x: 100, y: 100 },
|
position = { x: 96, y: 96 },
|
||||||
dimensions = { width: 100, height: 100 },
|
dimensions = { width: 96, height: 96 },
|
||||||
data,
|
data,
|
||||||
...rest
|
...rest
|
||||||
}: Partial<
|
}: Partial<
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { reactive, computed, toRefs } from 'vue';
|
|||||||
import type { ActionTypeDescription, SimplifiedNodeType } from '@/Interface';
|
import type { ActionTypeDescription, SimplifiedNodeType } from '@/Interface';
|
||||||
import { WEBHOOK_NODE_TYPE, DRAG_EVENT_DATA_KEY } from '@/constants';
|
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 NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
|
||||||
import { useViewStacks } from '../composables/useViewStacks';
|
import { useViewStacks } from '../composables/useViewStacks';
|
||||||
@@ -76,7 +76,10 @@ function onDragOver(event: DragEvent): void {
|
|||||||
return;
|
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 };
|
state.draggablePosition = { x, y };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ describe('Canvas', () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
position: { x: 120, y: 120 },
|
position: { x: 112, y: 112 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`CanvasBackground > should render the background with the correct gap 1`] = `
|
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">
|
"<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>
|
<circle cx="0.5" cy="0.5" r="0.5" fill="#aaa"></circle>
|
||||||
<!---->
|
<!---->
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ describe('CanvasNode', () => {
|
|||||||
const inputHandles = getAllByTestId('canvas-node-input-handle');
|
const inputHandles = getAllByTestId('canvas-node-input-handle');
|
||||||
|
|
||||||
expect(inputHandles[1]).toHaveStyle('left: 40px');
|
expect(inputHandles[1]).toHaveStyle('left: 40px');
|
||||||
expect(inputHandles[2]).toHaveStyle('left: 160px');
|
expect(inputHandles[2]).toHaveStyle('left: 168px');
|
||||||
expect(inputHandles[3]).toHaveStyle('left: 200px');
|
expect(inputHandles[3]).toHaveStyle('left: 232px');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import type { EventBus } from '@n8n/utils/event-bus';
|
|||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
|
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> & {
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -186,7 +186,7 @@ const createEndpointMappingFn =
|
|||||||
connectingHandle.value?.handleId === handleId;
|
connectingHandle.value?.handleId === handleId;
|
||||||
const offsetValue =
|
const offsetValue =
|
||||||
position === Position.Bottom
|
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)}%`;
|
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -35,13 +35,12 @@ describe('CanvasNodeDefault', () => {
|
|||||||
|
|
||||||
describe('inputs and outputs', () => {
|
describe('inputs and outputs', () => {
|
||||||
it.each([
|
it.each([
|
||||||
[1, 1, '100px'],
|
[1, 1, '96px'],
|
||||||
[3, 1, '100px'],
|
[1, 3, '128px'],
|
||||||
[4, 1, '140px'],
|
[1, 4, '160px'],
|
||||||
[1, 1, '100px'],
|
[3, 1, '128px'],
|
||||||
[1, 3, '100px'],
|
[4, 1, '160px'],
|
||||||
[1, 4, '140px'],
|
[4, 4, '160px'],
|
||||||
[4, 4, '140px'],
|
|
||||||
])(
|
])(
|
||||||
'should adjust height css variable based on the number of inputs and outputs (%i inputs, %i outputs)',
|
'should adjust height css variable based on the number of inputs and outputs (%i inputs, %i outputs)',
|
||||||
(inputCount, outputCount, expected) => {
|
(inputCount, outputCount, expected) => {
|
||||||
@@ -205,7 +204,7 @@ describe('CanvasNodeDefault', () => {
|
|||||||
[
|
[
|
||||||
'1 required',
|
'1 required',
|
||||||
[{ type: NodeConnectionTypes.AiLanguageModel, index: 0, required: true }],
|
[{ type: NodeConnectionTypes.AiLanguageModel, index: 0, required: true }],
|
||||||
'240px',
|
'272px',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'2 required, 1 optional',
|
'2 required, 1 optional',
|
||||||
@@ -214,7 +213,7 @@ describe('CanvasNodeDefault', () => {
|
|||||||
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
|
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
|
||||||
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
|
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
|
||||||
],
|
],
|
||||||
'240px',
|
'272px',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'2 required, 2 optional',
|
'2 required, 2 optional',
|
||||||
@@ -224,7 +223,7 @@ describe('CanvasNodeDefault', () => {
|
|||||||
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
|
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
|
||||||
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
|
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
|
||||||
],
|
],
|
||||||
'240px',
|
'272px',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'1 required, 4 optional',
|
'1 required, 4 optional',
|
||||||
@@ -235,7 +234,7 @@ describe('CanvasNodeDefault', () => {
|
|||||||
{ type: NodeConnectionTypes.AiMemory, index: 0 },
|
{ type: NodeConnectionTypes.AiMemory, index: 0 },
|
||||||
{ 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)',
|
'should adjust width css variable based on the number of non-main inputs (%s)',
|
||||||
|
|||||||
@@ -234,7 +234,8 @@ function onActivate(event: MouseEvent) {
|
|||||||
|
|
||||||
&.configuration {
|
&.configuration {
|
||||||
.icon {
|
.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) {
|
&:not(.running) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
|||||||
<div
|
<div
|
||||||
class="node configurable"
|
class="node configurable"
|
||||||
data-test-id="canvas-configurable-node"
|
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-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
@@ -49,7 +49,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
|||||||
<div
|
<div
|
||||||
class="node configurable configuration"
|
class="node configurable configuration"
|
||||||
data-test-id="canvas-configurable-node"
|
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-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
@@ -139,7 +139,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="node"
|
class="node"
|
||||||
data-test-id="canvas-default-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-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
@@ -184,7 +184,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
|||||||
<div
|
<div
|
||||||
class="node trigger"
|
class="node trigger"
|
||||||
data-test-id="canvas-trigger-node"
|
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-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,26 +3,26 @@
|
|||||||
exports[`useCanvasLayout > should layout a basic workflow 1`] = `
|
exports[`useCanvasLayout > should layout a basic workflow 1`] = `
|
||||||
{
|
{
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
"height": 100,
|
"height": 96,
|
||||||
"width": 540,
|
"width": 544,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "node1",
|
"id": "node1",
|
||||||
"x": 100,
|
"x": 96,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node2",
|
"id": "node2",
|
||||||
"x": 320,
|
"x": 320,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node3",
|
"id": "node3",
|
||||||
"x": 540,
|
"x": 544,
|
||||||
"y": 100,
|
"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`] = `
|
exports[`useCanvasLayout > should layout a basic workflow with selected nodes 1`] = `
|
||||||
{
|
{
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
"height": 100,
|
"height": 96,
|
||||||
"width": 540,
|
"width": 544,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "node1",
|
"id": "node1",
|
||||||
"x": 100,
|
"x": 96,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node2",
|
"id": "node2",
|
||||||
"x": 320,
|
"x": 320,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node3",
|
"id": "node3",
|
||||||
"x": 540,
|
"x": 544,
|
||||||
"y": 100,
|
"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`] = `
|
exports[`useCanvasLayout > should layout a workflow with AI nodes 1`] = `
|
||||||
{
|
{
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
"height": 540,
|
"height": 544,
|
||||||
"width": 820,
|
"width": 832,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 220,
|
"y": 224,
|
||||||
},
|
},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "node1",
|
"id": "node1",
|
||||||
"x": 100,
|
"x": 96,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aiTool1",
|
"id": "aiTool1",
|
||||||
@@ -77,28 +77,28 @@ exports[`useCanvasLayout > should layout a workflow with AI nodes 1`] = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aiTool2",
|
"id": "aiTool2",
|
||||||
"x": 460,
|
"x": 464,
|
||||||
"y": 320,
|
"y": 320,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aiTool3",
|
"id": "aiTool3",
|
||||||
"x": 600,
|
"x": 608,
|
||||||
"y": 540,
|
"y": 544,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aiAgent",
|
"id": "aiAgent",
|
||||||
"x": 460,
|
"x": 464,
|
||||||
"y": 100,
|
"y": 96,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "configurableAiTool",
|
"id": "configurableAiTool",
|
||||||
"x": 600,
|
"x": 608,
|
||||||
"y": 320,
|
"y": 320,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node2",
|
"id": "node2",
|
||||||
"x": 820,
|
"x": 832,
|
||||||
"y": 100,
|
"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`] = `
|
exports[`useCanvasLayout > should layout a workflow with sticky notes 1`] = `
|
||||||
{
|
{
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
"height": 100,
|
"height": 96,
|
||||||
"width": 760,
|
"width": 768,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
@@ -120,22 +120,22 @@ exports[`useCanvasLayout > should layout a workflow with sticky notes 1`] = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node2",
|
"id": "node2",
|
||||||
"x": 220,
|
"x": 224,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node3",
|
"id": "node3",
|
||||||
"x": 440,
|
"x": 448,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node4",
|
"id": "node4",
|
||||||
"x": 660,
|
"x": 672,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sticky",
|
"id": "sticky",
|
||||||
"x": 130,
|
"x": 134,
|
||||||
"y": -240,
|
"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`] = `
|
exports[`useCanvasLayout > should not reorder nodes vertically as it affects execution order 1`] = `
|
||||||
{
|
{
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
"height": 300,
|
"height": 288,
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -154,17 +154,17 @@ exports[`useCanvasLayout > should not reorder nodes vertically as it affects exe
|
|||||||
{
|
{
|
||||||
"id": "node1",
|
"id": "node1",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": -100,
|
"y": -112,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node3",
|
"id": "node3",
|
||||||
"x": 220,
|
"x": 224,
|
||||||
"y": -200,
|
"y": -208,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "node2",
|
"id": "node2",
|
||||||
"x": 220,
|
"x": 224,
|
||||||
"y": 0,
|
"y": -16,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('useCanvasLayout', () => {
|
|||||||
|
|
||||||
const { layout } = createTestSetup(nodes, connections);
|
const { layout } = createTestSetup(nodes, connections);
|
||||||
const result = layout('all');
|
const result = layout('all');
|
||||||
|
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
expect(matchesGrid(result)).toBe(true);
|
expect(matchesGrid(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -153,8 +154,8 @@ describe('useCanvasLayout', () => {
|
|||||||
test('should not reorder nodes vertically as it affects execution order', () => {
|
test('should not reorder nodes vertically as it affects execution order', () => {
|
||||||
const nodes = [
|
const nodes = [
|
||||||
createCanvasGraphNode({ id: 'node1', position: { x: 0, y: 0 } }),
|
createCanvasGraphNode({ id: 'node1', position: { x: 0, y: 0 } }),
|
||||||
createCanvasGraphNode({ id: 'node2', position: { x: 400, y: 200 } }),
|
createCanvasGraphNode({ id: 'node2', position: { x: 400, y: 208 } }),
|
||||||
createCanvasGraphNode({ id: 'node3', position: { x: 400, y: -200 } }),
|
createCanvasGraphNode({ id: 'node3', position: { x: 400, y: -208 } }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const connections: Array<[string, string]> = [
|
const connections: Array<[string, string]> = [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type CanvasNodeData,
|
type CanvasNodeData,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { isPresent } from '../utils/typesUtils';
|
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 CanvasLayoutOptions = { id?: string };
|
||||||
export type CanvasLayoutTarget = 'selection' | 'all';
|
export type CanvasLayoutTarget = 'selection' | 'all';
|
||||||
@@ -40,12 +40,12 @@ export type CanvasLayoutEvent = {
|
|||||||
|
|
||||||
export type CanvasNodeDictionary = Record<string, GraphNode<CanvasNodeData>>;
|
export type CanvasNodeDictionary = Record<string, GraphNode<CanvasNodeData>>;
|
||||||
|
|
||||||
const NODE_X_SPACING = GRID_SIZE * 6;
|
const NODE_X_SPACING = GRID_SIZE * 8;
|
||||||
const NODE_Y_SPACING = GRID_SIZE * 5;
|
const NODE_Y_SPACING = GRID_SIZE * 6;
|
||||||
const SUBGRAPH_SPACING = GRID_SIZE * 8;
|
const SUBGRAPH_SPACING = GRID_SIZE * 8;
|
||||||
const AI_X_SPACING = GRID_SIZE * 2;
|
const AI_X_SPACING = GRID_SIZE * 3;
|
||||||
const AI_Y_SPACING = GRID_SIZE * 6;
|
const AI_Y_SPACING = GRID_SIZE * 8;
|
||||||
const STICKY_BOTTOM_PADDING = GRID_SIZE * 3;
|
const STICKY_BOTTOM_PADDING = GRID_SIZE * 4;
|
||||||
|
|
||||||
export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -113,7 +113,10 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
|||||||
function createDagreSubGraph({
|
function createDagreSubGraph({
|
||||||
nodeIds,
|
nodeIds,
|
||||||
parent,
|
parent,
|
||||||
}: { nodeIds: string[]; parent: dagre.graphlib.Graph }) {
|
}: {
|
||||||
|
nodeIds: string[];
|
||||||
|
parent: dagre.graphlib.Graph;
|
||||||
|
}) {
|
||||||
const subGraph = new dagre.graphlib.Graph();
|
const subGraph = new dagre.graphlib.Graph();
|
||||||
subGraph.setGraph({
|
subGraph.setGraph({
|
||||||
rankdir: 'LR',
|
rankdir: 'LR',
|
||||||
@@ -165,7 +168,10 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
|||||||
function createAiSubGraph({
|
function createAiSubGraph({
|
||||||
parent,
|
parent,
|
||||||
nodeIds,
|
nodeIds,
|
||||||
}: { parent: dagre.graphlib.Graph; nodeIds: string[] }) {
|
}: {
|
||||||
|
parent: dagre.graphlib.Graph;
|
||||||
|
nodeIds: string[];
|
||||||
|
}) {
|
||||||
const subGraph = new dagre.graphlib.Graph();
|
const subGraph = new dagre.graphlib.Graph();
|
||||||
subGraph.setGraph({
|
subGraph.setGraph({
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
||||||
@@ -449,7 +455,7 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
|||||||
const aiGraphBoundingBox = compositeBoundingBox(
|
const aiGraphBoundingBox = compositeBoundingBox(
|
||||||
aiNodes.map((nodeId) => boundingBoxByNodeId[nodeId]).filter(isPresent),
|
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;
|
aiGraphBoundingBox.y += aiNodeVerticalCorrection;
|
||||||
|
|
||||||
const hasConflictingNodes = Object.entries(boundingBoxByNodeId)
|
const hasConflictingNodes = Object.entries(boundingBoxByNodeId)
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ describe('useCanvasOperations', () => {
|
|||||||
{
|
{
|
||||||
type: 'type',
|
type: 'type',
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [20, 20],
|
position: [32, 32],
|
||||||
},
|
},
|
||||||
mockNodeTypeDescription({ name: 'type' }),
|
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', () => {
|
it('should not assign credentials when multiple credentials are available', () => {
|
||||||
@@ -274,13 +274,13 @@ describe('useCanvasOperations', () => {
|
|||||||
|
|
||||||
describe('resolveNodePosition', () => {
|
describe('resolveNodePosition', () => {
|
||||||
it('should return the node position if it is already set', () => {
|
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 nodeTypeDescription = mockNodeTypeDescription();
|
||||||
|
|
||||||
const { resolveNodePosition } = useCanvasOperations();
|
const { resolveNodePosition } = useCanvasOperations();
|
||||||
const position = resolveNodePosition(node, nodeTypeDescription);
|
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', () => {
|
it('should place the node at the last cancelled connection position', () => {
|
||||||
@@ -302,7 +302,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const { resolveNodePosition } = useCanvasOperations();
|
const { resolveNodePosition } = useCanvasOperations();
|
||||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||||
|
|
||||||
expect(position).toEqual([200, 160]);
|
expect(position).toEqual([208, 160]);
|
||||||
expect(uiStore.lastCancelledConnectionPosition).toBeUndefined();
|
expect(uiStore.lastCancelledConnectionPosition).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
|
||||||
uiStore.lastInteractedWithNode = createTestNode({
|
uiStore.lastInteractedWithNode = createTestNode({
|
||||||
position: [100, 100],
|
position: [112, 112],
|
||||||
type: 'test',
|
type: 'test',
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
});
|
});
|
||||||
@@ -327,7 +327,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const { resolveNodePosition } = useCanvasOperations();
|
const { resolveNodePosition } = useCanvasOperations();
|
||||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
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', () => {
|
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);
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
|
||||||
uiStore.lastInteractedWithNode = createTestNode({
|
uiStore.lastInteractedWithNode = createTestNode({
|
||||||
position: [100, 100],
|
position: [96, 96],
|
||||||
type: 'test',
|
type: 'test',
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
});
|
});
|
||||||
@@ -358,7 +358,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const { resolveNodePosition } = useCanvasOperations();
|
const { resolveNodePosition } = useCanvasOperations();
|
||||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
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', () => {
|
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 node = createTestNode({ id: '0' });
|
||||||
const nodeTypeDescription = mockNodeTypeDescription();
|
const nodeTypeDescription = mockNodeTypeDescription();
|
||||||
|
|
||||||
workflowsStore.workflowTriggerNodes = [
|
workflowsStore.workflowTriggerNodes = [createTestNode({ id: 'trigger', position: [96, 96] })];
|
||||||
createTestNode({ id: 'trigger', position: [100, 100] }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
|
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
|
||||||
lastClickPosition.value = [300, 300];
|
lastClickPosition.value = [300, 300];
|
||||||
|
|
||||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
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', () => {
|
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', () => {
|
it('records history for multiple node position updates when tracking is enabled', () => {
|
||||||
const historyStore = useHistoryStore();
|
const historyStore = useHistoryStore();
|
||||||
const events = [
|
const events = [
|
||||||
{ id: 'node1', position: { x: 100, y: 100 } },
|
{ id: 'node1', position: { x: 96, y: 96 } },
|
||||||
{ id: 'node2', position: { x: 200, y: 200 } },
|
{ id: 'node2', position: { x: 208, y: 208 } },
|
||||||
];
|
];
|
||||||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||||||
@@ -548,8 +546,8 @@ describe('useCanvasOperations', () => {
|
|||||||
it('updates positions for multiple nodes', () => {
|
it('updates positions for multiple nodes', () => {
|
||||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
const events = [
|
const events = [
|
||||||
{ id: 'node1', position: { x: 100, y: 100 } },
|
{ id: 'node1', position: { x: 96, y: 96 } },
|
||||||
{ id: 'node2', position: { x: 200, y: 200 } },
|
{ id: 'node2', position: { x: 208, y: 208 } },
|
||||||
];
|
];
|
||||||
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||||||
workflowsStore.getNodeById
|
workflowsStore.getNodeById
|
||||||
@@ -570,13 +568,13 @@ describe('useCanvasOperations', () => {
|
|||||||
updateNodesPosition(events);
|
updateNodesPosition(events);
|
||||||
|
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not record history when trackHistory is false', () => {
|
it('does not record history when trackHistory is false', () => {
|
||||||
const historyStore = useHistoryStore();
|
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 startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||||||
|
|
||||||
@@ -596,10 +594,10 @@ describe('useCanvasOperations', () => {
|
|||||||
target: 'all',
|
target: 'all',
|
||||||
result: {
|
result: {
|
||||||
nodes: [
|
nodes: [
|
||||||
{ id: 'node1', x: 100, y: 100 },
|
{ id: 'node1', x: 96, y: 96 },
|
||||||
{ id: 'node2', x: 200, y: 200 },
|
{ 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');
|
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||||
@@ -619,10 +617,10 @@ describe('useCanvasOperations', () => {
|
|||||||
target: 'all',
|
target: 'all',
|
||||||
result: {
|
result: {
|
||||||
nodes: [
|
nodes: [
|
||||||
{ id: 'node1', x: 100, y: 100 },
|
{ id: 'node1', x: 96, y: 96 },
|
||||||
{ id: 'node2', x: 200, y: 200 },
|
{ 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');
|
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||||||
@@ -644,8 +642,8 @@ describe('useCanvasOperations', () => {
|
|||||||
tidyUp(event);
|
tidyUp(event);
|
||||||
|
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
|
||||||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
|
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a "User tidied up workflow" telemetry event', () => {
|
it('should send a "User tidied up workflow" telemetry event', () => {
|
||||||
@@ -654,10 +652,10 @@ describe('useCanvasOperations', () => {
|
|||||||
target: 'all',
|
target: 'all',
|
||||||
result: {
|
result: {
|
||||||
nodes: [
|
nodes: [
|
||||||
{ id: 'node1', x: 100, y: 100 },
|
{ id: 'node1', x: 96, y: 96 },
|
||||||
{ id: 'node2', x: 200, y: 200 },
|
{ 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 nodeTypesStore = useNodeTypesStore();
|
||||||
const nodeTypeName = 'type';
|
const nodeTypeName = 'type';
|
||||||
const nodes = [
|
const nodes = [
|
||||||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
|
mockNode({ name: 'Node 1', type: nodeTypeName, position: [32, 32] }),
|
||||||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
|
mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 256] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||||||
@@ -758,14 +756,14 @@ describe('useCanvasOperations', () => {
|
|||||||
name: nodes[0].name,
|
name: nodes[0].name,
|
||||||
type: nodeTypeName,
|
type: nodeTypeName,
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [40, 40],
|
position: [32, 32],
|
||||||
parameters: {},
|
parameters: {},
|
||||||
});
|
});
|
||||||
expect(workflowsStore.addNode.mock.calls[1][0]).toMatchObject({
|
expect(workflowsStore.addNode.mock.calls[1][0]).toMatchObject({
|
||||||
name: nodes[1].name,
|
name: nodes[1].name,
|
||||||
type: nodeTypeName,
|
type: nodeTypeName,
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [100, 240],
|
position: [96, 256],
|
||||||
parameters: {},
|
parameters: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -775,8 +773,8 @@ describe('useCanvasOperations', () => {
|
|||||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
const nodeTypeName = 'type';
|
const nodeTypeName = 'type';
|
||||||
const nodes = [
|
const nodes = [
|
||||||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [120, 120] }),
|
mockNode({ name: 'Node 1', type: nodeTypeName, position: [128, 128] }),
|
||||||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [180, 320] }),
|
mockNode({ name: 'Node 2', type: nodeTypeName, position: [192, 320] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||||||
@@ -804,9 +802,9 @@ describe('useCanvasOperations', () => {
|
|||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const nodeTypeName = 'type';
|
const nodeTypeName = 'type';
|
||||||
const nodes = [
|
const nodes = [
|
||||||
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
|
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [32, 32] }),
|
||||||
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
|
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [32, 32] }),
|
||||||
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [100, 240] }),
|
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [96, 256] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||||||
@@ -842,7 +840,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const nodeTypeName = 'type';
|
const nodeTypeName = 'type';
|
||||||
const nodes = [
|
const nodes = [
|
||||||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
|
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(
|
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||||||
@@ -3007,8 +3005,8 @@ describe('useCanvasOperations', () => {
|
|||||||
|
|
||||||
// Create three nodes in a sequence: A -> B -> C
|
// Create three nodes in a sequence: A -> B -> C
|
||||||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
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] });
|
||||||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
|
||||||
|
|
||||||
const nodeTypeDescription = mockNodeTypeDescription({
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
name: nodeA.type,
|
name: nodeA.type,
|
||||||
@@ -3077,8 +3075,8 @@ describe('useCanvasOperations', () => {
|
|||||||
|
|
||||||
// Create three nodes in a sequence: A -> B -> C
|
// Create three nodes in a sequence: A -> B -> C
|
||||||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
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] });
|
||||||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
|
||||||
|
|
||||||
const nodeTypeDescription = mockNodeTypeDescription({
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
name: nodeA.type,
|
name: nodeA.type,
|
||||||
@@ -3144,8 +3142,8 @@ describe('useCanvasOperations', () => {
|
|||||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
|
||||||
// Create nodes: B -> C (no incoming to B)
|
// Create nodes: B -> C (no incoming to B)
|
||||||
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
|
||||||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
|
||||||
|
|
||||||
workflowsStore.workflow.nodes = [nodeB, nodeC];
|
workflowsStore.workflow.nodes = [nodeB, nodeC];
|
||||||
workflowsStore.workflow.connections = {
|
workflowsStore.workflow.connections = {
|
||||||
@@ -3173,7 +3171,7 @@ describe('useCanvasOperations', () => {
|
|||||||
|
|
||||||
// Create nodes: A -> B (no outgoing from B)
|
// Create nodes: A -> B (no outgoing from B)
|
||||||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
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.nodes = [nodeA, nodeB];
|
||||||
workflowsStore.workflow.connections = {
|
workflowsStore.workflow.connections = {
|
||||||
@@ -3220,7 +3218,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const nodeB: IWorkflowTemplateNode = createTestNode({
|
const nodeB: IWorkflowTemplateNode = createTestNode({
|
||||||
id: 'Y',
|
id: 'Y',
|
||||||
name: 'Node Y',
|
name: 'Node Y',
|
||||||
position: [180, 80],
|
position: [192, 80],
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow: IWorkflowTemplate['workflow'] = {
|
const workflow: IWorkflowTemplate['workflow'] = {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
} from '@/utils/canvasUtils';
|
} from '@/utils/canvasUtils';
|
||||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
import {
|
import {
|
||||||
|
GRID_SIZE,
|
||||||
CONFIGURABLE_NODE_SIZE,
|
CONFIGURABLE_NODE_SIZE,
|
||||||
CONFIGURATION_NODE_SIZE,
|
CONFIGURATION_NODE_SIZE,
|
||||||
DEFAULT_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
|
// When we're adding multiple nodes, increment the X position for the next one
|
||||||
insertPosition = [
|
insertPosition = [
|
||||||
lastAddedNode.position[0] + NodeViewUtils.NODE_SIZE * 2 + NodeViewUtils.GRID_SIZE,
|
lastAddedNode.position[0] + DEFAULT_NODE_SIZE[0] * 2 + GRID_SIZE,
|
||||||
lastAddedNode.position[1],
|
lastAddedNode.position[1],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1108,8 +1109,8 @@ export function useCanvasOperations() {
|
|||||||
if (lastInteractedWithNodeMainOutputs.length > 1) {
|
if (lastInteractedWithNodeMainOutputs.length > 1) {
|
||||||
const yOffsetValues = generateOffsets(
|
const yOffsetValues = generateOffsets(
|
||||||
lastInteractedWithNodeMainOutputs.length,
|
lastInteractedWithNodeMainOutputs.length,
|
||||||
NodeViewUtils.NODE_SIZE,
|
DEFAULT_NODE_SIZE[1],
|
||||||
NodeViewUtils.GRID_SIZE,
|
GRID_SIZE,
|
||||||
);
|
);
|
||||||
|
|
||||||
yOffset = yOffsetValues[connectionIndex];
|
yOffset = yOffsetValues[connectionIndex];
|
||||||
|
|||||||
@@ -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_PRICING_PAGE_URL = 'https://n8n.io/pricing';
|
||||||
export const N8N_MAIN_GITHUB_REPO_URL = 'https://github.com/n8n-io/n8n';
|
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
|
// node types
|
||||||
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';
|
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ import { updateCurrentUserSettings } from '@/api/users';
|
|||||||
import { useExecutingNode } from '@/composables/useExecutingNode';
|
import { useExecutingNode } from '@/composables/useExecutingNode';
|
||||||
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
||||||
import { isChatNode } from '@/utils/aiUtils';
|
import { isChatNode } from '@/utils/aiUtils';
|
||||||
|
import { snapPositionToGrid } from '@/utils/nodeViewUtils';
|
||||||
|
|
||||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -1290,6 +1291,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
node.type = getCredentialOnlyNodeTypeName(node.extendsCredential);
|
node.type = getCredentialOnlyNodeTypeName(node.extendsCredential);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.position) {
|
||||||
|
node.position = snapPositionToGrid(node.position);
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeMetadata.value[node.name]) {
|
if (!nodeMetadata.value[node.name]) {
|
||||||
nodeMetadata.value[node.name] = { pristine: true };
|
nodeMetadata.value[node.name] = { pristine: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -992,7 +992,7 @@ describe('insertSpacersBetweenEndpoints', () => {
|
|||||||
const endpoints = [{ index: 0, required: true }];
|
const endpoints = [{ index: 0, required: true }];
|
||||||
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
|
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', () => {
|
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 endpoints = [{ index: 0, required: false }];
|
||||||
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
|
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', () => {
|
it('should handle no endpoints', () => {
|
||||||
const endpoints: Array<{ index: number; required: boolean }> = [];
|
const endpoints: Array<{ index: number; required: boolean }> = [];
|
||||||
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
|
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', () => {
|
it('should handle required endpoints greater than NODE_MIN_INPUT_ITEMS_COUNT', () => {
|
||||||
@@ -1040,7 +1040,6 @@ describe('insertSpacersBetweenEndpoints', () => {
|
|||||||
{ index: 0, required: true },
|
{ index: 0, required: true },
|
||||||
{ index: 1, required: true },
|
{ index: 1, required: true },
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
{ index: 2 },
|
{ index: 2 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -1049,6 +1048,6 @@ describe('insertSpacersBetweenEndpoints', () => {
|
|||||||
const endpoints = [{ index: 0, required: true }];
|
const endpoints = [{ index: 0, required: true }];
|
||||||
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
|
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import {
|
|||||||
generateOffsets,
|
generateOffsets,
|
||||||
getGenericHints,
|
getGenericHints,
|
||||||
getNewNodePosition,
|
getNewNodePosition,
|
||||||
NODE_SIZE,
|
|
||||||
updateViewportToContainNodes,
|
updateViewportToContainNodes,
|
||||||
|
DEFAULT_NODE_SIZE,
|
||||||
|
snapPositionToGrid,
|
||||||
|
calculateNodeSize,
|
||||||
|
GRID_SIZE,
|
||||||
} from './nodeViewUtils';
|
} from './nodeViewUtils';
|
||||||
import type { INode, INodeTypeDescription, INodeExecutionData, Workflow } from 'n8n-workflow';
|
import type { INode, INodeTypeDescription, INodeExecutionData, Workflow } from 'n8n-workflow';
|
||||||
import type { INodeUi, XYPosition } from '@/Interface';
|
import type { INodeUi, XYPosition } from '@/Interface';
|
||||||
@@ -203,96 +206,112 @@ describe('getGenericHints', () => {
|
|||||||
|
|
||||||
describe('generateOffsets', () => {
|
describe('generateOffsets', () => {
|
||||||
it('should return correct offsets for 0 nodes', () => {
|
it('should return correct offsets for 0 nodes', () => {
|
||||||
const result = generateOffsets(0, 100, 20);
|
const result = generateOffsets(0, 96, GRID_SIZE);
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct offsets for 1 node', () => {
|
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]);
|
expect(result).toEqual([0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct offsets for 2 nodes', () => {
|
it('should return correct offsets for 2 nodes', () => {
|
||||||
const result = generateOffsets(2, 100, 20);
|
const result = generateOffsets(2, 96, GRID_SIZE);
|
||||||
expect(result).toEqual([-100, 100]);
|
expect(result).toEqual([-96, 96]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct offsets for 3 nodes', () => {
|
it('should return correct offsets for 3 nodes', () => {
|
||||||
const result = generateOffsets(3, 100, 20);
|
const result = generateOffsets(3, 96, GRID_SIZE);
|
||||||
expect(result).toEqual([-120, 0, 120]);
|
expect(result).toEqual([-112, 0, 112]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct offsets for 4 nodes', () => {
|
it('should return correct offsets for 4 nodes', () => {
|
||||||
const result = generateOffsets(4, 100, 20);
|
const result = generateOffsets(4, 96, GRID_SIZE);
|
||||||
expect(result).toEqual([-220, -100, 100, 220]);
|
expect(result).toEqual([-208, -96, 96, 208]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct offsets for large node count', () => {
|
it('should return correct offsets for large node count', () => {
|
||||||
const result = generateOffsets(10, 100, 20);
|
const result = generateOffsets(10, 96, GRID_SIZE);
|
||||||
expect(result).toEqual([-580, -460, -340, -220, -100, 100, 220, 340, 460, 580]);
|
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', () => {
|
describe('getNewNodePosition', () => {
|
||||||
it('should return the new position when there are no conflicts', () => {
|
it('should return the new position when there are no conflicts', () => {
|
||||||
const nodes: INodeUi[] = [];
|
const nodes: INodeUi[] = [];
|
||||||
const newPosition: XYPosition = [100, 100];
|
const newPosition: XYPosition = [96, 96];
|
||||||
const result = getNewNodePosition(nodes, newPosition);
|
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', () => {
|
it('should adjust the position to the closest grid size', () => {
|
||||||
const nodes: INodeUi[] = [];
|
const nodes: INodeUi[] = [];
|
||||||
const newPosition: XYPosition = [105, 115];
|
const newPosition: XYPosition = [105, 115];
|
||||||
const result = getNewNodePosition(nodes, newPosition);
|
const result = getNewNodePosition(nodes, newPosition);
|
||||||
expect(result).toEqual([120, 120]);
|
expect(result).toEqual([112, 128]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move the position to avoid conflicts', () => {
|
it('should move the position to avoid conflicts', () => {
|
||||||
const nodes: INodeUi[] = [
|
const nodes: INodeUi[] = [createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE })];
|
||||||
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
|
const newPosition: XYPosition = [96, 96];
|
||||||
];
|
|
||||||
const newPosition: XYPosition = [100, 100];
|
|
||||||
const result = getNewNodePosition(nodes, newPosition);
|
const result = getNewNodePosition(nodes, newPosition);
|
||||||
expect(result).toEqual([220, 220]);
|
expect(result).toEqual([240, 240]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip nodes in the conflict allowlist', () => {
|
it('should skip nodes in the conflict allowlist', () => {
|
||||||
const nodes: INodeUi[] = [
|
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);
|
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', () => {
|
it('should use the provided move position to resolve conflicts', () => {
|
||||||
const nodes: INodeUi[] = [
|
const nodes: INodeUi[] = [createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE })];
|
||||||
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
|
const newPosition: XYPosition = [96, 96];
|
||||||
];
|
const movePosition: XYPosition = [48, 48];
|
||||||
const newPosition: XYPosition = [100, 100];
|
|
||||||
const movePosition: XYPosition = [50, 50];
|
|
||||||
const result = getNewNodePosition(nodes, newPosition, {
|
const result = getNewNodePosition(nodes, newPosition, {
|
||||||
offset: movePosition,
|
offset: movePosition,
|
||||||
});
|
});
|
||||||
expect(result).toEqual([220, 220]);
|
expect(result).toEqual([240, 240]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple conflicts correctly', () => {
|
it('should handle multiple conflicts correctly', () => {
|
||||||
const nodes: INodeUi[] = [
|
const nodes: INodeUi[] = [
|
||||||
createTestNode({ id: '1', position: [100, 100], type: SET_NODE_TYPE }),
|
createTestNode({ id: '1', position: [96, 96], type: SET_NODE_TYPE }),
|
||||||
createTestNode({ id: '2', position: [140, 140], 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);
|
const result = getNewNodePosition(nodes, newPosition);
|
||||||
expect(result).toEqual([280, 280]);
|
expect(result).toEqual([288, 288]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const testNodes: INode[] = [
|
const testNodes: INode[] = [
|
||||||
createTestNode({ id: 'a', position: [0, 0] }),
|
createTestNode({ id: 'a', position: [0, 0] }),
|
||||||
createTestNode({ id: 'b', position: [100, 50] }),
|
createTestNode({ id: 'b', position: [96, 50] }),
|
||||||
createTestNode({ id: 'c', position: [50, 100] }),
|
createTestNode({ id: 'c', position: [50, 96] }),
|
||||||
createTestNode({ id: 'd', position: [-20, -10] }),
|
createTestNode({ id: 'd', position: [-20, -10] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -379,15 +398,15 @@ describe('getBottomMostNode', () => {
|
|||||||
describe('getNodesGroupSize', () => {
|
describe('getNodesGroupSize', () => {
|
||||||
it('calculates the group size correctly', () => {
|
it('calculates the group size correctly', () => {
|
||||||
const [width, height] = getNodesGroupSize(testNodes);
|
const [width, height] = getNodesGroupSize(testNodes);
|
||||||
expect(width).toBe(Math.abs(100 - -20) + NODE_SIZE);
|
expect(width).toBe(Math.abs(96 - -20) + DEFAULT_NODE_SIZE[0]);
|
||||||
expect(height).toBe(Math.abs(-10 - 100) + NODE_SIZE);
|
expect(height).toBe(Math.abs(-10 - 96) + DEFAULT_NODE_SIZE[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a single node', () => {
|
it('should handle a single node', () => {
|
||||||
const single = [testNodes[0]];
|
const single = [testNodes[0]];
|
||||||
const [w, h] = getNodesGroupSize(single);
|
const [w, h] = getNodesGroupSize(single);
|
||||||
expect(w).toBe(NODE_SIZE);
|
expect(w).toBe(DEFAULT_NODE_SIZE[0]);
|
||||||
expect(h).toBe(NODE_SIZE);
|
expect(h).toBe(DEFAULT_NODE_SIZE[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle nodes with equal positions', () => {
|
it('should handle nodes with equal positions', () => {
|
||||||
@@ -396,12 +415,12 @@ describe('getNodesGroupSize', () => {
|
|||||||
createTestNode({ id: 'y', position: [10, 20] }),
|
createTestNode({ id: 'y', position: [10, 20] }),
|
||||||
];
|
];
|
||||||
const [we, he] = getNodesGroupSize(equalNodes);
|
const [we, he] = getNodesGroupSize(equalNodes);
|
||||||
expect(we).toBe(NODE_SIZE);
|
expect(we).toBe(DEFAULT_NODE_SIZE[0]);
|
||||||
expect(he).toBe(NODE_SIZE);
|
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', () => {
|
it('should return the same viewport if given node is already in the viewport', () => {
|
||||||
const result = updateViewportToContainNodes(
|
const result = updateViewportToContainNodes(
|
||||||
{ x: 0, y: 0, zoom: 2 },
|
{ 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 {
|
function createTestGraphNode(data: Partial<GraphNode> = {}): GraphNode {
|
||||||
return {
|
return {
|
||||||
computedPosition: { z: 0, ...(data.position ?? { x: 0, y: 0 }) },
|
computedPosition: { z: 0, ...(data.position ?? { x: 0, y: 0 }) },
|
||||||
|
|||||||
@@ -32,17 +32,15 @@ import {
|
|||||||
* Canvas constants and functions
|
* 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 * 6, GRID_SIZE * 6];
|
||||||
export const DEFAULT_NODE_SIZE: [number, number] = [GRID_SIZE * 5, GRID_SIZE * 5];
|
export const CONFIGURATION_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 * 16, GRID_SIZE * 6];
|
||||||
export const CONFIGURABLE_NODE_SIZE: [number, number] = [GRID_SIZE * 12, GRID_SIZE * 5];
|
export const DEFAULT_START_POSITION_X = GRID_SIZE * 11;
|
||||||
export const DEFAULT_START_POSITION_X = GRID_SIZE * 9;
|
export const DEFAULT_START_POSITION_Y = GRID_SIZE * 15;
|
||||||
export const DEFAULT_START_POSITION_Y = GRID_SIZE * 12;
|
|
||||||
export const HEADER_HEIGHT = 65;
|
export const HEADER_HEIGHT = 65;
|
||||||
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = GRID_SIZE * 15;
|
export const PUSH_NODES_OFFSET = DEFAULT_NODE_SIZE[0] * 2 + GRID_SIZE;
|
||||||
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
|
|
||||||
export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
|
export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
|
||||||
xMin: -Infinity,
|
xMin: -Infinity,
|
||||||
yMin: -Infinity,
|
yMin: -Infinity,
|
||||||
@@ -50,6 +48,10 @@ export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
|
|||||||
yMax: Infinity,
|
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
|
* 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 rightMostNode = getRightMostNode(nodes);
|
||||||
const bottomMostNode = getBottomMostNode(nodes);
|
const bottomMostNode = getBottomMostNode(nodes);
|
||||||
|
|
||||||
const width = Math.abs(rightMostNode.position[0] - leftMostNode.position[0]) + NODE_SIZE;
|
const width =
|
||||||
const height = Math.abs(bottomMostNode.position[1] - topMostNode.position[1]) + NODE_SIZE;
|
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];
|
return [width, height];
|
||||||
};
|
};
|
||||||
@@ -155,6 +159,13 @@ const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): num
|
|||||||
return inputNumber2;
|
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
|
* 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;
|
normalize?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): XYPosition => {
|
): XYPosition => {
|
||||||
const resolvedOffset = [...offset];
|
const resolvedOffset = snapPositionToGrid(offset);
|
||||||
resolvedOffset[0] = closestNumberDivisibleBy(resolvedOffset[0], GRID_SIZE);
|
const resolvedPosition: XYPosition = snapPositionToGrid(initialPosition);
|
||||||
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);
|
|
||||||
|
|
||||||
if (normalize) {
|
if (normalize) {
|
||||||
let conflictFound = false;
|
let conflictFound = false;
|
||||||
@@ -290,7 +296,7 @@ export const getNodesWithNormalizedPosition = <T extends { position: XYPosition
|
|||||||
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
|
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
node.position[0] += diffX + NODE_SIZE * 2;
|
node.position[0] += diffX + DEFAULT_NODE_SIZE[0] * 2;
|
||||||
node.position[1] += diffY;
|
node.position[1] += diffY;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -610,18 +616,20 @@ export function calculateNodeSize(
|
|||||||
nonMainInputCount: number,
|
nonMainInputCount: number,
|
||||||
): { width: number; height: number } {
|
): { width: number; height: number } {
|
||||||
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
|
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) {
|
if (isConfigurable) {
|
||||||
return {
|
return {
|
||||||
width: (Math.max(NODE_MIN_INPUT_ITEMS_COUNT - 1, nonMainInputCount) * 2 + 4) * GRID_SIZE,
|
width:
|
||||||
height: isConfiguration ? 75 : height,
|
Math.max(NODE_MIN_INPUT_ITEMS_COUNT, nonMainInputCount) * GRID_SIZE * 4 +
|
||||||
|
CONFIGURATION_NODE_OFFSET * 2,
|
||||||
|
height: isConfiguration ? CONFIGURATION_NODE_SIZE[1] : height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isConfiguration) {
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user