mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 10:39:23 +02:00
fix(editor): Polish private credential pills, callout, and banners (#31604)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8eda311630
commit
7e83c7b591
|
|
@ -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..",
|
||||
|
|
|
|||
|
|
@ -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<Workflow>(), ['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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<span :class="$style.dynamicBadgeText">
|
||||
<N8nIcon icon="key-round" size="medium" />
|
||||
<N8nIcon icon="key-round" size="small" />
|
||||
{{ locale.baseText('credentials.private.badge') }}
|
||||
</span>
|
||||
</N8nBadge>
|
||||
|
|
@ -308,16 +301,6 @@ function moveResource() {
|
|||
{{ locale.baseText('credentials.item.connect') }}
|
||||
</N8nButton>
|
||||
</N8nTooltip>
|
||||
<span
|
||||
v-else-if="isPrivateConnected"
|
||||
:class="$style.connectedLabel"
|
||||
data-test-id="credential-card-connected"
|
||||
>
|
||||
<N8nIcon icon="circle-check" size="small" color="success" />
|
||||
<N8nText size="small" color="success">
|
||||
{{ locale.baseText('credentials.item.connected') }}
|
||||
</N8nText>
|
||||
</span>
|
||||
<N8nActionToggle
|
||||
data-test-id="credential-card-actions"
|
||||
:actions="actions"
|
||||
|
|
@ -367,18 +350,13 @@ function moveResource() {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.connectedLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.dynamicBadgeText {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
font-size: var(--font-size--3xs);
|
||||
height: 18px;
|
||||
font-size: var(--font-size--2xs);
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
|
|
|
|||
|
|
@ -411,13 +411,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
|||
>
|
||||
<template #button>
|
||||
<div :class="$style.bannerActions">
|
||||
<template v-if="isGoogleOAuthType">
|
||||
<p
|
||||
:class="$style.googleReconnectLabel"
|
||||
v-text="`${i18n.baseText('credentialEdit.credentialConfig.reconnect')}:`"
|
||||
/>
|
||||
<GoogleAuthButton @click="$emit('oauth')" />
|
||||
</template>
|
||||
<GoogleAuthButton v-if="isGoogleOAuthType" @click="$emit('oauth')" />
|
||||
<QuickConnectButton
|
||||
v-else
|
||||
size="small"
|
||||
|
|
@ -429,8 +423,8 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
|||
/>
|
||||
<N8nButton
|
||||
v-if="showDisconnectButton"
|
||||
variant="subtle"
|
||||
size="small"
|
||||
variant="outline"
|
||||
:size="isGoogleOAuthType ? 'xlarge' : 'small'"
|
||||
:label="i18n.baseText('credentialEdit.credentialConfig.disconnect')"
|
||||
data-test-id="oauth-disconnect-button"
|
||||
@click="$emit('disconnect')"
|
||||
|
|
@ -570,10 +564,6 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
|||
}
|
||||
}
|
||||
|
||||
.googleReconnectLabel {
|
||||
margin-right: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.bannerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -572,7 +572,14 @@ async function beforeClose() {
|
|||
{ cancelButtonText: i18n.baseText(`${I18N_PREFIX}.beforeClose1.cancelButtonText`) },
|
||||
);
|
||||
keepEditing = confirmAction === MODAL_CONFIRM;
|
||||
} else if (credentialPermissions.value.update && isOAuthType.value && !isOAuthConnected.value) {
|
||||
} else if (
|
||||
credentialPermissions.value.update &&
|
||||
isOAuthType.value &&
|
||||
!isOAuthConnected.value &&
|
||||
// Private credentials are only the reusable "blueprint" — connecting is a
|
||||
// per-user step done later, so we don't prompt to connect before closing.
|
||||
!isResolvable.value
|
||||
) {
|
||||
const confirmAction = await confirmModal('beforeClose2', undefined, {
|
||||
cancelButtonText: i18n.baseText(`${I18N_PREFIX}.beforeClose2.cancelButtonText`),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { createTestingPinia } from '@pinia/testing';
|
|||
import { setActivePinia } from 'pinia';
|
||||
import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import { SYSTEM_RESOLVER_ID } from '@n8n/api-types';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import NodeCredentials from './NodeCredentials.vue';
|
||||
|
|
@ -1478,17 +1479,17 @@ describe('NodeCredentials', () => {
|
|||
expect(screen.queryByTestId('node-credential-private-connect')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows not-connected status row with Connect link when connectedByMe is false', async () => {
|
||||
it('shows the connect prompt with a Connect button when connectedByMe is false', async () => {
|
||||
credentialsStore.state.credentials = {
|
||||
'private-cred-id': { ...privateCredential, connectedByMe: false },
|
||||
};
|
||||
renderComponent({ props: { node: notionNode, overrideCredType: 'openAiApi' } });
|
||||
|
||||
expect(screen.getByText("Your account isn't connected yet.")).toBeInTheDocument();
|
||||
expect(screen.getByText('Connect your account')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('node-credential-private-connect')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the Connect link when the user lacks update permission', async () => {
|
||||
it('hides the Connect button when the user lacks update permission', async () => {
|
||||
credentialsStore.state.credentials = {
|
||||
'private-cred-id': {
|
||||
...privateCredential,
|
||||
|
|
@ -1498,11 +1499,11 @@ describe('NodeCredentials', () => {
|
|||
};
|
||||
renderComponent({ props: { node: notionNode, overrideCredType: 'openAiApi' } });
|
||||
|
||||
expect(screen.getByText("Your account isn't connected yet.")).toBeInTheDocument();
|
||||
expect(screen.getByText('Connect your account')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('node-credential-private-connect')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking Connect link calls uiStore.openExistingCredential with the credential id', async () => {
|
||||
it('clicking the Connect button calls uiStore.openExistingCredential with the credential id', async () => {
|
||||
credentialsStore.state.credentials = {
|
||||
'private-cred-id': { ...privateCredential, connectedByMe: false },
|
||||
};
|
||||
|
|
@ -1515,5 +1516,25 @@ describe('NodeCredentials', () => {
|
|||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('still renders the callout when the workflow uses the default (system) resolver', async () => {
|
||||
workflowDocumentStore.mergeSettings({ credentialResolverId: SYSTEM_RESOLVER_ID });
|
||||
credentialsStore.state.credentials = {
|
||||
'private-cred-id': { ...privateCredential, connectedByMe: false },
|
||||
};
|
||||
renderComponent({ props: { node: notionNode, overrideCredType: 'openAiApi' } });
|
||||
|
||||
expect(screen.getByTestId('node-credential-private-callout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the callout when the workflow uses a non-default resolver', async () => {
|
||||
workflowDocumentStore.mergeSettings({ credentialResolverId: 'slack-resolver' });
|
||||
credentialsStore.state.credentials = {
|
||||
'private-cred-id': { ...privateCredential, connectedByMe: false },
|
||||
};
|
||||
renderComponent({ props: { node: notionNode, overrideCredType: 'openAiApi' } });
|
||||
|
||||
expect(screen.queryByTestId('node-credential-private-callout')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { isEmpty } from '@/app/utils/typesUtils';
|
|||
import { getResourcePermissions } from '@n8n/permissions';
|
||||
import { useNodeCredentialOptions } from '../composables/useNodeCredentialOptions';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { SYSTEM_RESOLVER_ID } from '@n8n/api-types';
|
||||
import { useAiGateway } from '@/app/composables/useAiGateway';
|
||||
import AiGatewaySelector from '@/app/components/AiGatewaySelector.vue';
|
||||
|
||||
|
|
@ -47,7 +48,6 @@ import {
|
|||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nLink,
|
||||
N8nNotice,
|
||||
N8nOption,
|
||||
N8nSelect,
|
||||
N8nText,
|
||||
|
|
@ -185,6 +185,16 @@ function canConnectPrivateCredential(credentialType: string): boolean {
|
|||
return getResourcePermissions(credential?.scopes).credential.update === true;
|
||||
}
|
||||
|
||||
// The connect / connected callout is only relevant when the workflow uses the
|
||||
// default (system) resolver, where resolution maps to the n8n user's own
|
||||
// connection. With a custom resolver (e.g. Slack, OAuth) the runtime account is
|
||||
// chosen by the resolver, not the n8n user, so their own connection state is
|
||||
// irrelevant and we don't surface it.
|
||||
const isDefaultResolver = computed(() => {
|
||||
const resolverId = workflowDocumentStore?.value.settings?.credentialResolverId;
|
||||
return !resolverId || resolverId === SYSTEM_RESOLVER_ID;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.node.parameters,
|
||||
(newValue, oldValue) => {
|
||||
|
|
@ -828,7 +838,7 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
data-test-id="credential-option-private-badge"
|
||||
>
|
||||
<span :class="$style.dynamicBadgeText">
|
||||
<N8nIcon icon="key-round" size="medium" />
|
||||
<N8nIcon icon="key-round" size="small" />
|
||||
{{ i18n.baseText('credentials.private.badge') }}
|
||||
</span>
|
||||
</N8nBadge>
|
||||
|
|
@ -860,7 +870,7 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
data-test-id="node-credential-private-icon"
|
||||
>
|
||||
<span :class="$style.dynamicBadgeText">
|
||||
<N8nIcon icon="key-round" size="medium" />
|
||||
<N8nIcon icon="key-round" size="small" />
|
||||
{{ i18n.baseText('credentials.private.badge') }}
|
||||
</span>
|
||||
</N8nBadge>
|
||||
|
|
@ -893,39 +903,60 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getSelectedPrivateCredential(type.name)" :class="$style.noticesContainer">
|
||||
<N8nNotice
|
||||
v-if="getSelectedPrivateCredential(type.name)"
|
||||
:theme="isPrivateConnected(type.name) ? 'info' : 'warning'"
|
||||
<div
|
||||
v-if="getSelectedPrivateCredential(type.name) && isDefaultResolver"
|
||||
:class="$style.noticesContainer"
|
||||
>
|
||||
<div
|
||||
v-if="isPrivateConnected(type.name)"
|
||||
:class="$style.privateConnectedNotice"
|
||||
data-test-id="node-credential-private-callout"
|
||||
>
|
||||
<div :class="$style.privateNoticeContent">
|
||||
<N8nIcon icon="user" size="small" :class="$style.privateNoticeIcon" />
|
||||
<div>
|
||||
<span>{{ i18n.baseText('credentials.private.callout.title') }}</span>
|
||||
<div :class="$style.privateStatusRow">
|
||||
<template v-if="isPrivateConnected(type.name)">
|
||||
<N8nIcon icon="circle-check" color="success" size="small" />
|
||||
<N8nText size="small">{{
|
||||
i18n.baseText('credentials.private.callout.connected')
|
||||
}}</N8nText>
|
||||
</template>
|
||||
<template v-else>
|
||||
<N8nText size="small" :class="$style.privateNotConnectedText">{{
|
||||
i18n.baseText('credentials.private.callout.notConnected')
|
||||
}}</N8nText>
|
||||
<N8nLink
|
||||
v-if="canConnectPrivateCredential(type.name)"
|
||||
data-test-id="node-credential-private-connect"
|
||||
@click="editCredential(type.name)"
|
||||
>
|
||||
{{ i18n.baseText('credentials.private.callout.connect') }}
|
||||
</N8nLink>
|
||||
</template>
|
||||
</div>
|
||||
<span :class="$style.privateNoticeIconBadge">
|
||||
<N8nIcon icon="user" size="medium" />
|
||||
</span>
|
||||
<div>
|
||||
<N8nText size="small">{{
|
||||
i18n.baseText('credentials.private.callout.title')
|
||||
}}</N8nText>
|
||||
<div :class="$style.privateStatusRow">
|
||||
<N8nIcon icon="circle-check" color="success" size="small" />
|
||||
<N8nText size="small" color="success">{{
|
||||
i18n.baseText('credentials.private.callout.connected')
|
||||
}}</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
</N8nNotice>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="$style.privateConnectPrompt"
|
||||
data-test-id="node-credential-private-callout"
|
||||
>
|
||||
<span :class="$style.privateNoticeIconBadge">
|
||||
<N8nIcon icon="user" size="medium" />
|
||||
</span>
|
||||
<div :class="$style.privateConnectText">
|
||||
<N8nText bold>{{
|
||||
i18n.baseText('credentials.private.callout.connectTitle')
|
||||
}}</N8nText>
|
||||
<N8nText size="small">{{
|
||||
getServiceName(type.name)
|
||||
? i18n.baseText('credentials.private.callout.connectDescription', {
|
||||
interpolate: { service: getServiceName(type.name) },
|
||||
})
|
||||
: i18n.baseText('credentials.private.callout.connectDescriptionGeneric')
|
||||
}}</N8nText>
|
||||
</div>
|
||||
<N8nButton
|
||||
v-if="canConnectPrivateCredential(type.name)"
|
||||
:class="$style.privateConnectButton"
|
||||
variant="outline"
|
||||
size="small"
|
||||
:label="i18n.baseText('credentials.private.callout.connect')"
|
||||
data-test-id="node-credential-private-connect"
|
||||
@click="editCredential(type.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</N8nInputLabel>
|
||||
</div>
|
||||
|
|
@ -1025,8 +1056,9 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
font-size: var(--font-size--3xs);
|
||||
height: 18px;
|
||||
font-size: var(--font-size--2xs);
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.noticesContainer {
|
||||
|
|
@ -1035,10 +1067,6 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
gap: var(--spacing--3xs);
|
||||
margin-top: var(--spacing--2xs);
|
||||
margin-bottom: var(--spacing--xs);
|
||||
|
||||
:global(.notice) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.privateStatusRow {
|
||||
|
|
@ -1048,18 +1076,56 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
|||
margin-top: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.privateNotConnectedText {
|
||||
color: var(--color--text--tint-1);
|
||||
}
|
||||
|
||||
.privateNoticeContent {
|
||||
.privateConnectedNotice {
|
||||
display: flex;
|
||||
gap: var(--spacing--xs);
|
||||
align-items: center;
|
||||
gap: var(--spacing--sm);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--color--text);
|
||||
|
||||
.privateNoticeIconBadge {
|
||||
background-color: var(--color--foreground--tint-1);
|
||||
color: var(--color--text--tint-1);
|
||||
}
|
||||
}
|
||||
|
||||
.privateNoticeIcon {
|
||||
.privateConnectPrompt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--sm);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
border: var(--border-width) var(--border-style) var(--callout--border-color--warning);
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--callout--color--background--warning);
|
||||
color: var(--color--text);
|
||||
|
||||
.privateNoticeIconBadge {
|
||||
background-color: var(--color--warning--tint-1);
|
||||
color: var(--color--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.privateNoticeIconBadge {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.privateConnectText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--5xs);
|
||||
}
|
||||
|
||||
.privateConnectButton {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.newCredential {
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ describe('CredentialsView', () => {
|
|||
expect(getByTestId('card-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Connected label for connected private credentials', () => {
|
||||
it('shows no connect prompt or connected label for connected private credentials', () => {
|
||||
enableDynamicCredentials();
|
||||
const credentialsStore = mockedStore(useCredentialsStore);
|
||||
credentialsStore.allCredentials = [
|
||||
|
|
@ -457,7 +457,7 @@ describe('CredentialsView', () => {
|
|||
const { getByTestId, queryByTestId } = renderComponent();
|
||||
|
||||
expect(queryByTestId('credential-card-connect')).not.toBeInTheDocument();
|
||||
expect(getByTestId('credential-card-connected')).toBeInTheDocument();
|
||||
expect(queryByTestId('credential-card-connected')).not.toBeInTheDocument();
|
||||
expect(getByTestId('card-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export const MINIMAX_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.minimax';
|
|||
export const MANUAL_TRIGGER_NODE_TYPES: readonly string[] = [
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const AI_VENDOR_NODE_TYPES = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user