From 685d66526831f5514a60339822fa28d716daa393 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 19 Dec 2024 16:18:51 +0200 Subject: [PATCH 1/4] fix(editor): fix configurable node design --- .../canvas/elements/nodes/CanvasNode.test.ts | 27 +++++ .../canvas/elements/nodes/CanvasNode.vue | 114 ++++++++++++------ 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts index 6b72cdf037afa..816310ac7d5d8 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts @@ -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', () => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue index dd558bbef5365..01801ff4e632e 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -65,12 +65,18 @@ const { connectingHandle } = useCanvas(); 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); @@ -101,45 +107,45 @@ 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).filter(Boolean), + ...nonMainInputsWithSpacer.value.map(nonMainInputsMappingFn).filter(Boolean), ]; }); +function insertSpacersBetweenEndpoints( + endpoints: CanvasConnectionPort[], + requiredEndpointsCount = 0, + minEndpointsCount = 4, +) { + const endpointsWithSpacers: Array = [...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 && requiredEndpointsCount > 0) { + for (let i = 0; i < spacerCount; i++) { + endpointsWithSpacers.splice(requiredEndpointsCount + i, 0, null); + } + } + + return endpointsWithSpacers; +} + /** * Outputs */ 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).filter(Boolean), + ...nonMainOutputs.value.map(nonMainOutputsMappingFn).filter(Boolean), ]; }); @@ -166,10 +172,14 @@ const createEndpointMappingFn = offsetAxis: 'top' | 'left'; }) => ( - endpoint: CanvasConnectionPort, + endpoint: CanvasConnectionPort | null, index: number, - endpoints: CanvasConnectionPort[], - ): CanvasElementPortWithRenderData => { + endpoints: Array, + ): CanvasElementPortWithRenderData | undefined => { + if (!endpoint) { + return; + } + const handleId = createCanvasConnectionHandleString({ mode, type: endpoint.type, @@ -194,6 +204,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 */ @@ -293,9 +327,10 @@ onBeforeUnmount(() => {