feat(editor): Make node credential select searchable (#12497)

This commit is contained in:
Elias Meire 2025-01-10 10:05:57 +01:00 committed by GitHub
parent b1a40a231b
commit 91277c44f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 192 additions and 72 deletions

View file

@ -250,7 +250,7 @@ describe('Webhook Trigger node', () => {
});
// add credentials
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.actions.fillCredentialsForm();
@ -293,7 +293,7 @@ describe('Webhook Trigger node', () => {
});
// add credentials
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.actions.fillCredentialsForm();

View file

@ -297,10 +297,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the credential in this project (+ the 'Create new' option) should
// be in the dropdown
// Only the credential in this project should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 2);
getVisibleSelect().find('li').should('have.length', 1);
});
it('should only show credentials in their personal project for members', () => {
@ -325,10 +324,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
// Only the own credential the shared one should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').should('have.length', 2);
});
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
@ -355,10 +353,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
// Only the own credential the shared one should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 2);
getVisibleSelect().find('li').should('have.length', 1);
});
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
@ -400,10 +397,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the personal credentials of the workflow owner and the global owner
// should show up.
// Only the personal credentials of the workflow owner and the global owner should show up.
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 4);
getVisibleSelect().find('li').should('have.length', 3);
});
it('should show all personal credentials if the global owner owns the workflow', () => {
@ -421,6 +417,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
// Show all personal credentials
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.have.length', 2);
getVisibleSelect().find('li').should('have.have.length', 1);
});
});

View file

