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(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', () => {
|
||||
|
|
|
@ -28,7 +28,10 @@ import { useContextMenu } from '@/composables/useContextMenu';
|
|||
import type { NodeProps, XYPosition } from '@vue-flow/core';
|
||||
import { Position } from '@vue-flow/core';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||
import {
|
||||
createCanvasConnectionHandleString,
|
||||
insertSpacersBetweenEndpoints,
|
||||
} from '@/utils/canvasUtilsV2';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
@ -77,12 +80,18 @@ const nodeClasses = ref<string[]>([]);
|
|||
const inputs = computed(() => props.data.inputs);
|
||||
const outputs = computed(() => props.data.outputs);
|
||||
const connections = computed(() => props.data.connections);
|
||||
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } =
|
||||
useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
const {
|
||||
mainInputs,
|
||||
nonMainInputs,
|
||||
requiredNonMainInputs,
|
||||
mainOutputs,
|
||||
nonMainOutputs,
|
||||
isValidConnection,
|
||||
} = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => props.data.disabled);
|
||||
|
||||
|
@ -114,23 +123,15 @@ function emitCanvasNodeEvent(event: CanvasEventBusEvents['nodes:action']) {
|
|||
* Inputs
|
||||
*/
|
||||
|
||||
const nonMainInputsWithSpacer = computed(() =>
|
||||
insertSpacersBetweenEndpoints(nonMainInputs.value, requiredNonMainInputs.value.length),
|
||||
);
|
||||
|
||||
const mappedInputs = computed(() => {
|
||||
return [
|
||||
...mainInputs.value.map(
|
||||
createEndpointMappingFn({
|
||||
mode: CanvasConnectionMode.Input,
|
||||
position: Position.Left,
|
||||
offsetAxis: 'top',
|
||||
}),
|
||||
),
|
||||
...nonMainInputs.value.map(
|
||||
createEndpointMappingFn({
|
||||
mode: CanvasConnectionMode.Input,
|
||||
position: Position.Bottom,
|
||||
offsetAxis: 'left',
|
||||
}),
|
||||
),
|
||||
];
|
||||
...mainInputs.value.map(mainInputsMappingFn),
|
||||
...nonMainInputsWithSpacer.value.map(nonMainInputsMappingFn),
|
||||
].filter((endpoint) => !!endpoint);
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -139,21 +140,9 @@ const mappedInputs = computed(() => {
|
|||
|
||||
const mappedOutputs = computed(() => {
|
||||
return [
|
||||
...mainOutputs.value.map(
|
||||
createEndpointMappingFn({
|
||||
mode: CanvasConnectionMode.Output,
|
||||
position: Position.Right,
|
||||
offsetAxis: 'top',
|
||||
}),
|
||||
),
|
||||
...nonMainOutputs.value.map(
|
||||
createEndpointMappingFn({
|
||||
mode: CanvasConnectionMode.Output,
|
||||
position: Position.Top,
|
||||
offsetAxis: 'left',
|
||||
}),
|
||||
),
|
||||
];
|
||||
...mainOutputs.value.map(mainOutputsMappingFn),
|
||||
...nonMainOutputs.value.map(nonMainOutputsMappingFn),
|
||||
].filter((endpoint) => !!endpoint);
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -179,10 +168,14 @@ const createEndpointMappingFn =
|
|||
offsetAxis: 'top' | 'left';
|
||||
}) =>
|
||||
(
|
||||
endpoint: CanvasConnectionPort,
|
||||
endpoint: CanvasConnectionPort | null,
|
||||
index: number,
|
||||
endpoints: CanvasConnectionPort[],
|
||||
): CanvasElementPortWithRenderData => {
|
||||
endpoints: Array<CanvasConnectionPort | null>,
|
||||
): CanvasElementPortWithRenderData | undefined => {
|
||||
if (!endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleId = createCanvasConnectionHandleString({
|
||||
mode,
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -201,6 +201,12 @@ function openContextMenu(event: MouseEvent) {
|
|||
var(--configurable-node--input-width)
|
||||
);
|
||||
|
||||
justify-content: flex-start;
|
||||
|
||||
:global(.n8n-node-icon) {
|
||||
margin-left: var(--configurable-node--icon-offset);
|
||||
}
|
||||
|
||||
.description {
|
||||
top: unset;
|
||||
position: relative;
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {
|
||||
checkOverlap,
|
||||
createCanvasConnectionHandleString,
|
||||
createCanvasConnectionId,
|
||||
insertSpacersBetweenEndpoints,
|
||||
mapCanvasConnectionToLegacyConnection,
|
||||
mapLegacyConnectionsToCanvasConnections,
|
||||
mapLegacyEndpointsToCanvasConnectionPort,
|
||||
parseCanvasConnectionHandleString,
|
||||
checkOverlap,
|
||||
} from '@/utils/canvasUtilsV2';
|
||||
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { IConnections, INodeTypeDescription, IConnection } from 'n8n-workflow';
|
||||
import type { CanvasConnection } from '@/types';
|
||||
import { CanvasConnectionMode } from '@/types';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
@ -976,3 +977,72 @@ describe('checkOverlap', () => {
|
|||
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