mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Improve configurable nodes design on new canvas (#12317)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
8cce588209
commit
0ecce10faf
|
@ -84,6 +84,33 @@ describe('CanvasNode', () => {
|
||||||
expect(inputHandles.length).toBe(3);
|
expect(inputHandles.length).toBe(3);
|
||||||
expect(outputHandles.length).toBe(2);
|
expect(outputHandles.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should insert spacers after required non-main input handle', () => {
|
||||||
|
const { getAllByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...createCanvasNodeProps({
|
||||||
|
data: {
|
||||||
|
inputs: [
|
||||||
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ type: NodeConnectionType.AiAgent, index: 0, required: true },
|
||||||
|
{ type: NodeConnectionType.AiTool, index: 0 },
|
||||||
|
],
|
||||||
|
outputs: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
Handle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputHandles = getAllByTestId('canvas-node-input-handle');
|
||||||
|
|
||||||
|
expect(inputHandles[1]).toHaveStyle('left: 20%');
|
||||||
|
expect(inputHandles[2]).toHaveStyle('left: 80%');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toolbar', () => {
|
describe('toolbar', () => {
|
||||||
|
|
|
@ -28,7 +28,10 @@ import { useContextMenu } from '@/composables/useContextMenu';
|
||||||
import type { NodeProps, XYPosition } from '@vue-flow/core';
|
import type { NodeProps, XYPosition } from '@vue-flow/core';
|
||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import {
|
||||||
|
createCanvasConnectionHandleString,
|
||||||
|
insertSpacersBetweenEndpoints,
|
||||||
|
} from '@/utils/canvasUtilsV2';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
@ -77,12 +80,18 @@ const nodeClasses = ref<string[]>([]);
|
||||||
const inputs = computed(() => props.data.inputs);
|
const inputs = computed(() => props.data.inputs);
|
||||||
const outputs = computed(() => props.data.outputs);
|
const outputs = computed(() => props.data.outputs);
|
||||||
const connections = computed(() => props.data.connections);
|
const connections = computed(() => props.data.connections);
|
||||||
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } =
|
const {
|
||||||
useNodeConnections({
|
mainInputs,
|
||||||
inputs,
|
nonMainInputs,
|
||||||
outputs,
|
requiredNonMainInputs,
|
||||||
connections,
|
mainOutputs,
|
||||||
});
|
nonMainOutputs,
|
||||||
|
isValidConnection,
|
||||||
|
} = useNodeConnections({
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
const isDisabled = computed(() => props.data.disabled);
|
const isDisabled = computed(() => props.data.disabled);
|
||||||
|
|
||||||
|
@ -114,23 +123,15 @@ function emitCanvasNodeEvent(event: CanvasEventBusEvents['nodes:action']) {
|
||||||
* Inputs
|
* Inputs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const nonMainInputsWithSpacer = computed(() =>
|
||||||
|
insertSpacersBetweenEndpoints(nonMainInputs.value, requiredNonMainInputs.value.length),
|
||||||
|
);
|
||||||
|
|
||||||
const mappedInputs = computed(() => {
|
const mappedInputs = computed(() => {
|
||||||
return [
|
return [
|
||||||
...mainInputs.value.map(
|
...mainInputs.value.map(mainInputsMappingFn),
|
||||||
createEndpointMappingFn({
|
...nonMainInputsWithSpacer.value.map(nonMainInputsMappingFn),
|
||||||
mode: CanvasConnectionMode.Input,
|
].filter((endpoint) => !!endpoint);
|
||||||
position: Position.Left,
|
|
||||||
offsetAxis: 'top',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...nonMainInputs.value.map(
|
|
||||||
createEndpointMappingFn({
|
|
||||||
mode: CanvasConnectionMode.Input,
|
|
||||||
position: Position.Bottom,
|
|
||||||
offsetAxis: 'left',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,21 +140,9 @@ const mappedInputs = computed(() => {
|
||||||
|
|
||||||
const mappedOutputs = computed(() => {
|
const mappedOutputs = computed(() => {
|
||||||
return [
|
return [
|
||||||
...mainOutputs.value.map(
|
...mainOutputs.value.map(mainOutputsMappingFn),
|
||||||
createEndpointMappingFn({
|
...nonMainOutputs.value.map(nonMainOutputsMappingFn),
|
||||||
mode: CanvasConnectionMode.Output,
|
].filter((endpoint) => !!endpoint);
|
||||||
position: Position.Right,
|
|
||||||
offsetAxis: 'top',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...nonMainOutputs.value.map(
|
|
||||||
createEndpointMappingFn({
|
|
||||||
mode: CanvasConnectionMode.Output,
|
|
||||||
position: Position.Top,
|
|
||||||
offsetAxis: 'left',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,10 +168,14 @@ const createEndpointMappingFn =
|
||||||
offsetAxis: 'top' | 'left';
|
offsetAxis: 'top' | 'left';
|
||||||
}) =>
|
}) =>
|
||||||
(
|
(
|
||||||
endpoint: CanvasConnectionPort,
|
endpoint: CanvasConnectionPort | null,
|
||||||
index: number,
|
index: number,
|
||||||
endpoints: CanvasConnectionPort[],
|
endpoints: Array<CanvasConnectionPort | null>,
|
||||||
): CanvasElementPortWithRenderData => {
|
): CanvasElementPortWithRenderData | undefined => {
|
||||||
|
if (!endpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleId = createCanvasConnectionHandleString({
|
const handleId = createCanvasConnectionHandleString({
|
||||||
mode,
|
mode,
|
||||||
type: endpoint.type,
|
type: endpoint.type,
|
||||||
|
@ -207,6 +200,30 @@ const createEndpointMappingFn =
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mainInputsMappingFn = createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
position: Position.Left,
|
||||||
|
offsetAxis: 'top',
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonMainInputsMappingFn = createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
position: Position.Bottom,
|
||||||
|
offsetAxis: 'left',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainOutputsMappingFn = createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
position: Position.Right,
|
||||||
|
offsetAxis: 'top',
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonMainOutputsMappingFn = createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
position: Position.Top,
|
||||||
|
offsetAxis: 'left',
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events
|
* Events
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -201,6 +201,12 @@ function openContextMenu(event: MouseEvent) {
|
||||||
var(--configurable-node--input-width)
|
var(--configurable-node--input-width)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
:global(.n8n-node-icon) {
|
||||||
|
margin-left: var(--configurable-node--icon-offset);
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
top: unset;
|
top: unset;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import {
|
import {
|
||||||
|
checkOverlap,
|
||||||
createCanvasConnectionHandleString,
|
createCanvasConnectionHandleString,
|
||||||
createCanvasConnectionId,
|
createCanvasConnectionId,
|
||||||
|
insertSpacersBetweenEndpoints,
|
||||||
mapCanvasConnectionToLegacyConnection,
|
mapCanvasConnectionToLegacyConnection,
|
||||||
mapLegacyConnectionsToCanvasConnections,
|
mapLegacyConnectionsToCanvasConnections,
|
||||||
mapLegacyEndpointsToCanvasConnectionPort,
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||||||
parseCanvasConnectionHandleString,
|
parseCanvasConnectionHandleString,
|
||||||
checkOverlap,
|
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
|
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { IConnections, INodeTypeDescription, IConnection } from 'n8n-workflow';
|
|
||||||
import type { CanvasConnection } from '@/types';
|
import type { CanvasConnection } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
|
@ -976,3 +977,72 @@ describe('checkOverlap', () => {
|
||||||
expect(checkOverlap(node1, node2)).toBe(false);
|
expect(checkOverlap(node1, node2)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
|
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
|
||||||
|
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 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 },
|
||||||
|
];
|
||||||
|
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
|
||||||
|
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
|
||||||
|
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);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ index: 0, required: true },
|
||||||
|
{ index: 1, required: true },
|
||||||
|
null,
|
||||||
|
{ index: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -210,3 +210,23 @@ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insertSpacersBetweenEndpoints<T>(
|
||||||
|
endpoints: T[],
|
||||||
|
requiredEndpointsCount = 0,
|
||||||
|
minEndpointsCount = 4,
|
||||||
|
) {
|
||||||
|
const endpointsWithSpacers: Array<T | null> = [...endpoints];
|
||||||
|
const optionalNonMainInputsCount = endpointsWithSpacers.length - requiredEndpointsCount;
|
||||||
|
const spacerCount = minEndpointsCount - 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) {
|
||||||
|
for (let i = 0; i < spacerCount; i++) {
|
||||||
|
endpointsWithSpacers.splice(requiredEndpointsCount + i, 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointsWithSpacers;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue