mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Deactivate workflow on save if trigger is missing (#15642)
This commit is contained in:
@@ -2572,6 +2572,7 @@
|
||||
"workflows.create.project.toast.title": "Workflow successfully created in {projectName}",
|
||||
"workflows.create.folder.toast.title": "Workflow successfully created in \"{projectName}\", within \"{folderName}\"",
|
||||
"workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.",
|
||||
"workflows.deactivated": "Workflow deactivated",
|
||||
"workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow",
|
||||
"importCurlModal.title": "Import cURL command",
|
||||
"importCurlModal.input.label": "cURL Command",
|
||||
|
||||
@@ -12,10 +12,13 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
|
||||
import * as apiWebhooks from '../api/webhooks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { nodeTypes } from '@/components/CanvasChat/__test__/data';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
|
||||
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||
name: 'Duplicate webhook test',
|
||||
@@ -68,6 +71,7 @@ describe('useWorkflowHelpers', () => {
|
||||
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
|
||||
let tagsStore: ReturnType<typeof useTagsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
@@ -460,6 +464,7 @@ describe('useWorkflowHelpers', () => {
|
||||
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeData', () => {
|
||||
it('should return empty execute data if no parent nodes', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
@@ -830,4 +835,58 @@ describe('useWorkflowHelpers', () => {
|
||||
expect(result.source).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveCurrentWorkflow', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.setNodeTypes(nodeTypes);
|
||||
});
|
||||
|
||||
it('should save the current workflow', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w0',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
await saveCurrentWorkflow({ id: 'w0' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w0',
|
||||
expect.objectContaining({ id: 'w0', active: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should include active=false in the request if the workflow has no activatable trigger node', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w1',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
await saveCurrentWorkflow({ id: 'w1' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w1',
|
||||
expect.objectContaining({ id: 'w1', active: false }),
|
||||
false,
|
||||
);
|
||||
expect(workflowsStore.setWorkflowInactive).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
MODAL_CONFIRM,
|
||||
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
VIEWS,
|
||||
@@ -36,6 +37,7 @@ import type {
|
||||
IWorkflowDataCreate,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
NotificationOptions,
|
||||
TargetItem,
|
||||
WorkflowTitleStatus,
|
||||
XYPosition,
|
||||
@@ -812,6 +814,50 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
}
|
||||
}
|
||||
|
||||
function isNodeActivatable(node: INode): boolean {
|
||||
if (node.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
|
||||
return (
|
||||
nodeType !== null &&
|
||||
nodeType.group.includes('trigger') &&
|
||||
!NON_ACTIVATABLE_TRIGGER_NODE_TYPES.includes(node.type)
|
||||
);
|
||||
}
|
||||
|
||||
async function getWorkflowDeactivationInfo(
|
||||
workflowId: string,
|
||||
request: IWorkflowDataUpdate,
|
||||
): Promise<Partial<NotificationOptions> | undefined> {
|
||||
const missingActivatableTriggerNode =
|
||||
request.nodes !== undefined && !request.nodes.some(isNodeActivatable);
|
||||
|
||||
if (missingActivatableTriggerNode) {
|
||||
// Automatically deactivate if all activatable triggers are removed
|
||||
return {
|
||||
title: i18n.baseText('workflows.deactivated'),
|
||||
message: i18n.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes'),
|
||||
type: 'info',
|
||||
};
|
||||
}
|
||||
|
||||
const conflictData = await checkConflictingWebhooks(workflowId);
|
||||
|
||||
if (conflictData) {
|
||||
// Workflow should not be active if there is live webhook with the same path
|
||||
return {
|
||||
title: 'Conflicting Webhook Path',
|
||||
message: `Workflow set to inactive: Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
|
||||
type: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function saveCurrentWorkflow(
|
||||
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
|
||||
redirect = true,
|
||||
@@ -855,17 +901,16 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
|
||||
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
|
||||
|
||||
// workflow should not be active if there is live webhook with the same path
|
||||
const conflictData = await checkConflictingWebhooks(currentWorkflow);
|
||||
if (conflictData) {
|
||||
const deactivateReason = await getWorkflowDeactivationInfo(
|
||||
currentWorkflow,
|
||||
workflowDataRequest,
|
||||
);
|
||||
|
||||
if (deactivateReason !== undefined) {
|
||||
workflowDataRequest.active = false;
|
||||
|
||||
if (workflowsStore.isWorkflowActive) {
|
||||
toast.showMessage({
|
||||
title: 'Conflicting Webhook Path',
|
||||
message: `Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
|
||||
type: 'error',
|
||||
});
|
||||
toast.showMessage(deactivateReason);
|
||||
|
||||
workflowsStore.setWorkflowInactive(currentWorkflow);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user