diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 91aad511bae..71dbdb462aa 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1141,13 +1141,14 @@ "credentials.item.needsSetup": "Needs first setup", "credentials.item.connect": "Connect", "credentials.item.connect.tooltip": "Connect your own account to use this credential in workflows. Only you can use your private credential.", - "credentials.item.connected": "Connected", "credentials.private.tooltip": "This credential uses a resolver to pick the right account at runtime based on who runs the workflow.", "credentials.private.tooltipTitle": "Private credentials", "credentials.private.badge": "Private", "credentials.private.callout.title": "This node is set up to use end user accounts", "credentials.private.callout.connected": "Your account is connected", - "credentials.private.callout.notConnected": "Your account isn't connected yet.", + "credentials.private.callout.connectTitle": "Connect your account", + "credentials.private.callout.connectDescription": "This workflow runs as you. Connect your {service} account to continue.", + "credentials.private.callout.connectDescriptionGeneric": "This workflow runs as you. Connect your account to continue.", "credentials.private.callout.connect": "Connect", "credentials.search.placeholder": "Search credentials...", "credentials.filters.type": "Type", @@ -3723,7 +3724,6 @@ "timeAgo.weeksAgo": "%s weeks ago", "timeAgo.yearsAgo": "%s years ago", "nodeIssues.credentials.notSet": "Credentials for {type} are not set.", - "nodeIssues.credentials.privateNotConnected": "'{name}' private credential is not connected for you. Connect yours to execute this step manually.", "nodeIssues.credentials.notAvailable": "Credential is not available", "nodeIssues.credentials.doNotExist": "Credentials with name {name} do not exist for {type}.", "nodeIssues.credentials.doNotExist.hint": "You can create credentials with the exact name and then they get auto-selected on refresh..", diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts index 801bca656ad..ed74893aeaf 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts @@ -807,6 +807,7 @@ describe('useNodeHelpers()', () => { const NOTION_API = 'notionApi'; const MANUAL_TRIGGER = 'n8n-nodes-base.manualTrigger'; const MANUAL_CHAT_TRIGGER = '@n8n/n8n-nodes-langchain.manualChatTrigger'; + const CHAT_TRIGGER = '@n8n/n8n-nodes-langchain.chatTrigger'; const WEBHOOK_TRIGGER = 'n8n-nodes-base.webhook'; const notionNodeType: INodeTypeDescription = { @@ -863,7 +864,7 @@ describe('useNodeHelpers()', () => { }); describe('not connected', () => { - it('emits privateNotConnected when declared-credential node has private cred not connected', () => { + it('emits no issue when declared-credential node has private cred not connected', () => { const cred = makePrivateCred({ connectedByMe: false }); mockedStore(useCredentialsStore).getCredentialById = vi.fn().mockReturnValue(cred); mockedStore(useCredentialsStore).getCredentialsByType = vi.fn().mockReturnValue([cred]); @@ -873,8 +874,9 @@ describe('useNodeHelpers()', () => { 'parameters', ]); - expect(result?.credentials?.[NOTION_API]).toBeDefined(); - expect(result?.credentials?.[NOTION_API][0]).toContain('My Notion'); + // An unconnected private credential is surfaced as a warning in the UI, + // not as a node issue. + expect(result?.credentials).toBeUndefined(); }); it('emits no issue when declared-credential node has private cred connected', () => { @@ -890,7 +892,7 @@ describe('useNodeHelpers()', () => { expect(result?.credentials).toBeUndefined(); }); - it('emits privateNotConnected for predefined-OAuth credential (HTTP Request, not in node type credentials array)', () => { + it('emits no issue for predefined-OAuth private credential not connected (HTTP Request, not in node type credentials array)', () => { mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue(httpRequestNodeType); const cred = makePrivateCred({ type: 'slackOAuth2Api', connectedByMe: false }); @@ -908,8 +910,7 @@ describe('useNodeHelpers()', () => { const { getNodeIssues } = useNodeHelpers(); const result = getNodeIssues(httpRequestNodeType, node, mock(), ['parameters']); - expect(result?.credentials?.slackOAuth2Api).toBeDefined(); - expect(result?.credentials?.slackOAuth2Api[0]).toContain('My Notion'); + expect(result?.credentials?.slackOAuth2Api).toBeUndefined(); }); it('emits no issue for static (non-resolvable) credential regardless of connectedByMe', () => { @@ -1006,6 +1007,16 @@ describe('useNodeHelpers()', () => { expect(result).toBeNull(); }); + it('does not warn when a private credential is used under a chat trigger', () => { + mockConnectedPrivateCred(true); + mockDocumentStore.workflowTriggerNodes = [buildTriggerNode(CHAT_TRIGGER)]; + + const { getNodeCredentialIssues } = useNodeHelpers(); + const result = getNodeCredentialIssues(buildNotionNode(), notionNodeType); + + expect(result).toBeNull(); + }); + it('warns when a private credential is used under a non-manual trigger', () => { mockConnectedPrivateCred(true); mockDocumentStore.workflowTriggerNodes = [buildTriggerNode(WEBHOOK_TRIGGER)]; @@ -1199,7 +1210,7 @@ describe('useNodeHelpers()', () => { }); describe('precedence', () => { - it('emits only privateNotConnected when both rules would fire (not-connected wins)', () => { + it('emits no issue for a not-connected private credential even under an incompatible trigger', () => { const cred = makePrivateCred({ connectedByMe: false }); mockedStore(useCredentialsStore).getCredentialById = vi.fn().mockReturnValue(cred); mockedStore(useCredentialsStore).getCredentialsByType = vi.fn().mockReturnValue([cred]); @@ -1208,9 +1219,9 @@ describe('useNodeHelpers()', () => { const { getNodeCredentialIssues } = useNodeHelpers(); const result = getNodeCredentialIssues(buildNotionNode(), notionNodeType); - expect(result?.credentials?.[NOTION_API]).toHaveLength(1); - expect(result?.credentials?.[NOTION_API][0]).toContain('My Notion'); - expect(result?.credentials?.[NOTION_API][0]).not.toContain('manually triggered workflows'); + // The not-connected state is surfaced as a UI warning; the manual-trigger + // requirement only applies once the user has connected their account. + expect(result).toBeNull(); }); }); }); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts index 61987afe99c..bcf093e98c5 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts @@ -436,13 +436,10 @@ export function useNodeHelpers() { const credential = credentialsStore.getCredentialById(details.id); if (!credential?.isResolvable) continue; - if (!credential.connectedByMe) { - foundIssues[credTypeName] = [ - i18n.baseText('nodeIssues.credentials.privateNotConnected', { - interpolate: { name: credential.name }, - }), - ]; - } else if (incompatibleTrigger) { + // An unconnected private credential is a missing setup step, not a hard + // error — it's surfaced as a warning via the credential callout/banner in + // the UI rather than a node issue, so we don't add it here. + if (credential.connectedByMe && incompatibleTrigger) { foundIssues[credTypeName] = [ i18n.baseText('nodeIssues.credentials.privateRequiresManualTrigger'), ]; diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.test.ts index 6243d4f2af8..f56bccba5ef 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.test.ts @@ -372,7 +372,7 @@ describe('CredentialCard', () => { expect(getByTestId('credential-card-connect')).toBeInTheDocument(); }); - it('should show Connected label when private and connected', () => { + it('should not show Connect button or Connected label when private and connected', () => { const data = createCredential({ isResolvable: true, connectedByMe: true, @@ -381,10 +381,10 @@ describe('CredentialCard', () => { const { getByTestId, queryByTestId } = renderComponent({ props: { data } }); + // Once connected, a private credential is just a regular credential with a + // Private label — no connect prompt and no separate connected state. expect(queryByTestId('credential-card-connect')).not.toBeInTheDocument(); - const connectedLabel = getByTestId('credential-card-connected'); - expect(connectedLabel).toBeInTheDocument(); - expect(connectedLabel).toHaveTextContent('Connected'); + expect(queryByTestId('credential-card-connected')).not.toBeInTheDocument(); expect(getByTestId('card-badge')).toBeInTheDocument(); }); diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue index 0747e44d9b7..3b516addad0 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue @@ -79,13 +79,6 @@ const isPrivateUnconnected = computed( credentialPermissions.value.update === true, ); -const isPrivateConnected = computed( - () => - isDynamicCredentialsEnabled.value && - props.data.isResolvable === true && - props.data.connectedByMe === true, -); - const actions = computed(() => { const items = [ { @@ -258,7 +251,7 @@ function moveResource() { data-test-id="credential-card-dynamic" > - + {{ locale.baseText('credentials.private.badge') }} @@ -308,16 +301,6 @@ function moveResource() { {{ locale.baseText('credentials.item.connect') }} - - - - {{ locale.baseText('credentials.item.connected') }} - - { >