mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Make adjustments to status icon and connector port position in canvas (#16469)
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user