fix(editor): Make adjustments to status icon and connector port position in canvas (#16469)

This commit is contained in:
Suguru Inoue
2025-06-20 11:52:53 +02:00
committed by GitHub
parent 67852b826f
commit 3ea51c11cb
9 changed files with 234 additions and 235 deletions

View File

@@ -94,6 +94,7 @@ describe('CanvasNode', () => {
inputs: [
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.AiAgent, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0 },
{ type: NodeConnectionTypes.AiTool, index: 0 },
],
outputs: [],
@@ -109,8 +110,9 @@ describe('CanvasNode', () => {
const inputHandles = getAllByTestId('canvas-node-input-handle');
expect(inputHandles[1]).toHaveStyle('left: 20%');
expect(inputHandles[2]).toHaveStyle('left: 80%');
expect(inputHandles[1]).toHaveStyle('left: 40px');
expect(inputHandles[2]).toHaveStyle('left: 160px');
expect(inputHandles[3]).toHaveStyle('left: 200px');
});
});

View File

@@ -34,6 +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';
type Props = NodeProps<CanvasNodeData> & {
readOnly?: boolean;
@@ -183,6 +184,10 @@ const createEndpointMappingFn =
connectingHandle.value?.nodeId === props.id &&
connectingHandle.value?.handleType === handleType &&
connectingHandle.value?.handleId === handleId;
const offsetValue =
position === Position.Bottom
? `${GRID_SIZE * (2 + index * 2)}px`
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
return {
...endpoint,
@@ -191,7 +196,7 @@ const createEndpointMappingFn =
isConnecting,
position,
offset: {
[offsetAxis]: `${(100 / (endpoints.length + 1)) * (index + 1)}%`,
[offsetAxis]: offsetValue,
},
};
};

View File

@@ -33,84 +33,41 @@ describe('CanvasNodeDefault', () => {
expect(getByTestId('canvas-default-node')).toMatchSnapshot();
});
describe('inputs', () => {
it('should adjust height css variable based on the number of inputs (1 input)', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [{ type: NodeConnectionTypes.Main, index: 0 }],
},
}),
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'],
])(
'should adjust height css variable based on the number of inputs and outputs (%i inputs, %i outputs)',
(inputCount, outputCount, expected) => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: Array.from({ length: inputCount }).map(() => ({
type: NodeConnectionTypes.Main,
index: 0,
})),
outputs: Array.from({ length: outputCount }).map(() => ({
type: NodeConnectionTypes.Main,
index: 0,
})),
},
}),
},
},
},
});
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--main-input-count': '1' }); // height calculation based on the number of inputs
});
it('should adjust height css variable based on the number of inputs (multiple inputs)', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.Main, index: 0 },
],
},
}),
},
},
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--main-input-count': '3' }); // height calculation based on the number of inputs
});
});
describe('outputs', () => {
it('should adjust height css variable based on the number of outputs (1 output)', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
outputs: [{ type: NodeConnectionTypes.Main, index: 0 }],
},
}),
},
},
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '1' }); // height calculation based on the number of outputs
});
it('should adjust height css variable based on the number of outputs (multiple outputs)', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
outputs: [
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.Main, index: 0 },
],
},
}),
},
},
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '3' }); // height calculation based on the number of outputs
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--height': expected });
},
);
});
describe('selected', () => {
@@ -244,33 +201,67 @@ describe('CanvasNodeDefault', () => {
});
describe('inputs', () => {
it('should adjust width css variable based on the number of non-main inputs', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [
{ type: NodeConnectionTypes.Main, index: 0 },
{ type: NodeConnectionTypes.AiTool, index: 0 },
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
],
render: {
type: CanvasNodeRenderType.Default,
options: {
configurable: true,
it.each([
[
'1 required',
[{ type: NodeConnectionTypes.AiLanguageModel, index: 0, required: true }],
'240px',
],
[
'2 required, 1 optional',
[
{ type: NodeConnectionTypes.AiTool, index: 0 },
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
],
'240px',
],
[
'2 required, 2 optional',
[
{ type: NodeConnectionTypes.AiTool, index: 0 },
{ type: NodeConnectionTypes.AiLanguageModel, index: 0 },
{ type: NodeConnectionTypes.AiDocument, index: 0, required: true },
{ type: NodeConnectionTypes.AiMemory, index: 0, required: true },
],
'240px',
],
[
'1 required, 4 optional',
[
{ type: NodeConnectionTypes.AiLanguageModel, index: 0, required: true },
{ type: NodeConnectionTypes.AiTool, index: 0 },
{ type: NodeConnectionTypes.AiDocument, index: 0 },
{ type: NodeConnectionTypes.AiMemory, index: 0 },
{ type: NodeConnectionTypes.AiMemory, index: 0 },
],
'280px',
],
])(
'should adjust width css variable based on the number of non-main inputs (%s)',
(_, nonMainInputs, expected) => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [{ type: NodeConnectionTypes.Main, index: 0 }, ...nonMainInputs],
render: {
type: CanvasNodeRenderType.Default,
options: {
configurable: true,
},
},
},
},
}),
}),
},
},
},
});
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--configurable-node--input-count': '3' });
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--canvas-node--width': expected });
},
);
});
});

