mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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.project.toast.title": "Workflow successfully created in {projectName}",
|
||||||
"workflows.create.folder.toast.title": "Workflow successfully created in \"{projectName}\", within \"{folderName}\"",
|
"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.create.project.toast.text": "All members from {projectName} will have access to this workflow.",
|
||||||
|
"workflows.deactivated": "Workflow deactivated",
|
||||||
"workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow",
|
"workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow",
|
||||||
"importCurlModal.title": "Import cURL command",
|
"importCurlModal.title": "Import cURL command",
|
||||||
"importCurlModal.input.label": "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 { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { useUIStore } from '@/stores/ui.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 { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
|
||||||
import * as apiWebhooks from '../api/webhooks';
|
import * as apiWebhooks from '../api/webhooks';
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
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 => ({
|
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||||
name: 'Duplicate webhook test',
|
name: 'Duplicate webhook test',
|
||||||
@@ -68,6 +71,7 @@ describe('useWorkflowHelpers', () => {
|
|||||||
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
|
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
|
||||||
let tagsStore: ReturnType<typeof useTagsStore>;
|
let tagsStore: ReturnType<typeof useTagsStore>;
|
||||||
let uiStore: ReturnType<typeof useUIStore>;
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
|
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setActivePinia(createTestingPinia());
|
setActivePinia(createTestingPinia());
|
||||||
@@ -460,6 +464,7 @@ describe('useWorkflowHelpers', () => {
|
|||||||
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
|
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('executeData', () => {
|
describe('executeData', () => {
|
||||||
it('should return empty execute data if no parent nodes', () => {
|
it('should return empty execute data if no parent nodes', () => {
|
||||||
const { executeData } = useWorkflowHelpers({ router });
|
const { executeData } = useWorkflowHelpers({ router });
|
||||||
@@ -830,4 +835,58 @@ describe('useWorkflowHelpers', () => {
|
|||||||
expect(result.source).toBeNull();
|
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 {
|
import {
|
||||||
HTTP_REQUEST_NODE_TYPE,
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
|
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
@@ -36,6 +37,7 @@ import type {
|
|||||||
IWorkflowDataCreate,
|
IWorkflowDataCreate,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
|
NotificationOptions,
|
||||||
TargetItem,
|
TargetItem,
|
||||||
WorkflowTitleStatus,
|
WorkflowTitleStatus,
|
||||||
XYPosition,
|
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(
|
async function saveCurrentWorkflow(
|
||||||
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
|
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
|
||||||
redirect = true,
|
redirect = true,
|
||||||
@@ -855,17 +901,16 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||||||
|
|
||||||
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
|
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
|
||||||
|
|
||||||
// workflow should not be active if there is live webhook with the same path
|
const deactivateReason = await getWorkflowDeactivationInfo(
|
||||||
const conflictData = await checkConflictingWebhooks(currentWorkflow);
|
currentWorkflow,
|
||||||
if (conflictData) {
|
workflowDataRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deactivateReason !== undefined) {
|
||||||
workflowDataRequest.active = false;
|
workflowDataRequest.active = false;
|
||||||
|
|
||||||
if (workflowsStore.isWorkflowActive) {
|
if (workflowsStore.isWorkflowActive) {
|
||||||
toast.showMessage({
|
toast.showMessage(deactivateReason);
|
||||||
title: 'Conflicting Webhook Path',
|
|
||||||
message: `Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
|
|
||||||
workflowsStore.setWorkflowInactive(currentWorkflow);
|
workflowsStore.setWorkflowInactive(currentWorkflow);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user