diff --git a/packages/editor-ui/src/__tests__/data/canvas.ts b/packages/editor-ui/src/__tests__/data/canvas.ts index 40f0060fe2..95c2ff3a5e 100644 --- a/packages/editor-ui/src/__tests__/data/canvas.ts +++ b/packages/editor-ui/src/__tests__/data/canvas.ts @@ -1,7 +1,13 @@ -import { CanvasNodeKey } from '@/constants'; +import { CanvasNodeHandleKey, CanvasNodeKey } from '@/constants'; import { ref } from 'vue'; -import type { CanvasNode, CanvasNodeData } from '@/types'; -import { CanvasNodeRenderType } from '@/types'; +import type { + CanvasNode, + CanvasNodeData, + CanvasNodeHandleInjectionData, + CanvasNodeInjectionData, +} from '@/types'; +import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; +import { NodeConnectionType } from 'n8n-workflow'; export function createCanvasNodeData({ id = 'node', @@ -11,7 +17,7 @@ export function createCanvasNodeData({ disabled = false, inputs = [], outputs = [], - connections = { input: {}, output: {} }, + connections = { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} }, execution = { running: false }, issues = { items: [], visible: false }, pinnedData = { count: 0, visible: false }, @@ -73,7 +79,12 @@ export function createCanvasNodeProvide({ label = 'Test Node', selected = false, data = {}, -}: { id?: string; label?: string; selected?: boolean; data?: Partial } = {}) { +}: { + id?: string; + label?: string; + selected?: boolean; + data?: Partial; +} = {}) { const props = createCanvasNodeProps({ id, label, selected, data }); return { [`${CanvasNodeKey}`]: { @@ -81,7 +92,28 @@ export function createCanvasNodeProvide({ label: ref(props.label), selected: ref(props.selected), data: ref(props.data), - }, + } satisfies CanvasNodeInjectionData, + }; +} + +export function createCanvasHandleProvide({ + label = 'Handle', + mode = CanvasConnectionMode.Input, + type = NodeConnectionType.Main, + connected = false, +}: { + label?: string; + mode?: CanvasConnectionMode; + type?: NodeConnectionType; + connected?: boolean; +} = {}) { + return { + [`${CanvasNodeHandleKey}`]: { + label: ref(label), + mode: ref(mode), + type: ref(type), + connected: ref(connected), + } satisfies CanvasNodeHandleInjectionData, }; } diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 122f98d11f..e0fa85ff77 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -26,6 +26,7 @@ const emit = defineEmits<{ 'update:node:selected': [id: string]; 'update:node:name': [id: string]; 'update:node:parameters': [id: string, parameters: Record]; + 'click:node:add': [id: string, handle: string]; 'run:node': [id: string]; 'delete:node': [id: string]; 'create:node': [source: NodeCreatorOpenSource]; @@ -108,6 +109,10 @@ const paneReady = ref(false); * Nodes */ +function onClickNodeAdd(id: string, handle: string) { + emit('click:node:add', id, handle); +} + function onNodeDragStop(e: NodeDragEvent) { e.nodes.forEach((node) => { onUpdateNodePosition(node.id, node.position); @@ -351,6 +356,7 @@ onPaneReady(async () => { @open:contextmenu="onOpenNodeContextMenu" @update="onUpdateNodeParameters" @move="onUpdateNodePosition" + @add="onClickNodeAdd" /> diff --git a/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.spec.ts b/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.spec.ts index 397033a5f0..9659770b68 100644 --- a/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.spec.ts @@ -4,6 +4,7 @@ import { createComponentRenderer } from '@/__tests__/render'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { Position } from '@vue-flow/core'; +import { NodeConnectionType } from 'n8n-workflow'; const DEFAULT_PROPS = { sourceX: 0, @@ -14,8 +15,8 @@ const DEFAULT_PROPS = { targetPosition: Position.Bottom, data: { status: undefined, - source: { index: 0, type: 'main' }, - target: { index: 0, type: 'main' }, + source: { index: 0, type: NodeConnectionType.Main }, + target: { index: 0, type: NodeConnectionType.Main }, }, } satisfies Partial; const renderComponent = createComponentRenderer(CanvasEdge, { diff --git a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.spec.ts b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.spec.ts similarity index 76% rename from packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.spec.ts rename to packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.spec.ts index 75d4889f8d..bbbdc008dc 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.spec.ts @@ -1,20 +1,21 @@ -import HandleRenderer from '@/components/canvas/elements/handles/HandleRenderer.vue'; +import CanvasHandleRenderer from '@/components/canvas/elements/handles/CanvasHandleRenderer.vue'; import { NodeConnectionType } from 'n8n-workflow'; import { createComponentRenderer } from '@/__tests__/render'; import { CanvasNodeHandleKey } from '@/constants'; import { ref } from 'vue'; +import { CanvasConnectionMode } from '@/types'; -const renderComponent = createComponentRenderer(HandleRenderer); +const renderComponent = createComponentRenderer(CanvasHandleRenderer); const Handle = { template: '
', }; -describe('HandleRenderer', () => { +describe('CanvasHandleRenderer', () => { it('should render the main input handle correctly', async () => { const { container } = renderComponent({ props: { - mode: 'input', + mode: CanvasConnectionMode.Input, type: NodeConnectionType.Main, index: 0, position: 'left', @@ -29,13 +30,13 @@ describe('HandleRenderer', () => { }); expect(container.querySelector('.handle')).toBeInTheDocument(); - expect(container.querySelector('.canvas-node-handle-main-input')).toBeInTheDocument(); + expect(container.querySelector('.inputs.main')).toBeInTheDocument(); }); it('should render the main output handle correctly', async () => { const { container } = renderComponent({ props: { - mode: 'output', + mode: CanvasConnectionMode.Output, type: NodeConnectionType.Main, index: 0, position: 'right', @@ -50,13 +51,13 @@ describe('HandleRenderer', () => { }); expect(container.querySelector('.handle')).toBeInTheDocument(); - expect(container.querySelector('.canvas-node-handle-main-output')).toBeInTheDocument(); + expect(container.querySelector('.outputs.main')).toBeInTheDocument(); }); it('should render the non-main handle correctly', async () => { const { container } = renderComponent({ props: { - mode: 'input', + mode: CanvasConnectionMode.Input, type: NodeConnectionType.AiTool, index: 0, position: 'top', @@ -71,7 +72,7 @@ describe('HandleRenderer', () => { }); expect(container.querySelector('.handle')).toBeInTheDocument(); - expect(container.querySelector('.canvas-node-handle-non-main')).toBeInTheDocument(); + expect(container.querySelector('.inputs.ai_tool')).toBeInTheDocument(); }); it('should provide the label correctly', async () => { diff --git a/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue new file mode 100644 index 0000000000..b6a9235bc8 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue b/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue deleted file mode 100644 index f1c42532ee..0000000000 --- a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.spec.ts b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.spec.ts deleted file mode 100644 index 1cc06fc74c..0000000000 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import CanvasHandleMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue'; -import { createComponentRenderer } from '@/__tests__/render'; -import { CanvasNodeHandleKey } from '@/constants'; -import { ref } from 'vue'; - -const renderComponent = createComponentRenderer(CanvasHandleMainInput); - -describe('CanvasHandleMainInput', () => { - it('should render correctly', async () => { - const label = 'Test Label'; - const { container, getByText } = renderComponent({ - global: { - provide: { - [`${CanvasNodeHandleKey}`]: { label: ref(label) }, - }, - }, - }); - - expect(container.querySelector('.canvas-node-handle-main-input')).toBeInTheDocument(); - expect(getByText(label)).toBeInTheDocument(); - }); -}); diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue deleted file mode 100644 index dd9bdec44d..0000000000 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.spec.ts b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.spec.ts index c324e3a2e7..22b8bbce31 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.spec.ts @@ -1,7 +1,6 @@ import CanvasHandleMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue'; import { createComponentRenderer } from '@/__tests__/render'; -import { CanvasNodeHandleKey } from '@/constants'; -import { ref } from 'vue'; +import { createCanvasHandleProvide } from '@/__tests__/data'; const renderComponent = createComponentRenderer(CanvasHandleMainOutput); @@ -11,7 +10,7 @@ describe('CanvasHandleMainOutput', () => { const { container, getByText } = renderComponent({ global: { provide: { - [`${CanvasNodeHandleKey}`]: { label: ref(label) }, + ...createCanvasHandleProvide({ label }), }, }, }); diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue index c0d016cbf1..ba9e32ade4 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue @@ -1,89 +1,27 @@ diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMain.spec.ts b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.spec.ts similarity index 60% rename from packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMain.spec.ts rename to packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.spec.ts index 7544c7587a..9e9dab81e0 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMain.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.spec.ts @@ -1,17 +1,16 @@ -import CanvasHandleNonMain from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMain.vue'; +import CanvasHandleNonMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue'; import { createComponentRenderer } from '@/__tests__/render'; -import { CanvasNodeHandleKey } from '@/constants'; -import { ref } from 'vue'; +import { createCanvasHandleProvide } from '@/__tests__/data'; -const renderComponent = createComponentRenderer(CanvasHandleNonMain); +const renderComponent = createComponentRenderer(CanvasHandleNonMainInput); -describe('CanvasHandleNonMain', () => { +describe('CanvasHandleNonMainInput', () => { it('should render correctly', async () => { const label = 'Test Label'; const { container, getByText } = renderComponent({ global: { provide: { - [`${CanvasNodeHandleKey}`]: { label: ref(label) }, + ...createCanvasHandleProvide({ label }), }, }, }); diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue new file mode 100644 index 0000000000..44f9f09f98 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue @@ -0,0 +1,49 @@ + + + + diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.spec.ts b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.spec.ts new file mode 100644 index 0000000000..841d11a44e --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.spec.ts @@ -0,0 +1,54 @@ +import { fireEvent } from '@testing-library/vue'; +import CanvasHandlePlus from './CanvasHandlePlus.vue'; +import { createComponentRenderer } from '@/__tests__/render'; +import { createCanvasHandleProvide } from '@/__tests__/data'; + +const renderComponent = createComponentRenderer(CanvasHandlePlus, { + global: { + provide: { + ...createCanvasHandleProvide(), + }, + }, +}); + +describe('CanvasHandlePlus', () => { + it('should render with default props', () => { + const { html } = renderComponent(); + + expect(html()).toMatchSnapshot(); + }); + + it('emits click:plus event when plus icon is clicked', async () => { + const { container, emitted } = renderComponent(); + const plusIcon = container.querySelector('svg.plus'); + + if (!plusIcon) throw new Error('Plus icon not found'); + + await fireEvent.click(plusIcon); + + expect(emitted()).toHaveProperty('click:plus'); + }); + + it('applies correct classes based on position prop', () => { + const positions = ['top', 'right', 'bottom', 'left']; + + positions.forEach((position) => { + const { container } = renderComponent({ + props: { position }, + }); + expect(container.firstChild).toHaveClass(position); + }); + }); + + it('renders SVG elements correctly', () => { + const { container } = renderComponent(); + + const lineSvg = container.querySelector('svg.line'); + expect(lineSvg).toBeTruthy(); + expect(lineSvg?.getAttribute('viewBox')).toBe('0 0 46 24'); + + const plusSvg = container.querySelector('svg.plus'); + expect(plusSvg).toBeTruthy(); + expect(plusSvg?.getAttribute('viewBox')).toBe('0 0 24 24'); + }); +}); diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue new file mode 100644 index 0000000000..27d9bceb1c --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/__snapshots__/CanvasHandlePlus.spec.ts.snap b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/__snapshots__/CanvasHandlePlus.spec.ts.snap new file mode 100644 index 0000000000..585581dfae --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/__snapshots__/CanvasHandlePlus.spec.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CanvasHandlePlus > should render with default props 1`] = ` +"
+ + + + + + +
" +`; diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.spec.ts index b19f16c883..8780eabf97 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.spec.ts @@ -66,7 +66,7 @@ describe('CanvasNode', () => { }, global: { stubs: { - HandleRenderer: true, + CanvasHandleRenderer: true, }, }, }); 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 fc201c281b..c94a1fbb6a 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -1,11 +1,16 @@ @@ -167,34 +209,44 @@ watch( :class="[$style.canvasNode, { [$style.showToolbar]: showToolbar }]" data-test-id="canvas-node" > - - { ...createCanvasNodeProvide({ data: { connections: { - input: { + [CanvasConnectionMode.Input]: { [NodeConnectionType.Main]: [ [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], ], }, - output: { + [CanvasConnectionMode.Output]: { [NodeConnectionType.Main]: [ [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], ], diff --git a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts index 05fe2738fa..e4fef99039 100644 --- a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts @@ -6,7 +6,10 @@ import { CanvasConnectionMode } from '@/types'; import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; describe('useNodeConnections', () => { - const defaultConnections = { input: {}, output: {} }; + const defaultConnections = { + [CanvasConnectionMode.Input]: {}, + [CanvasConnectionMode.Output]: {}, + }; describe('mainInputs', () => { it('should return main inputs when provided with main inputs', () => { const inputs = ref([ @@ -73,13 +76,13 @@ describe('useNodeConnections', () => { const inputs = ref([]); const outputs = ref([]); const connections = ref({ - input: { + [CanvasConnectionMode.Input]: { [NodeConnectionType.Main]: [ [{ node: 'node1', type: NodeConnectionType.Main, index: 0 }], [{ node: 'node2', type: NodeConnectionType.Main, index: 0 }], ], }, - output: {}, + [CanvasConnectionMode.Output]: {}, }); const { mainInputConnections } = useNodeConnections({ @@ -89,7 +92,9 @@ describe('useNodeConnections', () => { }); expect(mainInputConnections.value.length).toBe(2); - expect(mainInputConnections.value).toEqual(connections.value.input[NodeConnectionType.Main]); + expect(mainInputConnections.value).toEqual( + connections.value[CanvasConnectionMode.Input][NodeConnectionType.Main], + ); }); }); @@ -139,8 +144,8 @@ describe('useNodeConnections', () => { const inputs = ref([]); const outputs = ref([]); const connections = ref({ - input: {}, - output: { + [CanvasConnectionMode.Input]: {}, + [CanvasConnectionMode.Output]: { [NodeConnectionType.Main]: [ [{ node: 'node1', type: NodeConnectionType.Main, index: 0 }], [{ node: 'node2', type: NodeConnectionType.Main, index: 0 }], @@ -156,7 +161,7 @@ describe('useNodeConnections', () => { expect(mainOutputConnections.value.length).toBe(2); expect(mainOutputConnections.value).toEqual( - connections.value.output[NodeConnectionType.Main], + connections.value[CanvasConnectionMode.Output][NodeConnectionType.Main], ); }); }); diff --git a/packages/editor-ui/src/composables/useCanvasMapping.spec.ts b/packages/editor-ui/src/composables/useCanvasMapping.spec.ts index 79efa76f53..3ededa8bb3 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.spec.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.spec.ts @@ -122,11 +122,11 @@ describe('useCanvasMapping', () => { }, ], connections: { - input: {}, - output: {}, + [CanvasConnectionMode.Input]: {}, + [CanvasConnectionMode.Output]: {}, }, render: { - type: 'default', + type: CanvasNodeRenderType.Default, options: { configurable: false, configuration: false, @@ -205,10 +205,14 @@ describe('useCanvasMapping', () => { workflowObject: ref(workflowObject) as Ref, }); - expect(mappedNodes.value[0]?.data?.connections.output).toHaveProperty( + expect(mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output]).toHaveProperty( NodeConnectionType.Main, ); - expect(mappedNodes.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual( + expect( + mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][ + NodeConnectionType.Main + ][0][0], + ).toEqual( expect.objectContaining({ node: setNode.name, type: NodeConnectionType.Main, @@ -216,8 +220,14 @@ describe('useCanvasMapping', () => { }), ); - expect(mappedNodes.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main); - expect(mappedNodes.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual( + expect(mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input]).toHaveProperty( + NodeConnectionType.Main, + ); + expect( + mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][ + NodeConnectionType.Main + ][0][0], + ).toEqual( expect.objectContaining({ node: manualTriggerNode.name, type: NodeConnectionType.Main, diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts index b20e57ae92..c01e4c8081 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.ts @@ -18,7 +18,7 @@ import type { CanvasNodeDefaultRender, CanvasNodeStickyNoteRender, } from '@/types'; -import { CanvasNodeRenderType } from '@/types'; +import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { mapLegacyConnectionsToCanvasConnections, mapLegacyEndpointsToCanvasConnectionPort, @@ -263,8 +263,8 @@ export function useCanvasMapping({ inputs: nodeInputsById.value[node.id] ?? [], outputs: nodeOutputsById.value[node.id] ?? [], connections: { - input: inputConnections, - output: outputConnections, + [CanvasConnectionMode.Input]: inputConnections, + [CanvasConnectionMode.Output]: outputConnections, }, issues: { items: nodeIssuesById.value[node.id], diff --git a/packages/editor-ui/src/composables/useCanvasNode.spec.ts b/packages/editor-ui/src/composables/useCanvasNode.spec.ts index 9386c2906e..d5329c4741 100644 --- a/packages/editor-ui/src/composables/useCanvasNode.spec.ts +++ b/packages/editor-ui/src/composables/useCanvasNode.spec.ts @@ -1,7 +1,8 @@ import { useCanvasNode } from '@/composables/useCanvasNode'; import { inject, ref } from 'vue'; -import type { CanvasNodeInjectionData } from '../types'; -import { CanvasNodeRenderType } from '../types'; +import type { CanvasNodeData, CanvasNodeInjectionData } from '../types'; +import { CanvasConnectionMode, CanvasNodeRenderType } from '../types'; +import { NodeConnectionType } from 'n8n-workflow'; vi.mock('vue', async () => { const actual = await vi.importActual('vue'); @@ -18,7 +19,10 @@ describe('useCanvasNode', () => { expect(result.label.value).toBe(''); expect(result.inputs.value).toEqual([]); expect(result.outputs.value).toEqual([]); - expect(result.connections.value).toEqual({ input: {}, output: {} }); + expect(result.connections.value).toEqual({ + [CanvasConnectionMode.Input]: {}, + [CanvasConnectionMode.Output]: {}, + }); expect(result.isDisabled.value).toBe(false); expect(result.isSelected.value).toBeUndefined(); expect(result.pinnedDataCount.value).toBe(0); @@ -41,9 +45,12 @@ describe('useCanvasNode', () => { type: 'nodeType1', typeVersion: 1, disabled: true, - inputs: [{ type: 'main', index: 0 }], - outputs: [{ type: 'main', index: 0 }], - connections: { input: { '0': [] }, output: {} }, + inputs: [{ type: NodeConnectionType.Main, index: 0 }], + outputs: [{ type: NodeConnectionType.Main, index: 0 }], + connections: { + [CanvasConnectionMode.Input]: { '0': [] }, + [CanvasConnectionMode.Output]: {}, + }, issues: { items: ['issue1'], visible: true }, execution: { status: 'running', waiting: 'waiting', running: true }, runData: { count: 1, visible: true }, @@ -56,7 +63,7 @@ describe('useCanvasNode', () => { trigger: false, }, }, - }), + } satisfies CanvasNodeData), id: ref('1'), label: ref('Node 1'), selected: ref(true), @@ -68,9 +75,12 @@ describe('useCanvasNode', () => { expect(result.label.value).toBe('Node 1'); expect(result.name.value).toBe('Node 1'); - expect(result.inputs.value).toEqual([{ type: 'main', index: 0 }]); - expect(result.outputs.value).toEqual([{ type: 'main', index: 0 }]); - expect(result.connections.value).toEqual({ input: { '0': [] }, output: {} }); + expect(result.inputs.value).toEqual([{ type: NodeConnectionType.Main, index: 0 }]); + expect(result.outputs.value).toEqual([{ type: NodeConnectionType.Main, index: 0 }]); + expect(result.connections.value).toEqual({ + [CanvasConnectionMode.Input]: { '0': [] }, + [CanvasConnectionMode.Output]: {}, + }); expect(result.isDisabled.value).toBe(true); expect(result.isSelected.value).toBe(true); expect(result.pinnedDataCount.value).toBe(1); diff --git a/packages/editor-ui/src/composables/useCanvasNode.ts b/packages/editor-ui/src/composables/useCanvasNode.ts index 20d47d83f6..86eed0e1b0 100644 --- a/packages/editor-ui/src/composables/useCanvasNode.ts +++ b/packages/editor-ui/src/composables/useCanvasNode.ts @@ -6,7 +6,7 @@ import { CanvasNodeKey } from '@/constants'; import { computed, inject } from 'vue'; import type { CanvasNodeData } from '@/types'; -import { CanvasNodeRenderType } from '@/types'; +import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types'; export function useCanvasNode() { const node = inject(CanvasNodeKey); @@ -20,7 +20,7 @@ export function useCanvasNode() { disabled: false, inputs: [], outputs: [], - connections: { input: {}, output: {} }, + connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} }, issues: { items: [], visible: false }, pinnedData: { count: 0, visible: false }, execution: { diff --git a/packages/editor-ui/src/composables/useCanvasNodeHandle.ts b/packages/editor-ui/src/composables/useCanvasNodeHandle.ts new file mode 100644 index 0000000000..260849a3cb --- /dev/null +++ b/packages/editor-ui/src/composables/useCanvasNodeHandle.ts @@ -0,0 +1,25 @@ +/** + * Canvas V2 Only + * @TODO Remove this notice when Canvas V2 is the only one in use + */ + +import { CanvasNodeHandleKey } from '@/constants'; +import { computed, inject } from 'vue'; +import { NodeConnectionType } from 'n8n-workflow'; +import { CanvasConnectionMode } from '@/types'; + +export function useCanvasNodeHandle() { + const handle = inject(CanvasNodeHandleKey); + + const label = computed(() => handle?.label.value ?? ''); + const connected = computed(() => handle?.connected.value ?? false); + const type = computed(() => handle?.type.value ?? NodeConnectionType.Main); + const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input); + + return { + label, + connected, + type, + mode, + }; +} diff --git a/packages/editor-ui/src/composables/useNodeConnections.ts b/packages/editor-ui/src/composables/useNodeConnections.ts index 57c94ca873..0da4af618d 100644 --- a/packages/editor-ui/src/composables/useNodeConnections.ts +++ b/packages/editor-ui/src/composables/useNodeConnections.ts @@ -1,4 +1,5 @@ import type { CanvasNodeData } from '@/types'; +import { CanvasConnectionMode } from '@/types'; import type { MaybeRef } from 'vue'; import { computed, unref } from 'vue'; import { NodeConnectionType } from 'n8n-workflow'; @@ -31,7 +32,7 @@ export function useNodeConnections({ ); const mainInputConnections = computed( - () => unref(connections).input[NodeConnectionType.Main] ?? [], + () => unref(connections)[CanvasConnectionMode.Input][NodeConnectionType.Main] ?? [], ); /** @@ -47,7 +48,7 @@ export function useNodeConnections({ ); const mainOutputConnections = computed( - () => unref(connections).output[NodeConnectionType.Main] ?? [], + () => unref(connections)[CanvasConnectionMode.Output][NodeConnectionType.Main] ?? [], ); /** diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts index b9ff8efe34..dc2b9032ae 100644 --- a/packages/editor-ui/src/types/canvas.ts +++ b/packages/editor-ui/src/types/canvas.ts @@ -1,17 +1,16 @@ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */ import type { - ConnectionTypes, ExecutionStatus, - IConnection, INodeConnections, - INodeTypeDescription, + IConnection, + NodeConnectionType, } from 'n8n-workflow'; import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core'; import type { INodeUi } from '@/Interface'; -import type { ComputedRef, Ref } from 'vue'; +import type { Ref } from 'vue'; import type { PartialBy } from '@/utils/typeHelpers'; -export type CanvasConnectionPortType = ConnectionTypes; +export type CanvasConnectionPortType = NodeConnectionType; export const enum CanvasConnectionMode { Input = 'inputs', @@ -30,7 +29,8 @@ export type CanvasConnectionPort = { label?: string; }; -export interface CanvasElementPortWithPosition extends CanvasConnectionPort { +export interface CanvasElementPortWithRenderData extends CanvasConnectionPort { + connected: boolean; position: Position; offset?: { top?: string; left?: string }; } @@ -74,8 +74,8 @@ export interface CanvasNodeData { inputs: CanvasConnectionPort[]; outputs: CanvasConnectionPort[]; connections: { - input: INodeConnections; - output: INodeConnections; + [CanvasConnectionMode.Input]: INodeConnections; + [CanvasConnectionMode.Output]: INodeConnections; }; issues: { items: string[]; @@ -122,11 +122,13 @@ export interface CanvasNodeInjectionData { data: Ref; label: Ref; selected: Ref; - nodeType: ComputedRef; } export interface CanvasNodeHandleInjectionData { label: Ref; + mode: Ref; + type: Ref; + connected: Ref; } export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string }; diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts index 2b2a6089ee..79c7b998f9 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -148,7 +148,8 @@ export function mapLegacyEndpointsToCanvasConnectionPort( } return endpoints.map((endpoint, endpointIndex) => { - const type = typeof endpoint === 'string' ? endpoint : endpoint.type; + const typeValue = typeof endpoint === 'string' ? endpoint : endpoint.type; + const type = isValidNodeConnectionType(typeValue) ? typeValue : NodeConnectionType.Main; const label = typeof endpoint === 'string' ? undefined : endpoint.displayName; const index = endpoints diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 068e89c81d..5aa7069c1b 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -560,6 +560,16 @@ function onUpdateNodeParameters(id: string, parameters: Record) setNodeParameters(id, parameters); } +function onClickNodeAdd(source: string, sourceHandle: string) { + nodeCreatorStore.openNodeCreatorForConnectingNode({ + connection: { + source, + sourceHandle, + }, + eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, + }); +} + /** * Credentials */ @@ -1248,6 +1258,7 @@ onBeforeUnmount(() => { @update:node:enabled="onToggleNodeDisabled" @update:node:name="onOpenRenameNodeModal" @update:node:parameters="onUpdateNodeParameters" + @click:node:add="onClickNodeAdd" @run:node="onRunWorkflowToNode" @delete:node="onDeleteNode" @create:connection="onCreateConnection"