feat(editor): Add node enable/disable functionality in new canvas (no-changelog) (#9872)

This commit is contained in:
Alex Grozav
2024-06-26 16:56:58 +03:00
committed by GitHub
parent c39c087c20
commit e995309789
21 changed files with 489 additions and 46 deletions

View File

@@ -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<IWorkflowDb>({
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<IWorkflowDb>({
nodes: [manualTriggerNode],
});
const workflowObject = createTestWorkflowObject(workflow);
const { elements } = useCanvasMapping({
workflow: ref(workflow),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
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<IWorkflowDb>({
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<Workflow>,
});
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', () => {

View File

@@ -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<CanvasElementData['inputs']>([
@@ -14,7 +15,11 @@ describe('useNodeConnections', () => {
]);
const outputs = ref<CanvasElementData['outputs']>([]);
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<CanvasElementData['outputs']>([]);
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<CanvasElementData['outputs']>([]);
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<CanvasElementData['inputs']>([]);
const outputs = ref<CanvasElementData['outputs']>([]);
const connections = ref<CanvasElementData['connections']>({
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<CanvasElementData['inputs']>([]);
@@ -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<CanvasElementData['inputs']>([]);
const outputs = ref<CanvasElementData['outputs']>([]);
const connections = ref<CanvasElementData['connections']>({
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],
);
});
});
});

View File

@@ -89,12 +89,20 @@ export function useCanvasMapping({
const elements = computed<CanvasElement[]>(() => [
...workflow.value.nodes.map<CanvasElement>((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',
};

View File

@@ -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,

View File

@@ -6,9 +6,11 @@ import { NodeConnectionType } from 'n8n-workflow';
export function useNodeConnections({
inputs,
outputs,
connections,
}: {
inputs: MaybeRef<CanvasElementData['inputs']>;
outputs: MaybeRef<CanvasElementData['outputs']>;
connections: MaybeRef<CanvasElementData['connections']>;
}) {
/**
* 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,
};
}