From 63dc2854936f3e282ca422cd9b9babeab65a2375 Mon Sep 17 00:00:00 2001 From: MHMD <45968454+dou-mhmd@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:49:45 +0000 Subject: [PATCH] fix(editor): Show the owner and credential type for credentials the user cannot use (#40669) --- .../frontend/@n8n/i18n/src/locales/en.json | 2 + .../components/NodeCredentials.test.ts | 10 +++-- .../components/NodeCredentials.vue | 39 +++++++++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index e2f2e0ec7e2..6e438e05936 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2358,6 +2358,8 @@ "nodeCredentials.group.availableInThisProject": "Available in this project", "nodeCredentials.group.availableToEveryone": "Available to everyone", "nodeCredentials.group.notAvailableToYou": "Not available to you", + "nodeCredentials.unusable.displayName": "{owner}'s {type}", + "nodeCredentials.unusable.notShared": "Not shared with {project}", "nodeCredentials.unusable.owner": "{owner}'s", "nodeCredentials.unusable.ownerNotShared": "{owner}'s · not shared with {project}", "nodeCredentials.unusable.tooltip.title": "{owner}'s credential", diff --git a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts index e5c96a4918b..11fceb83f98 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts @@ -3687,7 +3687,7 @@ describe('NodeCredentials', () => { const select = screen.getByTestId('node-credentials-select'); await waitFor(() => - expect(within(select).getByRole('combobox')).toHaveValue("Alice's OpenAi"), + expect(within(select).getByRole('combobox')).toHaveValue("Alice Chen's OpenAi"), ); await userEvent.click(select); @@ -3695,7 +3695,9 @@ describe('NodeCredentials', () => { expect(await screen.findByTestId(UNUSABLE_HEADER)).toHaveTextContent('Not available to you'); const option = screen.getByTestId('node-credentials-select-item-alice-cred'); expect(option).toHaveClass('is-disabled'); - expect(option).toHaveTextContent("Alice Chen's · not shared with Marketing"); + expect(option).toHaveTextContent("Alice Chen's OpenAi"); + expect(option).toHaveTextContent('Not shared with Marketing'); + expect(option).not.toHaveTextContent("Alice's OpenAi"); // Only the current credential stays; others the user cannot use are not offered. expect(screen.queryByTestId('node-credentials-select-item-bob-cred')).not.toBeInTheDocument(); }); @@ -3707,7 +3709,7 @@ describe('NodeCredentials', () => { await userEvent.hover(warning.querySelector('svg') ?? warning); expect( - await screen.findByText("Only Alice Chen can run or publish with Alice's OpenAi."), + await screen.findByText("Only Alice Chen can run or publish with Alice Chen's OpenAi."), ).toBeInTheDocument(); expect( screen.getByText( @@ -3724,7 +3726,7 @@ describe('NodeCredentials', () => { await waitFor(() => expect( within(screen.getByTestId('node-credentials-select')).getByRole('combobox'), - ).toHaveValue("Alice's OpenAi"), + ).toHaveValue("Alice Chen's OpenAi"), ); }); diff --git a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue index 58339772d8e..fcfad4a9e38 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue @@ -317,6 +317,15 @@ function getUnlistedOwnerName(credentialType: string): string { ); } +function getUnusableDisplayName(credentialType: string): string { + return i18n.baseText('nodeCredentials.unusable.displayName', { + interpolate: { + owner: getUnlistedOwnerName(credentialType), + type: credentialTypeNames.value[credentialType] ?? credentialType, + }, + }); +} + /** * Resolve a picked credential from the rows the dropdown is showing before * consulting the store. A host-supplied `credentials` list can hold ids the @@ -660,6 +669,9 @@ function getSelectedName(type: string) { if (isAiGatewayManagedCredentials(type)) { return N8N_CREDITS_LABEL; } + if (getUnusableSelected(type)) { + return getUnusableDisplayName(type); + } return selected.value?.[type]?.name; } @@ -1168,14 +1180,15 @@ function buildCredentialRows( function buildCurrentRow(credentialType: string, usable: boolean): CredentialRow | undefined { const credential = getUnlistedSelected(credentialType); if (!credential || (credential.currentUserCanUse !== false) !== usable) return undefined; - if (!matches(filter.value, credential.name)) return undefined; + const name = usable ? credential.name : getUnusableDisplayName(credentialType); + if (!matches(filter.value, name)) return undefined; return { kind: 'current', key: credential.id, id: credential.id, - name: credential.name, - meta: ownerMeta(credentialType), + name, + meta: ownerMeta(credentialType, usable), usable, }; } @@ -1184,11 +1197,21 @@ function buildCurrentRow(credentialType: string, usable: boolean): CredentialRow * Who owns the current credential, and where it is missing: the project the * workflow lives in, which a personal space does not name. */ -function ownerMeta(credentialType: string): string { - const owner = getUnlistedOwnerName(credentialType); - const projectName = pickerHomeProject.value?.name; +function ownerMeta(credentialType: string, usable: boolean): string { + const projectName = !isPersonalSpace.value ? pickerHomeProject.value?.name : undefined; - return !isPersonalSpace.value && projectName + // The owner is already part of the name of a credential the user cannot use. + if (!usable) { + return projectName + ? i18n.baseText('nodeCredentials.unusable.notShared', { + interpolate: { project: projectName }, + }) + : ''; + } + + const owner = getUnlistedOwnerName(credentialType); + + return projectName ? i18n.baseText('nodeCredentials.unusable.ownerNotShared', { interpolate: { owner, project: projectName }, }) @@ -1233,7 +1256,7 @@ function unusableTooltip(credentialType: string): { title: string; lines: string : i18n.baseText('nodeCredentials.unusable.tooltip.title.unknownOwner'), lines: [ i18n.baseText('nodeCredentials.unusable.tooltip.onlyOwner', { - interpolate: { owner, credential: credential?.name ?? '' }, + interpolate: { owner, credential: getUnusableDisplayName(credentialType) }, }), i18n.baseText('nodeCredentials.unusable.tooltip.canEdit'), !isPersonalSpace.value && projectName