diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 8ce3bc4080..71c3083856 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -26,6 +26,22 @@ const nodeDetailsView = new NDV(); const NEW_CREDENTIAL_NAME = 'Something else'; const NEW_CREDENTIAL_NAME2 = 'Something else entirely'; +function createNotionCredential() { + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME); + workflowPage.actions.openNode(NOTION_NODE_NAME); + workflowPage.getters.nodeCredentialsSelect().click(); + getVisibleSelect().find('li').last().click(); + credentialsModal.actions.fillCredentialsForm(); + cy.get('body').type('{esc}'); + workflowPage.actions.deleteNode(NOTION_NODE_NAME); +} + +function deleteSelectedCredential() { + workflowPage.getters.nodeCredentialsEditButton().click(); + credentialsModal.getters.deleteButton().click(); + cy.get('.el-message-box').find('button').contains('Yes').click(); +} + describe('Credentials', () => { beforeEach(() => { cy.visit(credentialsPage.url); @@ -229,6 +245,40 @@ describe('Credentials', () => { .should('have.value', NEW_CREDENTIAL_NAME); }); + it('should set a default credential when adding nodes', () => { + workflowPage.actions.visit(); + + createNotionCredential(); + + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); + workflowPage.getters + .nodeCredentialsSelect() + .find('input') + .should('have.value', NEW_NOTION_ACCOUNT_NAME); + + deleteSelectedCredential(); + }); + + it('should set a default credential when editing a node', () => { + workflowPage.actions.visit(); + + createNotionCredential(); + + workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); + nodeDetailsView.getters.parameterInput('authentication').click(); + getVisibleSelect().find('li').contains('Predefined').click(); + + nodeDetailsView.getters.parameterInput('nodeCredentialType').click(); + getVisibleSelect().find('li').contains('Notion API').click(); + + workflowPage.getters + .nodeCredentialsSelect() + .find('input') + .should('have.value', NEW_NOTION_ACCOUNT_NAME); + + deleteSelectedCredential(); + }); + it('should setup generic authentication for HTTP node', () => { workflowPage.actions.visit(); workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 34657284c3..a718b91757 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -37,6 +37,7 @@ import { N8nText, N8nTooltip, } from 'n8n-design-system'; +import { isEmpty } from '@/utils/typesUtils'; interface CredentialDropdownOption extends ICredentialsResponse { typeDisplayName: string; @@ -87,9 +88,9 @@ const credentialTypesNode = computed(() => ); const credentialTypesNodeDescriptionDisplayed = computed(() => - credentialTypesNodeDescription.value.filter((credentialTypeDescription) => - displayCredentials(credentialTypeDescription), - ), + credentialTypesNodeDescription.value + .filter((credentialTypeDescription) => displayCredentials(credentialTypeDescription)) + .map((type) => ({ type, options: getCredentialOptions(getAllRelatedCredentialTypes(type)) })), ); const credentialTypesNodeDescription = computed(() => { if (typeof props.overrideCredType !== 'string') return []; @@ -149,6 +150,27 @@ watch( { immediate: true, deep: true }, ); +// Select most recent credential by default +watch( + credentialTypesNodeDescriptionDisplayed, + (types) => { + if (types.length === 0 || !isEmpty(selected.value)) return; + + const allOptions = types.map((type) => type.options).flat(); + + if (allOptions.length === 0) return; + + const mostRecentCredential = allOptions.reduce( + (mostRecent, current) => + mostRecent && mostRecent.updatedAt > current.updatedAt ? mostRecent : current, + allOptions[0], + ); + + onCredentialSelected(mostRecentCredential.type, mostRecentCredential.id); + }, + { immediate: true }, +); + onMounted(() => { credentialsStore.$onAction(({ name, after, args }) => { const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential']; @@ -481,12 +503,9 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s v-if="credentialTypesNodeDescriptionDisplayed.length" :class="['node-credentials', $style.container]" > -
+
-
+
@@ -567,10 +567,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
@@ -578,7 +575,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s icon="pen" class="clickable" :title="$locale.baseText('nodeCredentials.updateCredential')" - @click="editCredential(credentialTypeDescription.name)" + @click="editCredential(type.name)" />
diff --git a/packages/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/editor-ui/src/composables/useCanvasOperations.test.ts index 9b6a819238..b3c9c06b15 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.test.ts @@ -191,36 +191,6 @@ describe('useCanvasOperations', () => { expect(result.position).toEqual([20, 20]); }); - it('should create node with default credentials when only one credential is available', () => { - const credentialsStore = useCredentialsStore(); - const credential = mock({ id: '1', name: 'cred', type: 'cred' }); - const nodeTypeName = 'type'; - const nodeTypeDescription = mockNodeTypeDescription({ - name: nodeTypeName, - credentials: [{ name: credential.name }], - }); - - credentialsStore.state.credentials = { - [credential.id]: credential, - }; - - // @ts-expect-error Known pinia issue when spying on store getters - vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [ - credential, - ]); - - const { addNode } = useCanvasOperations({ router }); - const result = addNode( - { - type: nodeTypeName, - typeVersion: 1, - }, - nodeTypeDescription, - ); - - expect(result.credentials).toEqual({ [credential.name]: { id: '1', name: credential.name } }); - }); - it('should not assign credentials when multiple credentials are available', () => { const credentialsStore = useCredentialsStore(); const credentialA = mock({ id: '1', name: 'credA', type: 'cred' }); diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index 2d811a539e..a27c4f99db 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -777,7 +777,6 @@ export function useCanvasOperations({ router }: { router: ReturnType credentialsStore.getUsableCredentialByType(type.name)) - .flat(); - - if (credentialPerType?.length === 1) { - const defaultCredential = credentialPerType[0]; - - const selectedCredentials = credentialsStore.getCredentialById(defaultCredential.id); - const selected = { id: selectedCredentials.id, name: selectedCredentials.name }; - const credentials = { - [defaultCredential.type]: selected, - }; - - if (nodeTypeDescription.credentials) { - const authentication = nodeTypeDescription.credentials.find( - (type) => type.name === defaultCredential.type, - ); - - const authDisplayOptionsHide = authentication?.displayOptions?.hide; - const authDisplayOptionsShow = authentication?.displayOptions?.show; - - if (!authDisplayOptionsHide) { - if (!authDisplayOptionsShow) { - node.credentials = credentials; - } else if ( - Object.keys(authDisplayOptionsShow).length === 1 && - authDisplayOptionsShow.authentication - ) { - // ignore complex case when there's multiple dependencies - node.credentials = credentials; - - let parameters: { [key: string]: string } = {}; - for (const displayOption of Object.keys(authDisplayOptionsShow)) { - if (node.parameters && !node.parameters[displayOption]) { - parameters = {}; - node.credentials = undefined; - break; - } - const optionValue = authDisplayOptionsShow[displayOption]?.[0]; - if (optionValue && typeof optionValue === 'string') { - parameters[displayOption] = optionValue; - } - node.parameters = { - ...node.parameters, - ...parameters, - }; - } - } - } - } - } - } - function resolveNodePosition( node: Omit & { position?: INodeUi['position'] }, nodeTypeDescription: INodeTypeDescription,