diff --git a/packages/editor-ui/src/__tests__/data/canvas.ts b/packages/editor-ui/src/__tests__/data/canvas.ts index 7ff598996c..75fc413547 100644 --- a/packages/editor-ui/src/__tests__/data/canvas.ts +++ b/packages/editor-ui/src/__tests__/data/canvas.ts @@ -6,16 +6,20 @@ export function createCanvasNodeData({ id = 'node', type = 'test', typeVersion = 1, + disabled = false, inputs = [], outputs = [], + connections = { input: {}, output: {} }, renderType = 'default', }: Partial = {}): CanvasElementData { return { id, type, typeVersion, + disabled, inputs, outputs, + connections, renderType, }; } diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts index 620d5aefe5..08ca10de8a 100644 --- a/packages/editor-ui/src/__tests__/mocks.ts +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -29,12 +29,14 @@ export const mockNode = ({ name, type, position = [0, 0], + disabled = false, }: { id?: INode['id']; name: INode['name']; type: INode['type']; position?: INode['position']; -}) => mock({ id, name, type, position }); + disabled?: INode['disabled']; +}) => mock({ id, name, type, position, disabled }); export const mockNodeTypeDescription = ({ name, diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 1b6568ee00..918cc8e669 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -15,6 +15,7 @@ const emit = defineEmits<{ 'update:modelValue': [elements: CanvasElement[]]; 'update:node:position': [id: string, position: XYPosition]; 'update:node:active': [id: string]; + 'update:node:enabled': [id: string]; 'update:node:selected': [id?: string]; 'delete:node': [id: string]; 'delete:connection': [connection: Connection]; @@ -66,6 +67,10 @@ function onSelectNode() { emit('update:node:selected', selectedNodeId); } +function onToggleNodeEnabled(id: string) { + emit('update:node:enabled', id); +} + function onDeleteNode(id: string) { emit('delete:node', id); } @@ -126,6 +131,7 @@ function onClickPane(event: MouseEvent) { v-bind="canvasNodeProps" @delete="onDeleteNode" @select="onSelectNode" + @toggle="onToggleNodeEnabled" @activate="onSetNodeActive" /> 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 28d98d01d2..2f8ca03a4a 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -18,21 +18,24 @@ import type { NodeProps } from '@vue-flow/core'; const emit = defineEmits<{ delete: [id: string]; select: [id: string, selected: boolean]; + toggle: [id: string]; activate: [id: string]; }>(); - const props = defineProps>(); -const inputs = computed(() => props.data.inputs); -const outputs = computed(() => props.data.outputs); - const nodeTypesStore = useNodeTypesStore(); +const inputs = computed(() => props.data.inputs); +const outputs = computed(() => props.data.outputs); +const connections = computed(() => props.data.connections); const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs } = useNodeConnections({ inputs, outputs, + connections, }); +const isDisabled = computed(() => props.data.disabled); + const nodeType = computed(() => { return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion); }); @@ -107,6 +110,10 @@ function onDelete() { emit('delete', props.id); } +function onDisabledToggle() { + emit('toggle', props.id); +} + function onActivate() { emit('activate', props.id); } @@ -143,12 +150,12 @@ function onActivate() { data-test-id="canvas-node-toolbar" :class="$style.canvasNodeToolbar" @delete="onDelete" + @toggle="onDisabledToggle" /> - + - diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.spec.ts index 2b6432df34..ef51189d63 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.spec.ts @@ -53,39 +53,39 @@ describe('CanvasNodeToolbar', () => { }); it('should call toggleDisableNode function when disable node button is clicked', async () => { - const toggleDisableNode = vi.fn(); + const onToggleNode = vi.fn(); const { getByTestId } = renderComponent({ global: { provide: { ...createCanvasNodeProvide(), }, mocks: { - toggleDisableNode, + onToggleNode, }, }, }); await fireEvent.click(getByTestId('disable-node-button')); - expect(toggleDisableNode).toHaveBeenCalled(); + expect(onToggleNode).toHaveBeenCalled(); }); it('should call deleteNode function when delete node button is clicked', async () => { - const deleteNode = vi.fn(); + const onDeleteNode = vi.fn(); const { getByTestId } = renderComponent({ global: { provide: { ...createCanvasNodeProvide(), }, mocks: { - deleteNode, + onDeleteNode, }, }, }); await fireEvent.click(getByTestId('delete-node-button')); - expect(deleteNode).toHaveBeenCalled(); + expect(onDeleteNode).toHaveBeenCalled(); }); it('should call openContextMenu function when overflow node button is clicked', async () => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue index d9c42f90b1..3743e1f0c4 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue @@ -3,12 +3,14 @@ import { computed, inject, useCssModule } from 'vue'; import { CanvasNodeKey } from '@/constants'; import { useI18n } from '@/composables/useI18n'; -const emit = defineEmits(['delete']); +const emit = defineEmits<{ + delete: []; + toggle: []; +}>(); + const $style = useCssModule(); - -const node = inject(CanvasNodeKey); - const i18n = useI18n(); +const node = inject(CanvasNodeKey); const data = computed(() => node?.data.value); @@ -21,10 +23,11 @@ const nodeDisabledTitle = 'Test'; // @TODO function executeNode() {} -// @TODO -function toggleDisableNode() {} +function onToggleNode() { + emit('toggle'); +} -function deleteNode() { +function onDeleteNode() { emit('delete'); } @@ -53,7 +56,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {} size="small" icon="power-off" :title="nodeDisabledTitle" - @click="toggleDisableNode" + @click="onToggleNode" /> { }); }); + describe('disabled', () => { + it('should apply disabled class when node is disabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + data: { + disabled: true, + }, + }), + }, + }, + }); + + expect(getByText('Test Node').closest('.node')).toHaveClass('disabled'); + expect(getByText('(Deactivated)')).toBeVisible(); + }); + + it('should not apply disabled class when node is enabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide(), + }, + }, + }); + expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled'); + }); + }); + describe('inputs', () => { it('should adjust width css variable based on the number of non-main inputs', () => { const { getByText } = renderComponent({ diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue index fb4554abb1..376b4c986e 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue @@ -2,24 +2,32 @@ import { computed, inject, useCssModule } from 'vue'; import { CanvasNodeKey, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants'; import { useNodeConnections } from '@/composables/useNodeConnections'; +import { useI18n } from '@/composables/useI18n'; +import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue'; const node = inject(CanvasNodeKey); const $style = useCssModule(); +const i18n = useI18n(); const label = computed(() => node?.label.value ?? ''); + const inputs = computed(() => node?.data.value.inputs ?? []); const outputs = computed(() => node?.data.value.outputs ?? []); - +const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} }); const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({ inputs, outputs, + connections, }); +const isDisabled = computed(() => node?.data.value.disabled ?? false); + const classes = computed(() => { return { [$style.node]: true, [$style.selected]: node?.selected.value, + [$style.disabled]: isDisabled.value, }; }); @@ -46,7 +54,11 @@ const styles = computed(() => { @@ -54,12 +66,14 @@ const styles = computed(() => { .node { --configurable-node-min-input-count: 4; --configurable-node-input-width: 65px; - - width: calc( + --canvas-node--height: 100px; + --canvas-node--width: calc( max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) * var(--configurable-node-input-width) ); - height: 100px; + + width: var(--canvas-node--width); + height: var(--canvas-node--height); display: flex; align-items: center; justify-content: center; @@ -82,4 +96,8 @@ const styles = computed(() => { .selected { box-shadow: 0 0 0 4px var(--color-canvas-selected); } + +.disabled { + border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base)); +} diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.spec.ts index 378c87b96b..8770ef9ac6 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.spec.ts @@ -40,4 +40,34 @@ describe('CanvasNodeConfiguration', () => { expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected'); }); }); + + describe('disabled', () => { + it('should apply disabled class when node is disabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + data: { + disabled: true, + }, + }), + }, + }, + }); + + expect(getByText('Test Node').closest('.node')).toHaveClass('disabled'); + expect(getByText('(Deactivated)')).toBeVisible(); + }); + + it('should not apply disabled class when node is enabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide(), + }, + }, + }); + expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled'); + }); + }); }); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue index f98d4ff747..3f6f401fa2 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue @@ -1,17 +1,22 @@ @@ -19,14 +24,20 @@ const classes = computed(() => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts index 37e2415b5b..643128b22b 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts @@ -81,4 +81,34 @@ describe('CanvasNodeDefault', () => { expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected'); }); }); + + describe('disabled', () => { + it('should apply disabled class when node is disabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + data: { + disabled: true, + }, + }), + }, + }, + }); + + expect(getByText('Test Node').closest('.node')).toHaveClass('disabled'); + expect(getByText('(Deactivated)')).toBeVisible(); + }); + + it('should not apply disabled class when node is enabled', () => { + const { getByText } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide(), + }, + }, + }); + expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled'); + }); + }); }); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 7a301a8ce9..4b39771355 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -2,24 +2,32 @@ import { computed, inject, useCssModule } from 'vue'; import { useNodeConnections } from '@/composables/useNodeConnections'; import { CanvasNodeKey } from '@/constants'; +import { useI18n } from '@/composables/useI18n'; +import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue'; const node = inject(CanvasNodeKey); const $style = useCssModule(); +const i18n = useI18n(); const label = computed(() => node?.label.value ?? ''); + const inputs = computed(() => node?.data.value.inputs ?? []); const outputs = computed(() => node?.data.value.outputs ?? []); - +const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} }); const { mainOutputs } = useNodeConnections({ inputs, outputs, + connections, }); +const isDisabled = computed(() => node?.data.value.disabled ?? false); + const classes = computed(() => { return { [$style.node]: true, [$style.selected]: node?.selected.value, + [$style.disabled]: isDisabled.value, }; }); @@ -33,14 +41,21 @@ const styles = computed(() => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts new file mode 100644 index 0000000000..d7d58c9693 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts @@ -0,0 +1,35 @@ +import CanvasNodeDisabledStrikeThrough from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue'; +import { createComponentRenderer } from '@/__tests__/render'; +import { NodeConnectionType } from 'n8n-workflow'; +import { createCanvasNodeProvide } from '@/__tests__/data'; + +const renderComponent = createComponentRenderer(CanvasNodeDisabledStrikeThrough); + +describe('CanvasNodeDisabledStrikeThrough', () => { + it('should render node correctly', () => { + const { container } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + data: { + connections: { + input: { + [NodeConnectionType.Main]: [ + [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], + ], + }, + output: { + [NodeConnectionType.Main]: [ + [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }, + }, + }), + }, + }, + }); + + expect(container.firstChild).toBeVisible(); + }); +}); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue new file mode 100644 index 0000000000..75be6871a3 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/editor-ui/src/composables/__tests__/useCanvasMapping.spec.ts b/packages/editor-ui/src/composables/__tests__/useCanvasMapping.spec.ts index e1bbf0755b..e01eadb707 100644 --- a/packages/editor-ui/src/composables/__tests__/useCanvasMapping.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useCanvasMapping.spec.ts @@ -7,7 +7,8 @@ import { mock } from 'vitest-mock-extended'; import { useCanvasMapping } from '@/composables/useCanvasMapping'; import type { IWorkflowDb } from '@/Interface'; -import { createTestWorkflowObject, mockNodes } from '@/__tests__/mocks'; +import { createTestWorkflowObject, mockNode, mockNodes } from '@/__tests__/mocks'; +import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants'; vi.mock('@/stores/nodeTypes.store', () => ({ useNodeTypesStore: vi.fn(() => ({ @@ -48,7 +49,11 @@ describe('useCanvasMapping', () => { describe('elements', () => { it('should map nodes to canvas elements', () => { - const manualTriggerNode = mockNodes[0]; + const manualTriggerNode = mockNode({ + name: 'Manual Trigger', + type: MANUAL_TRIGGER_NODE_TYPE, + disabled: false, + }); const workflow = mock({ nodes: [manualTriggerNode], }); @@ -69,13 +74,75 @@ describe('useCanvasMapping', () => { id: manualTriggerNode.id, type: manualTriggerNode.type, typeVersion: expect.anything(), + disabled: false, inputs: [], outputs: [], + connections: { + input: {}, + output: {}, + }, renderType: 'default', }, }, ]); }); + + it('should handle node disabled state', () => { + const manualTriggerNode = mockNode({ + name: 'Manual Trigger', + type: MANUAL_TRIGGER_NODE_TYPE, + disabled: true, + }); + const workflow = mock({ + nodes: [manualTriggerNode], + }); + const workflowObject = createTestWorkflowObject(workflow); + + const { elements } = useCanvasMapping({ + workflow: ref(workflow), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(elements.value[0]?.data?.disabled).toEqual(true); + }); + + it('should handle input and output connections', () => { + const [manualTriggerNode, setNode] = mockNodes.slice(0, 2); + const workflow = mock({ + nodes: [manualTriggerNode, setNode], + connections: { + [manualTriggerNode.name]: { + [NodeConnectionType.Main]: [ + [{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }], + ], + }, + }, + }); + const workflowObject = createTestWorkflowObject(workflow); + + const { elements } = useCanvasMapping({ + workflow: ref(workflow), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(elements.value[0]?.data?.connections.output).toHaveProperty(NodeConnectionType.Main); + expect(elements.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual( + expect.objectContaining({ + node: setNode.name, + type: NodeConnectionType.Main, + index: 0, + }), + ); + + expect(elements.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main); + expect(elements.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual( + expect.objectContaining({ + node: manualTriggerNode.name, + type: NodeConnectionType.Main, + index: 0, + }), + ); + }); }); describe('connections', () => { diff --git a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts index 5201df4b73..0da4ee3537 100644 --- a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts @@ -4,6 +4,7 @@ import { useNodeConnections } from '@/composables/useNodeConnections'; import type { CanvasElementData } from '@/types'; describe('useNodeConnections', () => { + const defaultConnections = { input: {}, output: {} }; describe('mainInputs', () => { it('should return main inputs when provided with main inputs', () => { const inputs = ref([ @@ -14,7 +15,11 @@ describe('useNodeConnections', () => { ]); const outputs = ref([]); - const { mainInputs } = useNodeConnections({ inputs, outputs }); + const { mainInputs } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); expect(mainInputs.value.length).toBe(3); expect(mainInputs.value).toEqual(inputs.value.slice(0, 3)); @@ -30,7 +35,11 @@ describe('useNodeConnections', () => { ]); const outputs = ref([]); - const { nonMainInputs } = useNodeConnections({ inputs, outputs }); + const { nonMainInputs } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); expect(nonMainInputs.value.length).toBe(2); expect(nonMainInputs.value).toEqual(inputs.value.slice(1)); @@ -46,13 +55,42 @@ describe('useNodeConnections', () => { ]); const outputs = ref([]); - const { requiredNonMainInputs } = useNodeConnections({ inputs, outputs }); + const { requiredNonMainInputs } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); expect(requiredNonMainInputs.value.length).toBe(1); expect(requiredNonMainInputs.value).toEqual([inputs.value[1]]); }); }); + describe('mainInputConnections', () => { + it('should return main input connections when provided with main input connections', () => { + const inputs = ref([]); + const outputs = ref([]); + const connections = ref({ + input: { + [NodeConnectionType.Main]: [ + [{ node: 'node1', type: NodeConnectionType.Main, index: 0 }], + [{ node: 'node2', type: NodeConnectionType.Main, index: 0 }], + ], + }, + output: {}, + }); + + const { mainInputConnections } = useNodeConnections({ + inputs, + outputs, + connections, + }); + + expect(mainInputConnections.value.length).toBe(2); + expect(mainInputConnections.value).toEqual(connections.value.input[NodeConnectionType.Main]); + }); + }); + describe('mainOutputs', () => { it('should return main outputs when provided with main outputs', () => { const inputs = ref([]); @@ -63,7 +101,11 @@ describe('useNodeConnections', () => { { type: NodeConnectionType.AiAgent, index: 0 }, ]); - const { mainOutputs } = useNodeConnections({ inputs, outputs }); + const { mainOutputs } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); expect(mainOutputs.value.length).toBe(3); expect(mainOutputs.value).toEqual(outputs.value.slice(0, 3)); @@ -79,10 +121,41 @@ describe('useNodeConnections', () => { { type: NodeConnectionType.AiAgent, index: 1 }, ]); - const { nonMainOutputs } = useNodeConnections({ inputs, outputs }); + const { nonMainOutputs } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); expect(nonMainOutputs.value.length).toBe(2); expect(nonMainOutputs.value).toEqual(outputs.value.slice(1)); }); }); + + describe('mainOutputConnections', () => { + it('should return main output connections when provided with main output connections', () => { + const inputs = ref([]); + const outputs = ref([]); + const connections = ref({ + input: {}, + output: { + [NodeConnectionType.Main]: [ + [{ node: 'node1', type: NodeConnectionType.Main, index: 0 }], + [{ node: 'node2', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }); + + const { mainOutputConnections } = useNodeConnections({ + inputs, + outputs, + connections, + }); + + expect(mainOutputConnections.value.length).toBe(2); + expect(mainOutputConnections.value).toEqual( + connections.value.output[NodeConnectionType.Main], + ); + }); + }); }); diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts index 41e6e15a61..034eac1609 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.ts @@ -89,12 +89,20 @@ export function useCanvasMapping({ const elements = computed(() => [ ...workflow.value.nodes.map((node) => { + const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {}; + const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {}; + const data: CanvasElementData = { id: node.id, type: node.type, typeVersion: node.typeVersion, + disabled: !!node.disabled, inputs: nodeInputsById.value[node.id] ?? [], outputs: nodeOutputsById.value[node.id] ?? [], + connections: { + input: inputConnections, + output: outputConnections, + }, renderType: renderTypeByNodeType.value[node.type] ?? 'default', }; diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index 8af85df8e5..dec5f19476 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -46,6 +46,7 @@ import { useCredentialsStore } from '@/stores/credentials.store'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import type { useRouter } from 'vue-router'; import { useCanvasStore } from '@/stores/canvas.store'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; type AddNodeData = { name?: string; @@ -78,6 +79,7 @@ export function useCanvasOperations({ const i18n = useI18n(); const toast = useToast(); const workflowHelpers = useWorkflowHelpers({ router }); + const nodeHelpers = useNodeHelpers(); const telemetry = useTelemetry(); const externalHooks = useExternalHooks(); @@ -235,6 +237,18 @@ export function useCanvasOperations({ uiStore.lastSelectedNode = node.name; } + function toggleNodeDisabled( + id: string, + { trackHistory = true }: { trackHistory?: boolean } = {}, + ) { + const node = workflowsStore.getNodeById(id); + if (!node) { + return; + } + + nodeHelpers.disableNodes([node], trackHistory); + } + async function addNodes( nodes: AddedNodesAndConnections['nodes'], { @@ -886,6 +900,7 @@ export function useCanvasOperations({ setNodeActive, setNodeActiveByName, setNodeSelected, + toggleNodeDisabled, renameNode, revertRenameNode, deleteNode, diff --git a/packages/editor-ui/src/composables/useNodeConnections.ts b/packages/editor-ui/src/composables/useNodeConnections.ts index 9b581a4043..80a2f51ee7 100644 --- a/packages/editor-ui/src/composables/useNodeConnections.ts +++ b/packages/editor-ui/src/composables/useNodeConnections.ts @@ -6,9 +6,11 @@ import { NodeConnectionType } from 'n8n-workflow'; export function useNodeConnections({ inputs, outputs, + connections, }: { inputs: MaybeRef; outputs: MaybeRef; + connections: MaybeRef; }) { /** * Inputs @@ -26,6 +28,10 @@ export function useNodeConnections({ nonMainInputs.value.filter((input) => input.required), ); + const mainInputConnections = computed( + () => unref(connections).input[NodeConnectionType.Main] ?? [], + ); + /** * Outputs */ @@ -33,15 +39,22 @@ export function useNodeConnections({ const mainOutputs = computed(() => unref(outputs).filter((output) => output.type === NodeConnectionType.Main), ); + const nonMainOutputs = computed(() => unref(outputs).filter((output) => output.type !== NodeConnectionType.Main), ); + const mainOutputConnections = computed( + () => unref(connections).output[NodeConnectionType.Main] ?? [], + ); + return { mainInputs, nonMainInputs, requiredNonMainInputs, + mainInputConnections, mainOutputs, nonMainOutputs, + mainOutputConnections, }; } diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts index 1e11dbc54d..11a15e7b7a 100644 --- a/packages/editor-ui/src/types/canvas.ts +++ b/packages/editor-ui/src/types/canvas.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */ -import type { ConnectionTypes, INodeTypeDescription } from 'n8n-workflow'; +import type { ConnectionTypes, INodeConnections, INodeTypeDescription } from 'n8n-workflow'; import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core'; import type { INodeUi } from '@/Interface'; @@ -25,8 +25,13 @@ export interface CanvasElementData { id: INodeUi['id']; type: INodeUi['type']; typeVersion: INodeUi['typeVersion']; + disabled: INodeUi['disabled']; inputs: CanvasConnectionPort[]; outputs: CanvasConnectionPort[]; + connections: { + input: INodeConnections; + output: INodeConnections; + }; renderType: 'default' | 'trigger' | 'configuration' | 'configurable'; } diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 726529bde5..a044828451 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -95,6 +95,7 @@ const { revertRenameNode, setNodeActive, setNodeSelected, + toggleNodeDisabled, deleteNode, revertDeleteNode, addNodes, @@ -363,6 +364,14 @@ function onRevertDeleteNode({ node }: { node: INodeUi }) { revertDeleteNode(node); } +function onToggleNodeDisabled(id: string) { + if (!checkIfEditingIsAllowed()) { + return; + } + + toggleNodeDisabled(id); +} + function onSetNodeActive(id: string) { setNodeActive(id); } @@ -680,6 +689,7 @@ onBeforeUnmount(() => { @update:node:position="onUpdateNodePosition" @update:node:active="onSetNodeActive" @update:node:selected="onSetNodeSelected" + @update:node:enabled="onToggleNodeDisabled" @delete:node="onDeleteNode" @create:connection="onCreateConnection" @delete:connection="onDeleteConnection"