@ -31,7 +31,7 @@ function createNotionCredential() {
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
workflowPage.actions.openNode(NOTION_NODE_NAME);
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.actions.fillCredentialsForm();
cy.get('body').type('{esc}');
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
@ -79,7 +79,7 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
@ -99,7 +99,7 @@ describe('Credentials', () => {
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
@ -107,14 +107,13 @@ describe('Credentials', () => {
cy.get('.el-message-box').find('button').contains('Close').click();
workflowPage.getters.nodeCredentialsSelect().click();
// Add Service account credentials
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().last().click();
credentialsModal.actions.fillCredentialsForm();
// Both (+ the 'Create new' option) should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length.greaterThan', 2);
getVisibleSelect().find('li').should('have.length', 3);
});
it('should correctly render required and optional credentials', () => {
@ -130,13 +129,13 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').contains('Create New Credential').click();
workflowPage.getters.nodeCredentialsCreateOption().first().click();
// This one should show auth type selector
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}');
workflowPage.getters.nodeCredentialsSelect().last().click();
getVisibleSelect().find('li').contains('Create New Credential').click();
workflowPage.getters.nodeCredentialsCreateOption().last().click();
// This one should not show auth type selector
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
});
@ -148,7 +147,7 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters
@ -164,7 +163,7 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters
.nodeCredentialsSelect()
@ -189,7 +188,7 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
@ -232,7 +231,7 @@ describe('Credentials', () => {
cy.getByTestId('credential-select').click();
cy.contains('Adalo API').click();
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
@ -296,7 +295,7 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters
.nodeCredentialsSelect()
@ -325,7 +324,7 @@ describe('Credentials', () => {
workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
nodeDetailsView.getters.copyInput().should('not.exist');
});

View file

@ -89,7 +89,7 @@ describe('Community and custom nodes in canvas', () => {
workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
workflowPage.getters.nodeCredentialsLabel().click();
cy.contains('Create New Credential').click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.editCredentialModal().should('be.visible');
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
});
@ -98,7 +98,7 @@ describe('Community and custom nodes in canvas', () => {
workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
workflowPage.getters.nodeCredentialsLabel().click();
cy.contains('Create New Credential').click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.editCredentialModal().should('be.visible');
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
});

View file

@ -367,7 +367,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click();
@ -382,7 +382,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click();
@ -396,7 +396,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click();
@ -407,7 +407,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click();
@ -425,7 +425,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account personal project');
ndv.getters.backToCanvas().click();
@ -436,7 +436,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.should('have.length', 1)
.first()
.should('contain.text', 'Notion account personal project');
});

View file

@ -5,7 +5,6 @@ import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant';
import { NodeCreator } from '../pages/features/node-creator';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
const ndv = new NDV();
@ -434,7 +433,7 @@ describe('AI Assistant Credential Help', () => {
wf.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
wf.getters.nodeCredentialsSelect().should('exist');
wf.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
wf.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
ndv.getters.copyInput().should('not.exist');
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
@ -467,7 +466,7 @@ describe('AI Assistant Credential Help', () => {
wf.actions.addNodeToCanvas('Microsoft Outlook', true, true, 'Get a calendar');
wf.getters.nodeCredentialsSelect().should('exist');
wf.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
wf.getters.nodeCredentialsCreateOption().click();
ndv.getters.copyInput().should('not.exist');
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
credentialsModal.getters.credentialInputs().should('have.length', 1);

View file

@ -136,6 +136,12 @@ defineExpose({
<template v-if="$slots.suffix" #suffix>
<slot name="suffix" />
</template>
<template v-if="$slots.footer" #footer>
<slot name="footer" />
</template>
<template v-if="$slots.empty" #empty>
<slot name="empty" />
</template>
<slot></slot>
</ElSelect>
</div>

View file

@ -37,6 +37,8 @@
// Danger
--color-danger-shade-1: var(--prim-color-alt-c-shade-100);
--color-danger: var(--prim-color-alt-c);
--color-danger-light: var(--prim-color-alt-c-tint-150);
--color-danger-light-2: var(--prim-color-alt-c-tint-250);
--color-danger-tint-1: var(--prim-color-alt-c-tint-400);
--color-danger-tint-2: var(--prim-color-alt-c-tint-450);

View file

@ -1,5 +1,6 @@
import { describe, it } from 'vitest';
import { fireEvent, screen } from '@testing-library/vue';
import { screen } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import NodeCredentials from './NodeCredentials.vue';
import type { RenderOptions } from '@/__tests__/render';
@ -8,6 +9,7 @@ import { useCredentialsStore } from '@/stores/credentials.store';
import { mockedStore } from '@/__tests__/utils';
import type { INodeUi } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { useUIStore } from '../stores/ui.store';
const httpNode: INodeUi = {
parameters: {
@ -67,6 +69,7 @@ describe('NodeCredentials', () => {
const credentialsStore = mockedStore(useCredentialsStore);
const ndvStore = mockedStore(useNDVStore);
const uiStore = mockedStore(useUIStore);
beforeAll(() => {
credentialsStore.state.credentialTypes = {
@ -120,7 +123,7 @@ describe('NodeCredentials', () => {
const credentialsSelect = screen.getByTestId('node-credentials-select');
await fireEvent.click(credentialsSelect);
await userEvent.click(credentialsSelect);
expect(screen.queryByText('OpenAi account')).toBeInTheDocument();
});
@ -150,7 +153,7 @@ describe('NodeCredentials', () => {
const credentialsSelect = screen.getByTestId('node-credentials-select');
await fireEvent.click(credentialsSelect);
await userEvent.click(credentialsSelect);
expect(screen.queryByText('OpenAi account')).toBeInTheDocument();
expect(screen.queryByText('OpenAi account 2')).not.toBeInTheDocument();
@ -188,9 +191,69 @@ describe('NodeCredentials', () => {
const credentialsSelect = screen.getByTestId('node-credentials-select');
await fireEvent.click(credentialsSelect);
await userEvent.click(credentialsSelect);
expect(screen.queryByText('OpenAi account')).toBeInTheDocument();
expect(screen.queryByText('OpenAi account 2')).toBeInTheDocument();
});
it('should filter available credentials in the dropdown', async () => {
ndvStore.activeNode = httpNode;
credentialsStore.state.credentials = {
c8vqdPpPClh4TgIO: {
id: 'c8vqdPpPClh4TgIO',
name: 'OpenAi account',
type: 'openAiApi',
isManaged: false,
createdAt: '',
updatedAt: '',
},
test: {
id: 'test',
name: 'Test OpenAi account',
type: 'openAiApi',
isManaged: false,
createdAt: '',
updatedAt: '',
},
};
renderComponent();
const credentialsSelect = screen.getByTestId('node-credentials-select');
await userEvent.click(credentialsSelect);
expect(screen.queryByText('OpenAi account')).toBeInTheDocument();
expect(screen.queryByText('Test OpenAi account')).toBeInTheDocument();
const credentialSearch = credentialsSelect.querySelector('input') as HTMLElement;
await userEvent.type(credentialSearch, 'test');
expect(screen.queryByText('OpenAi account')).not.toBeInTheDocument();
expect(screen.queryByText('Test OpenAi account')).toBeInTheDocument();
});
it('should open the new credential modal when clicked', async () => {
ndvStore.activeNode = httpNode;
credentialsStore.state.credentials = {
c8vqdPpPClh4TgIO: {
id: 'c8vqdPpPClh4TgIO',
name: 'OpenAi account',
type: 'openAiApi',
isManaged: false,
createdAt: '',
updatedAt: '',
},
};
renderComponent();
const credentialsSelect = screen.getByTestId('node-credentials-select');
await userEvent.click(credentialsSelect);
await userEvent.click(screen.getByTestId('node-credentials-select-item-new'));
expect(uiStore.openNewCredential).toHaveBeenCalledWith('openAiApi', true);
});
});

View file

@ -2,11 +2,12 @@
import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
import {
HTTP_REQUEST_NODE_TYPE,
type ICredentialType,
type INodeCredentialDescription,
type INodeCredentialsDetails,
type NodeParameterValueType,
} from 'n8n-workflow';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
@ -31,6 +32,7 @@ import {
updateNodeAuthType,
} from '@/utils/nodeTypesUtils';
import {
N8nIcon,
N8nInput,
N8nInputLabel,
N8nOption,
@ -67,7 +69,7 @@ const emit = defineEmits<{
const telemetry = useTelemetry();
const i18n = useI18n();
const NEW_CREDENTIALS_TEXT = `- ${i18n.baseText('nodeCredentials.createNew')} -`;
const NEW_CREDENTIALS_TEXT = i18n.baseText('nodeCredentials.createNew');
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
@ -79,7 +81,9 @@ const nodeHelpers = useNodeHelpers();
const toast = useToast();
const subscribedToCredentialType = ref('');
const filter = ref('');
const listeningForAuthChange = ref(false);
const selectRefs = ref<Array<InstanceType<typeof N8nSelect>>>([]);
const credentialTypesNode = computed(() =>
credentialTypesNodeDescription.value.map(
@ -344,9 +348,8 @@ function onCredentialSelected(
credentialId: string | null | undefined,
showAuthOptions = false,
) {
const newCredentialOptionSelected = credentialId === NEW_CREDENTIALS_TEXT;
if (!credentialId || newCredentialOptionSelected) {
createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
if (!credentialId) {
createNewCredential(credentialType, false, showAuthOptions);
return;
}
@ -501,6 +504,20 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
}
return i18n.baseText('nodeCredentials.credentialsLabel');
}
function setFilter(newFilter = '') {
filter.value = newFilter;
}
function matches(needle: string, haystack: string) {
return haystack.toLocaleLowerCase().includes(needle);
}
async function onClickCreateCredential(type: ICredentialType | INodeCredentialDescription) {
selectRefs.value.forEach((select) => select.blur());
await nextTick();
createNewCredential(type.name, true, showMixedCredentials(type));
}
</script>
<template>
@ -530,16 +547,20 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
data-test-id="node-credentials-select"
>
<N8nSelect
ref="selectRefs"
:model-value="getSelectedId(type.name)"
:placeholder="getSelectPlaceholder(type.name, getIssues(type.name))"
size="small"
filterable
:filter-method="setFilter"
:popper-class="$style.selectPopper"
@update:model-value="
(value: string) => onCredentialSelected(type.name, value, showMixedCredentials(type))
"
@blur="emit('blur', 'credentials')"
>
<N8nOption
v-for="item in options"
v-for="item in options.filter((o) => matches(filter, o.name))"
:key="item.id"
:data-test-id="`node-credentials-select-item-${item.id}`"
:label="item.name"
@ -550,13 +571,17 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
<N8nText size="small">{{ item.typeDisplayName }}</N8nText>
</div>
</N8nOption>
<N8nOption
:key="NEW_CREDENTIALS_TEXT"
<template #empty> </template>
<template #footer>
<div
data-test-id="node-credentials-select-item-new"
:value="NEW_CREDENTIALS_TEXT"
:label="NEW_CREDENTIALS_TEXT"
:class="['clickable', $style.newCredential]"
@click="onClickCreateCredential(type)"
>
</N8nOption>
<N8nIcon size="xsmall" icon="plus" />
<N8nText bold>{{ NEW_CREDENTIALS_TEXT }}</N8nText>
</div>
</template>
</N8nSelect>
<div v-if="getIssues(type.name).length && !hideIssues" :class="$style.warning">
@ -567,7 +592,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
:items="getIssues(type.name)"
/>
</template>
<font-awesome-icon icon="exclamation-triangle" />
<N8nIcon icon="exclamation-triangle" />
</N8nTooltip>
</div>
@ -576,7 +601,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
:class="$style.edit"
data-test-id="credential-edit-button"
>
<font-awesome-icon
<N8nIcon
icon="pen"
class="clickable"
:title="i18n.baseText('nodeCredentials.updateCredential')"
@ -598,10 +623,25 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
}
}
.selectPopper {
:global(.el-select-dropdown__list) {
padding: 0;
}
:has(.newCredential:hover) :global(.hover) {
background-color: transparent;
}
&:not(:has(li)) .newCredential {
border-top: none;
box-shadow: none;
border-radius: var(--border-radius-base);
}
}
.warning {
min-width: 20px;
margin-left: 5px;
color: #ff8080;
margin-left: var(--spacing-4xs);
color: var(--color-danger-light);
font-size: var(--font-size-s);
}
@ -610,8 +650,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
justify-content: center;
align-items: center;
color: var(--color-text-base);
min-width: 20px;
margin-left: 5px;
margin-left: var(--spacing-3xs);
font-size: var(--font-size-s);
}
@ -629,4 +668,21 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
display: flex;
flex-direction: column;
}
.newCredential {
display: flex;
gap: var(--spacing-3xs);
align-items: center;
font-weight: var(--font-weight-bold);
padding: var(--spacing-xs) var(--spacing-m);
background-color: var(--color-background-light);
border-top: var(--border-base);
box-shadow: var(--box-shadow-light);
clip-path: inset(-12px 0 0 0); // Only show box shadow on top
&:hover {
color: var(--color-primary);
}
}
</style>

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import TitledList from '@/components/TitledList.vue';
import { useI18n } from '@/composables/useI18n';
import { N8nTooltip, N8nIcon } from 'n8n-design-system';
defineProps<{
issues: string[];
@ -11,22 +12,21 @@ const i18n = useI18n();
<template>
<div v-if="issues.length" :class="$style['parameter-issues']" data-test-id="parameter-issues">
<n8n-tooltip placement="top">
<N8nTooltip placement="top">
<template #content>
<TitledList :title="`${i18n.baseText('parameterInput.issues')}:`" :items="issues" />
</template>
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
<N8nIcon icon="exclamation-triangle" />
</N8nTooltip>
</div>
</template>
<style module lang="scss">
.parameter-issues {
width: 20px;
text-align: right;
float: right;
color: #ff8080;
color: var(--color-danger-light);
font-size: var(--font-size-s);
padding-left: var(--spacing-4xs);
padding-left: var(--spacing-3xs);
}
</style>

View file

@ -597,7 +597,6 @@
"credentialsList.confirmMessage.confirmButtonText": "Yes, delete",
"credentialsList.confirmMessage.headline": "Delete Credential?",
"credentialsList.confirmMessage.message": "Are you sure you want to delete {credentialName}?",
"credentialsList.createNewCredential": "Create New Credential",
"credentialsList.created": "Created",
"credentialsList.credentials": "Credentials",
"credentialsList.deleteCredential": "Delete Credential",
@ -1187,7 +1186,7 @@
"nodeCreator.aiPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
"nodeCreator.nodeItem.triggerIconTitle": "Trigger Node",
"nodeCreator.nodeItem.aiIconTitle": "LangChain AI Node",
"nodeCredentials.createNew": "Create New Credential",
"nodeCredentials.createNew": "Create new credential",
"nodeCredentials.credentialFor": "Credential for {credentialType}",
"nodeCredentials.credentialsLabel": "Credential to connect with",
"nodeCredentials.issues": "Issues",