diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index c0981aad625..3daa468c619 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -949,9 +949,7 @@
"credentialEdit.credentialConfig.missingCredentialType": "This credential's type isn't available. This usually happens when a previously installed community or custom node was uninstalled.",
"credentialEdit.credentialConfig.oauthModeManaged": "Managed OAuth2 (recommended)",
"credentialEdit.credentialConfig.oauthModeCustom": "Custom OAuth2",
- "credentialEdit.credentialConfig.oauthModeManagedTitle": "Setup managed OAuth",
- "credentialEdit.credentialConfig.oauthModeCustomTitle": "Setup custom OAuth",
- "credentialEdit.credentialConfig.genericTitle": "Setup {credential}",
+
"credentialEdit.credentialConfig.setupCredential": "Setup credential",
"credentialEdit.credentialConfig.quickConnect": "Use quick connect",
"credentialEdit.credentialConfig.quickConnectTitle": "Connect to set up a credential",
diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.test.ts
index 454564c2fe1..2a13a55d4e0 100644
--- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.test.ts
+++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.test.ts
@@ -1,13 +1,22 @@
import CredentialConfig from './CredentialConfig.vue';
import { screen } from '@testing-library/vue';
-import type { ICredentialDataDecryptedObject, ICredentialType } from 'n8n-workflow';
+import type {
+ ICredentialDataDecryptedObject,
+ ICredentialType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+import { NodeConnectionTypes } from 'n8n-workflow';
import { createTestingPinia } from '@pinia/testing';
import type { RenderOptions } from '@/__tests__/render';
import { createComponentRenderer } from '@/__tests__/render';
import { STORES } from '@n8n/stores';
import { vi } from 'vitest';
import { useCredentialsStore } from '../../credentials.store';
+import { useNDVStore } from '@/features/ndv/shared/ndv.store';
+import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
+import { mockedStore } from '@/__tests__/utils';
import { addCredentialTranslation } from '@n8n/i18n';
+import type { INodeUi } from '@/Interface';
vi.mock('@n8n/i18n', async () => {
const actual = await vi.importActual('@n8n/i18n');
@@ -440,4 +449,163 @@ describe('CredentialConfig', () => {
expect(screen.queryByTestId('copy-input')).not.toBeInTheDocument();
});
});
+
+ describe('Mode Selector visibility', () => {
+ const dropboxApiType: ICredentialType = {
+ name: 'dropboxApi',
+ displayName: 'Dropbox API',
+ properties: [
+ { displayName: 'Access Token', name: 'accessToken', type: 'string', default: '' },
+ ],
+ };
+
+ const dropboxOAuth2ApiType: ICredentialType = {
+ name: 'dropboxOAuth2Api',
+ extends: ['oAuth2Api'],
+ displayName: 'Dropbox OAuth2 API',
+ properties: [],
+ };
+
+ const twoAuthNodeType = {
+ displayName: 'Dropbox',
+ name: 'n8n-nodes-base.dropbox',
+ group: ['input'],
+ version: 1,
+ description: 'Access data on Dropbox',
+ defaults: { name: 'Dropbox' },
+ inputs: [NodeConnectionTypes.Main],
+ outputs: [NodeConnectionTypes.Main],
+ credentials: [
+ {
+ name: 'dropboxApi',
+ required: true,
+ displayOptions: { show: { authentication: ['accessToken'] } },
+ },
+ {
+ name: 'dropboxOAuth2Api',
+ required: true,
+ displayOptions: { show: { authentication: ['oAuth2'] } },
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Authentication',
+ name: 'authentication',
+ type: 'options',
+ options: [
+ { name: 'Access Token', value: 'accessToken' },
+ { name: 'OAuth2', value: 'oAuth2' },
+ ],
+ default: 'accessToken',
+ },
+ ],
+ } as unknown as INodeTypeDescription;
+
+ const writePermissions = {
+ create: true,
+ update: true,
+ read: true,
+ delete: true,
+ share: true,
+ list: true,
+ move: true,
+ };
+
+ function setupMultiAuthStores() {
+ const pinia = createTestingPinia({
+ stubActions: false,
+ initialState: {
+ [STORES.SETTINGS]: {
+ settings: { enterprise: { sharing: false, externalSecrets: false } },
+ },
+ },
+ });
+
+ const ndvStore = mockedStore(useNDVStore);
+ ndvStore.activeNode = {
+ parameters: { authentication: 'accessToken' },
+ type: 'n8n-nodes-base.dropbox',
+ typeVersion: 1,
+ position: [0, 0],
+ id: 'test-node-id',
+ name: 'Test Node',
+ credentials: {},
+ } as INodeUi;
+
+ const nodeTypesStore = mockedStore(useNodeTypesStore);
+ nodeTypesStore.setNodeTypes([twoAuthNodeType]);
+
+ const credStore = useCredentialsStore();
+ credStore.state.credentialTypes = {
+ dropboxApi: dropboxApiType,
+ dropboxOAuth2Api: dropboxOAuth2ApiType,
+ };
+
+ return pinia;
+ }
+
+ it('should show mode selector for existing credential with update permission', () => {
+ const pinia = setupMultiAuthStores();
+
+ renderComponent({
+ pinia,
+ props: {
+ isManaged: false,
+ mode: 'edit',
+ credentialId: 'existing-cred-123',
+ credentialType: dropboxApiType,
+ credentialProperties: dropboxApiType.properties,
+ credentialData: {} as ICredentialDataDecryptedObject,
+ credentialPermissions: writePermissions,
+ },
+ });
+
+ expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
+ });
+
+ it('should show mode selector for new credential with create permission', () => {
+ const pinia = setupMultiAuthStores();
+
+ renderComponent({
+ pinia,
+ props: {
+ isManaged: false,
+ mode: 'new',
+ credentialType: dropboxApiType,
+ credentialProperties: dropboxApiType.properties,
+ credentialData: {} as ICredentialDataDecryptedObject,
+ credentialPermissions: writePermissions,
+ },
+ });
+
+ expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
+ });
+
+ it('should not show mode selector for existing credential without update permission', () => {
+ const pinia = setupMultiAuthStores();
+
+ renderComponent({
+ pinia,
+ props: {
+ isManaged: false,
+ mode: 'edit',
+ credentialId: 'existing-cred-123',
+ credentialType: dropboxApiType,
+ credentialProperties: dropboxApiType.properties,
+ credentialData: {} as ICredentialDataDecryptedObject,
+ credentialPermissions: {
+ create: false,
+ update: false,
+ read: true,
+ delete: false,
+ share: false,
+ list: true,
+ move: false,
+ },
+ },
+ });
+
+ expect(screen.queryByTestId('credential-mode-selector')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
index 7f908eef9bf..3bd9c19d0c5 100644
--- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
+++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
@@ -283,7 +283,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
{
- describe('2 auth options (switch link)', () => {
- it('should show switch link when there are exactly 2 options', () => {
- const pinia = setupStores({
- nodeType: twoAuthNodeType,
- node: makeNode('n8n-nodes-base.dropbox', 'accessToken'),
- credentialTypes: {
- dropboxApi: dropboxApiType,
- dropboxOAuth2Api: dropboxOAuth2ApiType,
- },
- });
-
- renderComponent({
- pinia,
- props: {
- credentialType: dropboxApiType,
- },
- });
-
- expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
- expect(screen.getByTestId('credential-mode-switch-link')).toBeInTheDocument();
- expect(screen.queryByTestId('credential-mode-dropdown-trigger')).not.toBeInTheDocument();
- });
-
- it('should emit authTypeChanged when clicking switch link', async () => {
- const pinia = setupStores({
- nodeType: twoAuthNodeType,
- node: makeNode('n8n-nodes-base.dropbox', 'accessToken'),
- credentialTypes: {
- dropboxApi: dropboxApiType,
- dropboxOAuth2Api: dropboxOAuth2ApiType,
- },
- });
-
- const { emitted } = renderComponent({
- pinia,
- props: {
- credentialType: dropboxApiType,
- },
- });
-
- await userEvent.click(screen.getByTestId('credential-mode-switch-link'));
-
- expect(emitted('update:authType')).toHaveLength(1);
- expect(emitted('update:authType')[0]).toEqual([{ type: 'oAuth2' }]);
- });
-
- it('should emit the other auth type when switch link is clicked from OAuth2', async () => {
- const pinia = setupStores({
- nodeType: twoAuthNodeType,
- node: makeNode('n8n-nodes-base.dropbox', 'oAuth2'),
- credentialTypes: {
- dropboxApi: dropboxApiType,
- dropboxOAuth2Api: dropboxOAuth2ApiType,
- },
- });
-
- const { emitted } = renderComponent({
- pinia,
- props: {
- credentialType: dropboxOAuth2ApiType,
- },
- });
-
- // From OAuth2, switch to Access Token
- await userEvent.click(screen.getByTestId('credential-mode-switch-link'));
- expect(emitted('update:authType')[0]).toEqual([{ type: 'accessToken' }]);
- });
- });
-
- describe('3+ auth options (dropdown menu)', () => {
+ describe('dropdown menu', () => {
it('should show dropdown trigger when there are 3+ options', () => {
const pinia = setupStores({
nodeType: threeAuthNodeType,
@@ -245,7 +176,6 @@ describe('CredentialModeSelector', () => {
expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
expect(screen.getByTestId('credential-mode-dropdown-trigger')).toBeInTheDocument();
- expect(screen.queryByTestId('credential-mode-switch-link')).not.toBeInTheDocument();
expect(screen.getByText('Setup credential')).toBeInTheDocument();
});
@@ -331,7 +261,6 @@ describe('CredentialModeSelector', () => {
// plus the Access Token option = 3 total, so dropdown should appear
expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
expect(screen.getByTestId('credential-mode-dropdown-trigger')).toBeInTheDocument();
- expect(screen.queryByTestId('credential-mode-switch-link')).not.toBeInTheDocument();
expect(screen.getByText('Setup credential')).toBeInTheDocument();
});
@@ -468,7 +397,7 @@ describe('CredentialModeSelector', () => {
properties: [],
} as unknown as INodeTypeDescription;
- it('should show "Use quick connect" link in manual mode and emit quickConnectEnabled on click', async () => {
+ it('should emit quickConnectEnabled when selecting quick connect from dropdown', async () => {
const pinia = setupStores({
nodeType: singleCredNodeType,
node: makeNode('n8n-nodes-base.firecrawl', ''),
@@ -484,17 +413,23 @@ describe('CredentialModeSelector', () => {
},
});
- const link = screen.getByTestId('credential-mode-switch-link');
- expect(link).toHaveTextContent('Use quick connect');
- expect(screen.queryByTestId('credential-mode-dropdown-trigger')).not.toBeInTheDocument();
+ expect(screen.getByTestId('credential-mode-dropdown-trigger')).toBeInTheDocument();
- await userEvent.click(link);
+ await userEvent.click(screen.getByTestId('credential-mode-dropdown-trigger'));
- expect(emitted('update:authType')).toHaveLength(1);
- expect(emitted('update:authType')[0]).toEqual([{ type: '', quickConnectEnabled: true }]);
+ await waitFor(() => {
+ expect(document.querySelector('[role="menu"]')).toBeInTheDocument();
+ });
+
+ await userEvent.click(screen.getByRole('menuitem', { name: /Use quick connect/ }));
+
+ await waitFor(() => {
+ expect(emitted('update:authType')).toHaveLength(1);
+ expect(emitted('update:authType')[0]).toEqual([{ type: '', quickConnectEnabled: true }]);
+ });
});
- it('should show "Set up manually" link in QC mode and emit manual fallback on click', async () => {
+ it('should emit manual fallback when selecting set up manually from dropdown in QC mode', async () => {
const pinia = setupStores({
nodeType: singleCredNodeType,
node: makeNode('n8n-nodes-base.firecrawl', ''),
@@ -510,13 +445,18 @@ describe('CredentialModeSelector', () => {
},
});
- const link = screen.getByTestId('credential-mode-switch-link');
- expect(link).toHaveTextContent('Set up manually');
+ await userEvent.click(screen.getByTestId('credential-mode-dropdown-trigger'));
- await userEvent.click(link);
+ await waitFor(() => {
+ expect(document.querySelector('[role="menu"]')).toBeInTheDocument();
+ });
- expect(emitted('update:authType')).toHaveLength(1);
- expect(emitted('update:authType')[0]).toEqual([{ type: '' }]);
+ await userEvent.click(screen.getByRole('menuitem', { name: /Set up manually/ }));
+
+ await waitFor(() => {
+ expect(emitted('update:authType')).toHaveLength(1);
+ expect(emitted('update:authType')[0]).toEqual([{ type: '' }]);
+ });
});
it('should not show selector when quickConnectAvailable is false for single-cred nodes', () => {
@@ -559,7 +499,6 @@ describe('CredentialModeSelector', () => {
// QC + Access Token + OAuth2 = 3 options → dropdown
expect(screen.getByTestId('credential-mode-selector')).toBeInTheDocument();
expect(screen.getByTestId('credential-mode-dropdown-trigger')).toBeInTheDocument();
- expect(screen.queryByTestId('credential-mode-switch-link')).not.toBeInTheDocument();
});
});
diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialModeSelector.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialModeSelector.vue
index 217d657d70c..df116fcae60 100644
--- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialModeSelector.vue
+++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialModeSelector.vue
@@ -4,7 +4,7 @@ import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
import { computed } from 'vue';
-import { N8nButton, N8nIcon, N8nLink, N8nText } from '@n8n/design-system';
+import { N8nButton, N8nIcon, N8nText } from '@n8n/design-system';
import {
N8nDropdownMenu,
type DropdownMenuItemProps,
@@ -147,41 +147,15 @@ function isSelected(option: CredentialModeOption): boolean {
}
const showSelector = computed(() => options.value.length >= 2);
-const showDropdown = computed(() => options.value.length > 2);
const selectedOption = computed(() => {
- const selected = options.value.find((option) => isSelected(option.value)) ?? null;
- return selected;
-});
-
-const otherOption = computed(() => {
- if (showDropdown.value) return null;
- return options.value.find((option) => !isSelected(option.value)) ?? null;
+ return options.value.find((option) => isSelected(option.value)) ?? null;
});
const headingText = computed(() => {
if (props.isQuickConnectMode) {
return i18n.baseText('credentialEdit.credentialConfig.quickConnectTitle');
}
-
- if (options.value.length > 2) {
- return i18n.baseText('credentialEdit.credentialConfig.setupCredential');
- }
-
- if (props.showManagedOauthOptions) {
- if (props.useCustomOauth) {
- return i18n.baseText('credentialEdit.credentialConfig.oauthModeCustomTitle');
- }
- return i18n.baseText('credentialEdit.credentialConfig.oauthModeManagedTitle');
- }
-
- const authName = selectedAuthType.value?.name;
- if (!authName) {
- return i18n.baseText('credentialEdit.credentialConfig.setupCredential');
- }
-
- return i18n.baseText('credentialEdit.credentialConfig.genericTitle', {
- interpolate: { credential: authName },
- });
+ return i18n.baseText('credentialEdit.credentialConfig.setupCredential');
});
const menuItems = computed>>(() => {
@@ -196,12 +170,6 @@ function onOptionChange(value: CredentialModeOption): void {
if (isSelected(value)) return;
emit('update:authType', value);
}
-
-function switchToOther(): void {
- if (otherOption.value) {
- onOptionChange(otherOption.value.value);
- }
-}
@@ -211,20 +179,7 @@ function switchToOther(): void {
{{ headingText }}
-
- {{ otherOption?.name }}
-
-