feat(editor): Add node enable/disable functionality in new canvas (no-changelog) (#9872)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user