fix(editor): Show the owner and credential type for credentials the user cannot use (#40669)

This commit is contained in:
MHMD 2026-10-08 13:49:45 +00:00 committed by GitHub
parent 5a46327401
commit 63dc285493
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 39 additions and 12 deletions

View File

@ -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",

View File

@ -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"),
);
});

View File

@ -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