View File

@@ -3,10 +3,10 @@ import { computed, ref, useCssModule, watch } from 'vue';
import { useNodeConnections } from '@/composables/useNodeConnections';
import { useI18n } from '@n8n/i18n';
import { useCanvasNode } from '@/composables/useCanvasNode';
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
import type { CanvasNodeDefaultRender } from '@/types';
import { useCanvas } from '@/composables/useCanvas';
import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas';
import { calculateNodeSize } from '@/utils/nodeViewUtils';
import ExperimentalCanvasNodeSettings from '../../../components/ExperimentalCanvasNodeSettings.vue';
const $style = useCssModule();
@@ -36,18 +36,12 @@ const {
hasIssues,
render,
} = useCanvasNode();
const {
mainOutputs,
mainOutputConnections,
mainInputs,
mainInputConnections,
nonMainInputs,
requiredNonMainInputs,
} = useNodeConnections({
inputs,
outputs,
connections,
});
const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, nonMainInputs } =
useNodeConnections({
inputs,
outputs,
connections,
});
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
@@ -71,29 +65,24 @@ const classes = computed(() => {
};
});
const styles = computed(() => {
const stylesObject: Record<string, string | number> = {};
const iconSize = computed(() => (renderOptions.value.configuration ? 30 : 40));
if (renderOptions.value.configurable) {
let spacerCount = 0;
if (NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS && requiredNonMainInputs.value.length > 0) {
const requiredNonMainInputsCount = requiredNonMainInputs.value.length;
const optionalNonMainInputsCount = nonMainInputs.value.length - requiredNonMainInputsCount;
spacerCount = requiredNonMainInputsCount > 0 && optionalNonMainInputsCount > 0 ? 1 : 0;
}
const nodeSize = computed(() =>
calculateNodeSize(
renderOptions.value.configuration ?? false,
renderOptions.value.configurable ?? false,
mainInputs.value.length,
mainOutputs.value.length,
nonMainInputs.value.length,
),
);
stylesObject['--configurable-node--input-count'] = nonMainInputs.value.length + spacerCount;
}
if (nodeSettingsZoom.value !== undefined) {
stylesObject['--zoom'] = nodeSettingsZoom.value;
}
stylesObject['--canvas-node--main-input-count'] = mainInputs.value.length;
stylesObject['--canvas-node--main-output-count'] = mainOutputs.value.length;
return stylesObject;
});
const styles = computed(() => ({
'--canvas-node--width': `${nodeSize.value.width}px`,
'--canvas-node--height': `${nodeSize.value.height}px`,
'--node-icon-size': `${iconSize.value}px`,
...(nodeSettingsZoom.value === undefined ? {} : { '--zoom': nodeSettingsZoom.value }),
}));
const dataTestId = computed(() => {
let type = 'default';
@@ -117,8 +106,6 @@ const isStrikethroughVisible = computed(() => {
return isDisabled.value && isSingleMainInputNode && isSingleMainOutputNode;
});
const iconSize = computed(() => (renderOptions.value.configuration ? 30 : 40));
const iconSource = computed(() => renderOptions.value.icon);
const showTooltip = ref(false);
@@ -156,8 +143,13 @@ function onActivate(event: MouseEvent) {
<ExperimentalCanvasNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
<template v-else>
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
<NodeIcon :icon-source="iconSource" :size="iconSize" :shrink="false" :disabled="isDisabled" />
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
<NodeIcon
:icon-source="iconSource"
:size="iconSize"
:shrink="false"
:disabled="isDisabled"
:class="$style.icon"
/>
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
<div :class="$style.description">
<div v-if="label" :class="$style.label">
@@ -168,24 +160,14 @@ function onActivate(event: MouseEvent) {
</div>
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
</div>
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
</template>
</div>
</template>
<style lang="scss" module>
.node {
--canvas-node--max-vertical-handles: max(
var(--canvas-node--main-input-count),
var(--canvas-node--main-output-count),
1
);
--canvas-node--height: calc(100px + max(0, var(--canvas-node--max-vertical-handles) - 3) * 42px);
--canvas-node--width: 100px;
--canvas-node-border-width: 2px;
--configurable-node--min-input-count: 4;
--configurable-node--input-width: 64px;
--configurable-node--icon-offset: 30px;
--configurable-node--icon-size: 30px;
--trigger-node--border-radius: 36px;
--canvas-node--status-icons-offset: var(--spacing-3xs);
--node-icon-color: var(--color-foreground-dark);
@@ -207,7 +189,6 @@ function onActivate(event: MouseEvent) {
}
&.settingsView {
/*margin-top: calc(var(--canvas-node--width) * 0.8);*/
height: calc(var(--canvas-node--height) * 2.4) !important;
width: calc(var(--canvas-node--width) * 1.6) !important;
align-items: flex-start;
@@ -226,13 +207,10 @@ function onActivate(event: MouseEvent) {
*/
&.configuration {
--canvas-node--width: 80px;
--canvas-node--height: 80px;
background: var(--canvas-node--background, var(--node-type-supplemental-background));
border: var(--canvas-node-border-width) solid
var(--canvas-node--border-color, var(--color-foreground-dark));
border-radius: 50px;
border-radius: calc(var(--canvas-node--height) / 2);
.statusIcons {
right: unset;
@@ -240,16 +218,8 @@ function onActivate(event: MouseEvent) {
}
&.configurable {
--canvas-node--height: 100px;
--canvas-node--width: calc(
max(var(--configurable-node--input-count, 4), var(--configurable-node--min-input-count)) *
var(--configurable-node--input-width)
);
justify-content: flex-start;
:global(.n8n-node-icon) {
margin-left: var(--configurable-node--icon-offset);
.icon {
margin-left: calc(40px - (var(--node-icon-size)) / 2 - var(--canvas-node-border-width));
}
.description {
@@ -260,11 +230,10 @@ function onActivate(event: MouseEvent) {
margin-right: var(--spacing-s);
width: auto;
min-width: unset;
max-width: calc(
var(--canvas-node--width) - var(--configurable-node--icon-offset) - var(
--configurable-node--icon-size
) - 2 * var(--spacing-s)
);
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
flex-shrink: 1;
}
.label {
@@ -276,11 +245,19 @@ function onActivate(event: MouseEvent) {
}
&.configuration {
--canvas-node--height: 75px;
.icon {
margin-left: calc((var(--canvas-node--height) - var(--node-icon-size)) / 2);
}
.statusIcons {
right: calc(-1 * var(--spacing-2xs));
bottom: 0;
&:not(.running) {
.statusIcons {
position: static;
margin-right: var(--spacing-2xs);
}
}
.description {
margin-right: var(--spacing-xs);
}
}
}
@@ -333,7 +310,6 @@ function onActivate(event: MouseEvent) {
display: flex;
flex-direction: column;
gap: var(--spacing-4xs);
align-items: center;
}
.label,
@@ -367,4 +343,9 @@ function onActivate(event: MouseEvent) {
bottom: var(--canvas-node--status-icons-offset);
right: var(--canvas-node--status-icons-offset);
}
.icon {
flex-grow: 0;
flex-shrink: 0;
}
</style>

View File

@@ -4,12 +4,12 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
<div
class="node configurable"
data-test-id="canvas-configurable-node"
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
style="--canvas-node--width: 240px; --canvas-node--height: 100px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon"
class="n8n-node-icon icon icon"
shrink="false"
>
<div
@@ -27,7 +27,6 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
</div>
</div>
<!--v-if-->
<!--v-if-->
<div
class="description"
>
@@ -43,6 +42,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
Test Node Subtitle
</div>
</div>
<!--v-if-->
</div>
`;
@@ -51,12 +51,12 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
<div
class="node configurable configuration"
data-test-id="canvas-configurable-node"
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
style="--canvas-node--width: 240px; --canvas-node--height: 75px; --node-icon-size: 30px;"
>
<!--v-if-->
<div
class="n8n-node-icon"
class="n8n-node-icon icon icon"
shrink="false"
>
<div
@@ -74,7 +74,6 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
</div>
</div>
<!--v-if-->
<!--v-if-->
<div
class="description"
>
@@ -90,6 +89,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
Test Node Subtitle
</div>
</div>
<!--v-if-->
</div>
`;
@@ -98,12 +98,12 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
<div
class="node configuration"
data-test-id="canvas-configuration-node"
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
style="--canvas-node--width: 80px; --canvas-node--height: 80px; --node-icon-size: 30px;"
>
<!--v-if-->
<div
class="n8n-node-icon"
class="n8n-node-icon icon icon"
shrink="false"
>
<div
@@ -121,7 +121,6 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
</div>
</div>
<!--v-if-->
<!--v-if-->
<div
class="description"
>
@@ -137,6 +136,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
Test Node Subtitle
</div>
</div>
<!--v-if-->
</div>
`;
@@ -145,12 +145,12 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
<div
class="node"
data-test-id="canvas-default-node"
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon"
class="n8n-node-icon icon icon"
shrink="false"
>
<div
@@ -168,7 +168,6 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
</div>
</div>
<!--v-if-->
<!--v-if-->
<div
class="description"
>
@@ -184,6 +183,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
Test Node Subtitle
</div>
</div>
<!--v-if-->
</div>
`;
@@ -192,12 +192,12 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
<div
class="node trigger"
data-test-id="canvas-trigger-node"
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon"
class="n8n-node-icon icon icon"
shrink="false"
>
<div
@@ -215,7 +215,6 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
</div>
</div>
<!--v-if-->
<!--v-if-->
<div
class="description"
>
@@ -231,6 +230,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
Test Node Subtitle
</div>
</div>
<!--v-if-->
</div>
`;

View File

@@ -115,8 +115,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_INSERT_SPACER_BETWEEN_INPUT_GROUPS = false;
export const NODE_MIN_INPUT_ITEMS_COUNT = 4;
export const NODE_MIN_INPUT_ITEMS_COUNT = 5;
// node types
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';

View File

@@ -15,6 +15,7 @@ import { CanvasConnectionMode } from '@/types';
import type { INodeUi } from '@/Interface';
import type { Connection } from '@vue-flow/core';
import { createTestNode } from '@/__tests__/mocks';
import { NODE_MIN_INPUT_ITEMS_COUNT } from '@/constants';
vi.mock('uuid', () => ({
v4: vi.fn(() => 'mock-uuid'),
@@ -990,52 +991,56 @@ describe('insertSpacersBetweenEndpoints', () => {
it('should insert spacers when there are less than min endpoints count', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([{ index: 0, required: true }, null, null, null, null]);
});
it('should not insert spacers when there are at least min endpoints count', () => {
const endpoints = [{ index: 0, required: true }, { index: 1 }, { index: 2 }, { index: 3 }];
const endpoints = [
{ index: 0, required: true },
{ index: 1 },
{ index: 2 },
{ index: 3 },
{ index: 4 },
];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual(endpoints);
});
it('should handle zero required endpoints', () => {
const endpoints = [{ index: 0, required: false }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([null, null, null, { index: 0, required: false }]);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([null, 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, 4);
expect(result).toEqual([null, null, null, null]);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([null, null, null, null, null]);
});
it('should handle required endpoints greater than min endpoints count', () => {
const endpoints = [
{ index: 0, required: true },
{ index: 1, required: true },
{ index: 2, required: true },
{ index: 3, required: true },
{ index: 4, required: true },
];
it('should handle required endpoints greater than NODE_MIN_INPUT_ITEMS_COUNT', () => {
const endpoints = Array.from({ length: NODE_MIN_INPUT_ITEMS_COUNT + 1 }).map((_, index) => ({
index,
required: true,
}));
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual(endpoints);
});
it('should insert spacers between required and optional endpoints', () => {
const endpoints = [{ index: 0, required: true }, { index: 1, required: true }, { index: 2 }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([
{ index: 0, required: true },
{ index: 1, required: true },
null,
null,
{ index: 2 },
]);
});
@@ -1043,14 +1048,7 @@ describe('insertSpacersBetweenEndpoints', () => {
it('should handle required endpoints count greater than endpoints length', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
});
it('should handle min endpoints count less than required endpoints count', () => {
const endpoints = [{ index: 0, required: false }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 0);
expect(result).toEqual([{ index: 0, required: false }]);
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount);
expect(result).toEqual([{ index: 0, required: true }, null, null, null, null]);
});
});

View File

@@ -10,6 +10,7 @@ import { CanvasConnectionMode } from '@/types';
import type { Connection } from '@vue-flow/core';
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
import { NodeConnectionTypes } from 'n8n-workflow';
import { NODE_MIN_INPUT_ITEMS_COUNT } from '@/constants';
/**
* Maps multiple legacy n8n connections to VueFlow connections
@@ -246,18 +247,15 @@ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) {
/**
* Inserts spacers between endpoints to visually separate them
*/
export function insertSpacersBetweenEndpoints<T>(
endpoints: T[],
requiredEndpointsCount = 0,
minEndpointsCount = 4,
) {
export function insertSpacersBetweenEndpoints<T>(endpoints: T[], requiredEndpointsCount = 0) {
const endpointsWithSpacers: Array<T | null> = [...endpoints];
const optionalNonMainInputsCount = endpointsWithSpacers.length - requiredEndpointsCount;
const spacerCount = minEndpointsCount - requiredEndpointsCount - optionalNonMainInputsCount;
const spacerCount =
NODE_MIN_INPUT_ITEMS_COUNT - requiredEndpointsCount - optionalNonMainInputsCount;
// Insert `null` in between required non-main inputs and non-required non-main inputs
// to separate them visually if there are less than 4 inputs in total
if (endpointsWithSpacers.length < minEndpointsCount) {
// to separate them visually if there are less than `minEndpointsCount` inputs in total
if (endpointsWithSpacers.length < NODE_MIN_INPUT_ITEMS_COUNT) {
for (let i = 0; i < spacerCount; i++) {
endpointsWithSpacers.splice(requiredEndpointsCount + i, 0, null);
}

View File

@@ -2,6 +2,7 @@ import {
AI_MCP_TOOL_NODE_TYPE,
LIST_LIKE_NODE_OPERATIONS,
MAIN_HEADER_TABS,
NODE_MIN_INPUT_ITEMS_COUNT,
NODE_POSITION_CONFLICT_ALLOWLIST,
SET_NODE_TYPE,
SPLIT_IN_BATCHES_NODE_TYPE,
@@ -33,14 +34,14 @@ import {
export const GRID_SIZE = 20;
export const NODE_SIZE = 100;
export const DEFAULT_NODE_SIZE: [number, number] = [100, 100];
export const CONFIGURATION_NODE_SIZE: [number, number] = [80, 80];
export const CONFIGURABLE_NODE_SIZE: [number, number] = [256, 100];
export const DEFAULT_START_POSITION_X = 180;
export const DEFAULT_START_POSITION_Y = 240;
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 HEADER_HEIGHT = 65;
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300;
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = GRID_SIZE * 15;
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
export const DEFAULT_VIEWPORT_BOUNDARIES: ViewportBoundaries = {
xMin: -Infinity,
@@ -600,3 +601,27 @@ export function updateViewportToContainNodes(
zoom,
};
}
export function calculateNodeSize(
isConfiguration: boolean,
isConfigurable: boolean,
mainInputCount: number,
mainOutputCount: number,
nonMainInputCount: number,
): { width: number; height: number } {
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
const height = 100 + Math.max(0, maxVerticalHandles - 3) * GRID_SIZE * 2;
if (isConfigurable) {
return {
width: (Math.max(NODE_MIN_INPUT_ITEMS_COUNT - 1, nonMainInputCount) * 2 + 4) * GRID_SIZE,
height: isConfiguration ? 75 : height,
};
}
if (isConfiguration) {
return { width: GRID_SIZE * 4, height: GRID_SIZE * 4 };
}
return { width: 100, height };
}