diff --git a/packages/editor-ui/src/components/WorkflowActivator.test.ts b/packages/editor-ui/src/components/WorkflowActivator.test.ts index 506f40b787..46bc6a7190 100644 --- a/packages/editor-ui/src/components/WorkflowActivator.test.ts +++ b/packages/editor-ui/src/components/WorkflowActivator.test.ts @@ -95,17 +95,17 @@ describe('WorkflowActivator', () => { expect(getByTestId('workflow-activator-status')).toHaveTextContent('Inactive'); }); - it('Should show warning toast if the workflow to be activated has free OpenAI credentials', async () => { + it('Should show warning toast if the workflow to be activated has non-disabled node using free OpenAI credentials', async () => { const toast = useToast(); - mockWorkflowsStore.workflow.usedCredentials = [ - { + mockWorkflowsStore.usedCredentials = { + '1': { id: '1', name: '', credentialType: '', currentUserHasAccess: false, }, - ]; + }; mockCredentialsStore.state.credentials = { '1': { @@ -123,6 +123,24 @@ describe('WorkflowActivator', () => { { type: WOOCOMMERCE_TRIGGER_NODE_TYPE, disabled: false } as never, ]; + mockWorkflowsStore.allNodes = [ + { + credentials: { + openAiApi: { + name: 'OpenAI', + id: '1', + }, + }, + disabled: false, + position: [1, 1], + name: '', + id: '', + typeVersion: 0, + type: '', + parameters: {}, + }, + ]; + const { rerender } = renderComponent({ props: { workflowActive: false, @@ -143,4 +161,104 @@ describe('WorkflowActivator', () => { }), ); }); + + it('Should not show warning toast if the workflow to be activated has disabled node using free OpenAI credentials', async () => { + const toast = useToast(); + + mockWorkflowsStore.usedCredentials = { + '1': { + id: '1', + name: '', + credentialType: '', + currentUserHasAccess: false, + }, + }; + + mockCredentialsStore.state.credentials = { + '1': { + id: '1', + name: 'OpenAI', + type: 'openAiApi', + data: '', + isManaged: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }; + + mockWorkflowsStore.workflowTriggerNodes = [ + { type: WOOCOMMERCE_TRIGGER_NODE_TYPE, disabled: false } as never, + ]; + + mockWorkflowsStore.allNodes = [ + { + credentials: { + openAiApi: { + name: 'OpenAI', + id: '1', + }, + }, + disabled: true, + position: [1, 1], + name: '', + id: '', + typeVersion: 0, + type: '', + parameters: {}, + }, + ]; + + const { rerender } = renderComponent({ + props: { + workflowActive: false, + workflowId: '1', + workflowPermissions: { update: true }, + }, + }); + + await rerender({ workflowActive: true }); + + expect(toast.showMessage).not.toHaveBeenCalled(); + }); + + it('Should not show warning toast if the workflow to be activated has no node with free OpenAI credential', async () => { + const toast = useToast(); + + mockWorkflowsStore.usedCredentials = { + '1': { + id: '1', + name: '', + credentialType: '', + currentUserHasAccess: false, + }, + }; + + mockCredentialsStore.state.credentials = { + '1': { + id: '1', + name: 'Jira', + type: 'jiraApi', + data: '', + isManaged: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }; + + mockWorkflowsStore.workflowTriggerNodes = [ + { type: WOOCOMMERCE_TRIGGER_NODE_TYPE, disabled: false } as never, + ]; + + const { rerender } = renderComponent({ + props: { + workflowActive: false, + workflowId: '1', + workflowPermissions: { update: true }, + }, + }); + + await rerender({ workflowActive: true }); + + expect(toast.showMessage).not.toHaveBeenCalled(); + }); }); diff --git a/packages/editor-ui/src/components/WorkflowActivator.vue b/packages/editor-ui/src/components/WorkflowActivator.vue index dc1c123e94..888f473cc0 100644 --- a/packages/editor-ui/src/components/WorkflowActivator.vue +++ b/packages/editor-ui/src/components/WorkflowActivator.vue @@ -10,7 +10,8 @@ import type { PermissionsRecord } from '@/permissions'; import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import WorkflowActivationErrorMessage from './WorkflowActivationErrorMessage.vue'; import { useCredentialsStore } from '@/stores/credentials.store'; -import type { IUsedCredential } from '@/Interface'; +import type { INodeUi, IUsedCredential } from '@/Interface'; +import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow'; const props = defineProps<{ workflowActive: boolean; @@ -72,12 +73,39 @@ const disabled = computed((): boolean => { return false; }); -const currentWorkflowHasFreeAiCredits = computed((): boolean => { - if (!workflowsStore?.workflow?.usedCredentials) return false; - return workflowsStore.workflow.usedCredentials.some( - (usedCredential: IUsedCredential) => - credentialsStore.state.credentials[usedCredential.id].isManaged, +function findManagedOpenAiCredentialId( + usedCredentials: Record, +): string | undefined { + return Object.keys(usedCredentials).find((credentialId) => { + const credential = credentialsStore.state.credentials[credentialId]; + return credential.isManaged && credential.type === OPEN_AI_API_CREDENTIAL_TYPE; + }); +} + +function hasActiveNodeUsingCredential(nodes: INodeUi[], credentialId: string): boolean { + return nodes.some( + (node) => + node?.credentials?.[OPEN_AI_API_CREDENTIAL_TYPE]?.id === credentialId && !node.disabled, ); +} + +/** + * Determines if the warning for free AI credits should be shown in the workflow. + * + * This computed property evaluates whether to display a warning about free AI credits + * in the workflow. The warning is shown when both conditions are met: + * 1. The workflow uses managed OpenAI API credentials + * 2. Those credentials are associated with at least one enabled node + * + */ +const shouldShowFreeAiCreditsWarning = computed((): boolean => { + const usedCredentials = workflowsStore?.usedCredentials; + if (!usedCredentials) return false; + + const managedOpenAiCredentialId = findManagedOpenAiCredentialId(usedCredentials); + if (!managedOpenAiCredentialId) return false; + + return hasActiveNodeUsingCredential(workflowsStore.allNodes, managedOpenAiCredentialId); }); async function activeChanged(newActiveState: boolean) { @@ -115,7 +143,7 @@ async function displayActivationError() { watch( () => props.workflowActive, (workflowActive) => { - if (workflowActive && currentWorkflowHasFreeAiCredits.value) { + if (workflowActive && shouldShowFreeAiCreditsWarning.value) { showMessage({ title: i18n.baseText('freeAi.credits.showWarning.workflow.activation.title'), message: i18n.baseText('freeAi.credits.showWarning.workflow.activation.description'),