diff --git a/cypress/constants.ts b/cypress/constants.ts index 55aaf319a9..26740bfb8b 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -7,5 +7,15 @@ export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; export const SET_NODE_NAME = 'Set'; +export const GMAIL_NODE_NAME = 'Gmail'; +export const TRELLO_NODE_NAME = 'Trello'; +export const NOTION_NODE_NAME = 'Notion'; +export const PIPEDRIVE_NODE_NAME = 'Pipedrive'; +export const HTTP_REQUEST_NODE_NAME = 'HTTP Request'; export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}'; + +export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account'; +export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account'; +export const NEW_NOTION_ACCOUNT_NAME = 'Notion account'; +export const NEW_QUERY_AUTH_ACCOUNT_NAME = 'Query Auth account'; diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 66daf406d4..4444f6f9a9 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -1,6 +1,9 @@ -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; +import { HTTP_REQUEST_NODE_TYPE } from './../../packages/editor-ui/src/constants'; +import { NEW_NOTION_ACCOUNT_NAME, NOTION_NODE_NAME, PIPEDRIVE_NODE_NAME, HTTP_REQUEST_NODE_NAME, NEW_QUERY_AUTH_ACCOUNT_NAME } from './../constants'; +import { visit } from 'recast'; +import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD, GMAIL_NODE_NAME, NEW_GOOGLE_ACCOUNT_NAME, NEW_TRELLO_ACCOUNT_NAME, SCHEDULE_TRIGGER_NODE_NAME, TRELLO_NODE_NAME } from '../constants'; import { randFirstName, randLastName } from '@ngneat/falso'; -import { CredentialsPage, CredentialsModal } from '../pages'; +import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages'; const email = DEFAULT_USER_EMAIL; const password = DEFAULT_USER_PASSWORD; @@ -8,6 +11,10 @@ const firstName = randFirstName(); const lastName = randLastName(); const credentialsPage = new CredentialsPage(); const credentialsModal = new CredentialsModal(); +const workflowPage = new WorkflowPage(); +const nodeDetailsView = new NDV(); + +const NEW_CREDENTIAL_NAME = 'Something else'; describe('Credentials', () => { before(() => { @@ -83,4 +90,149 @@ describe('Credentials', () => { credentialsPage.getters.credentialCards().eq(0).should('contain.text', 'Notion'); credentialsPage.actions.sortBy('nameAsc'); }); + + it('should create credentials from NDV for node with multiple auth options', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(GMAIL_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); + credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); + credentialsModal.actions.fillCredentialsForm(); + cy.get('.el-message-box').find('button').contains('Close').click(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_GOOGLE_ACCOUNT_NAME); + }) + + it('should show multiple credential types in the same dropdown', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(GMAIL_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + workflowPage.getters.nodeCredentialsSelect().click(); + // Add oAuth credentials + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); + credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); + credentialsModal.actions.fillCredentialsForm(); + cy.get('.el-message-box').find('button').contains('Close').click(); + workflowPage.getters.nodeCredentialsSelect().click(); + // Add Service account credentials + workflowPage.getters.nodeCredentialsSelect().find('li').last().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().find('li').should('have.length.greaterThan', 3); + }); + + it('should correctly render required and optional credentials', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true); + cy.get('body').type('{downArrow}'); + cy.get('body').type('{enter}'); + // Select incoming authentication + nodeDetailsView.getters.parameterInput('incomingAuthentication').should('exist'); + nodeDetailsView.getters.parameterInput('incomingAuthentication').click(); + nodeDetailsView.getters.parameterInput('incomingAuthentication').find('li').first().click(); + // There should be two credential fields + workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); + + workflowPage.getters.nodeCredentialsSelect().first().click(); + workflowPage.getters.nodeCredentialsSelect().first().find('li').last().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(); + workflowPage.getters.nodeCredentialsSelect().last().find('li').last().click(); + // This one should not show auth type selector + credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); + }); + + it('should create credentials from NDV for node with no auth options', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(TRELLO_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); + credentialsModal.actions.fillCredentialsForm(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_TRELLO_ACCOUNT_NAME); + }); + + it('should delete credentials from NDV', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.actions.fillCredentialsForm(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_NOTION_ACCOUNT_NAME); + + workflowPage.getters.nodeCredentialsEditButton().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.getters.deleteButton().click(); + cy.get('.el-message-box').find('button').contains('Yes').click(); + workflowPage.getters.successToast().contains('Credential deleted'); + workflowPage.getters.nodeCredentialsSelect().should('not.contain', NEW_TRELLO_ACCOUNT_NAME); + }); + + it('should rename credentials from NDV', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(TRELLO_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.actions.fillCredentialsForm(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_TRELLO_ACCOUNT_NAME); + + workflowPage.getters.nodeCredentialsEditButton().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.getters.name().click(); + credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); + credentialsModal.getters.saveButton().click(); + credentialsModal.getters.closeButton().click(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_CREDENTIAL_NAME); + }); + + it('should setup generic authentication for HTTP node', () => { + workflowPage.actions.visit(); + cy.waitForLoad(); + workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME); + workflowPage.getters.canvasNodes().last().click(); + cy.get('body').type('{enter}'); + nodeDetailsView.getters.parameterInput('authentication').click(); + nodeDetailsView.getters.parameterInput('authentication').find('li').should('have.length', 3); + nodeDetailsView.getters.parameterInput('authentication').find('li').last().click(); + nodeDetailsView.getters.parameterInput('genericAuthType').should('exist'); + nodeDetailsView.getters.parameterInput('genericAuthType').click(); + nodeDetailsView.getters.parameterInput('genericAuthType').find('li').should('have.length.greaterThan', 0); + nodeDetailsView.getters.parameterInput('genericAuthType').find('li').last().click(); + + workflowPage.getters.nodeCredentialsSelect().should('exist'); + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.actions.fillCredentialsForm(); + workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_QUERY_AUTH_ACCOUNT_NAME); + }); }); diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 4de09b7183..a0a0d1cafa 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -19,7 +19,12 @@ export class CredentialsModal extends BasePage { nameInput: () => cy.getByTestId('credential-name').find('input'), // Saving of the credentials takes a while on the CI so we need to increase the timeout saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }), + deleteButton: () => cy.getByTestId('credential-delete-button'), closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(), + credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'), + credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'), + credentialAuthTypeRadioButtons: () => this.getters.credentialsAuthTypeSelector().find('label[role=radio]'), + credentialInputs: () => cy.getByTestId('credential-connection-parameter'), }; actions = { setName: (name: string) => { @@ -41,5 +46,19 @@ export class CredentialsModal extends BasePage { close: () => { this.getters.closeButton().click(); }, + fillCredentialsForm: () => { + this.getters.credentialsEditModal().should('be.visible'); + this.getters.credentialInputs().should('have.length.greaterThan', 0); + this.getters.credentialInputs().find('input[type=text], input[type=password]').each(($el) => { + cy.wrap($el).type('test'); + }); + this.getters.saveButton().click(); + this.getters.closeButton().click(); + }, + renameCredential: (newName: string) => { + this.getters.nameInput().type('{selectall}'); + this.getters.nameInput().type(newName); + this.getters.nameInput().type('{enter}'); + } }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 1b5a3a067a..98f408e0ed 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -75,6 +75,11 @@ export class WorkflowPage extends BasePage { zoomOutButton: () => cy.getByTestId('zoom-out-button'), resetZoomButton: () => cy.getByTestId('reset-zoom-button'), executeWorkflowButton: () => cy.getByTestId('execute-workflow-button'), + nodeCredentialsSelect: () => cy.getByTestId('node-credentials-select'), + nodeCredentialsEditButton: () => cy.getByTestId('credential-edit-button'), + nodeCreatorItems: () => cy.getByTestId('item-iterator-item'), + ndvParameters: () => cy.getByTestId('parameter-item'), + nodeCredentialsLabel: () => cy.getByTestId('credentials-label'), }; actions = { visit: () => { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 66d207ee42..260820b8f6 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1,3 +1,4 @@ +import { CREDENTIAL_EDIT_MODAL_KEY } from './constants'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { IMenuItem } from 'n8n-design-system'; import { @@ -37,6 +38,7 @@ import { INodeListSearchItems, NodeParameterValueType, INodeActionTypeDescription, + IDisplayOptions, IAbstractEventMessage, } from 'n8n-workflow'; import { FAKE_DOOR_FEATURES } from './constants'; @@ -1093,14 +1095,26 @@ export interface ITagsState { fetchedUsageCount: boolean; } -export interface IModalState { +export type Modals = + | { + [key: string]: ModalState; + } + | { + [CREDENTIAL_EDIT_MODAL_KEY]: NewCredentialsModal; + }; + +export type ModalState = { open: boolean; mode?: string | null; data?: Record; activeId?: string | null; curlCommand?: string; httpNodeParameters?: string; -} +}; + +export type NewCredentialsModal = ModalState & { + showAuthSelector?: boolean; +}; export type IRunDataDisplayMode = 'table' | 'json' | 'binary' | 'schema' | 'html'; export type NodePanelType = 'input' | 'output'; @@ -1148,28 +1162,12 @@ export interface NDVState { }; } -export interface IUiState { - sidebarMenuCollapsed: boolean; - modalStack: string[]; - modals: { - [key: string]: IModalState; - }; - isPageLoading: boolean; - currentView: string; - fakeDoorFeatures: IFakeDoor[]; - nodeViewInitialized: boolean; - addFirstStepOnLoad: boolean; - executionSidebarAutoRefresh: boolean; -} - export interface UIState { activeActions: string[]; activeCredentialType: string | null; sidebarMenuCollapsed: boolean; modalStack: string[]; - modals: { - [key: string]: IModalState; - }; + modals: Modals; isPageLoading: boolean; currentView: string; mainPanelPosition: number; @@ -1463,3 +1461,9 @@ export type UsageState = { managementToken?: string; }; }; + +export type NodeAuthenticationOption = { + name: string; + value: string; + displayOptions?: IDisplayOptions; +}; diff --git a/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue b/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue new file mode 100644 index 0000000000..e75ee0e906 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index 86d70afa95..a3f22a23e2 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -1,5 +1,5 @@ diff --git a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue index 8052704af5..ad6d2dfaa7 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue @@ -227,6 +227,8 @@ const { setAddedNodeActionParameters, } = useNodeCreatorStore(); +const { getNodeType } = useNodeTypesStore(); + const telemetry = instance?.proxy.$telemetry; const { categorizedItems: allNodes, isTriggerNode } = useNodeTypesStore(); const containsAPIAction = computed( @@ -368,18 +370,26 @@ function onActionSelected(actionCreateElement: INodeCreateElement) { setAddedNodeActionParameters(actionUpdateData, telemetry); } function addHttpNode() { + const app_identifier = state.activeNodeActions?.name; + let nodeCredentialType = ''; + const nodeType = app_identifier ? getNodeType(app_identifier) : null; + + if (nodeType && nodeType.credentials && nodeType.credentials.length > 0) { + nodeCredentialType = nodeType.credentials[0].name; + } + const updateData = { name: '', key: HTTP_REQUEST_NODE_TYPE, value: { authentication: 'predefinedCredentialType', + nodeCredentialType, }, } as IUpdateInformation; emit('nodeTypeSelected', [MANUAL_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]); setAddedNodeActionParameters(updateData, telemetry, false); - const app_identifier = state.activeNodeActions?.name; $externalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); } diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 58e34d5b85..1f7b551d4b 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -8,17 +8,12 @@ :key="credentialTypeDescription.name" >
-
+
+
+ {{ item.name }} + {{ item.typeDisplayName }} +
{ + const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential']; + const credentialType = this.subscribedToCredentialType; + if (!credentialType) { + return; + } + + after(async (result) => { + if (!listeningForActions.includes(name)) { + return; + } + const current = this.selected[credentialType]; + let credentialsOfType: ICredentialsResponse[] = []; + if (this.showAll) { + if (this.node) { + credentialsOfType = [ + ...(this.credentialsStore.allUsableCredentialsForNode(this.node) || []), + ]; + } + } else { + credentialsOfType = [ + ...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []), + ]; + } + switch (name) { + // new credential was added + case 'createNewCredential': + if (result) { + this.onCredentialSelected(credentialType, (result as ICredentialsResponse).id); + } + break; + case 'updateCredential': + const updatedCredential = result as ICredentialsResponse; + // credential name was changed, update it + if (updatedCredential.name !== current.name) { + this.onCredentialSelected(credentialType, current.id); + } + break; + case 'deleteCredential': + // all credentials were deleted + if (credentialsOfType.length === 0) { + this.clearSelectedCredential(credentialType); + } else { + const id = args[0].id; + // credential was deleted, select last one added to replace with + if (current.id === id) { + this.onCredentialSelected( + credentialType, + credentialsOfType[credentialsOfType.length - 1].id, + ); + } + } + break; + } + }); + }); + }, + watch: { + 'node.parameters': { + immediate: true, + deep: true, + handler(newValue: INodeParameters, oldValue: INodeParameters) { + // When active node parameters change, check if authentication type has been changed + // and set `subscribedToCredentialType` to corresponding credential type + const isActive = this.node.name === this.ndvStore.activeNode?.name; + const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); + // Only do this for active node and if it's listening for auth change + if (isActive && nodeType && this.listeningForAuthChange) { + if (this.mainNodeAuthField && oldValue && newValue) { + const newAuth = newValue[this.mainNodeAuthField.name]; + + if (newAuth) { + const credentialType = getNodeCredentialForSelectedAuthType( + nodeType, + newAuth.toString(), + ); + if (credentialType) { + this.subscribedToCredentialType = credentialType.name; + } + } + } + } + }, + }, }, computed: { ...mapStores( useCredentialsStore, useNodeTypesStore, + useNDVStore, useUIStore, useUsersStore, useWorkflowsStore, @@ -189,11 +312,39 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( selected(): { [type: string]: INodeCredentialsDetails } { return this.node.credentials || {}; }, + nodeType(): INodeTypeDescription | null { + return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); + }, + mainNodeAuthField(): INodeProperties | null { + return getMainAuthField(this.nodeType); + }, }, methods: { - getCredentialOptions(type: string): ICredentialsResponse[] { - return this.credentialsStore.allUsableCredentialsByType[type]; + getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] { + const isRequiredCredential = this.showMixedCredentials(credentialType); + if (isRequiredCredential) { + if (this.mainNodeAuthField) { + const credentials = getAllNodeCredentialForAuthType( + this.nodeType, + this.mainNodeAuthField.name, + ); + return credentials.map((cred) => cred.name); + } + } + return [credentialType.name]; + }, + getCredentialOptions(types: string[]): CredentialDropdownOption[] { + let options: CredentialDropdownOption[] = []; + types.forEach((type) => { + options = options.concat( + this.credentialsStore.allUsableCredentialsByType[type].map((option: any) => ({ + ...option, + typeDisplayName: this.credentialsStore.getCredentialTypeByName(type).displayName, + })), + ); + }); + return options; }, getSelectedId(type: string) { if (this.isCredentialExisting(type)) { @@ -226,70 +377,6 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( return styles; }, - // Listen for credentials store changes so credential selection can be updated if creds are changed from the modal - listenForCredentialUpdates() { - const getCounts = () => { - return Object.keys(this.credentialsStore.allUsableCredentialsByType).reduce( - (counts: { [key: string]: number }, key: string) => { - counts[key] = this.credentialsStore.allUsableCredentialsByType[key].length; - return counts; - }, - {}, - ); - }; - - let previousCredentialCounts = getCounts(); - const onCredentialMutation = () => { - // This data pro stores credential type that the component is currently interested in - const credentialType = this.subscribedToCredentialType; - if (!credentialType) { - return; - } - - let credentialsOfType = [ - ...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []), - ]; - // all credentials were deleted - if (credentialsOfType.length === 0) { - this.clearSelectedCredential(credentialType); - return; - } - - credentialsOfType = credentialsOfType.sort((a, b) => (a.id < b.id ? -1 : 1)); - const previousCredsOfType = previousCredentialCounts[credentialType] || 0; - const current = this.selected[credentialType]; - - // new credential was added - if (credentialsOfType.length > previousCredsOfType || !current) { - this.onCredentialSelected( - credentialType, - credentialsOfType[credentialsOfType.length - 1].id, - ); - return; - } - - const matchingCredential = credentialsOfType.find((cred) => cred.id === current.id); - // credential was deleted, select last one added to replace with - if (!matchingCredential) { - this.onCredentialSelected( - credentialType, - credentialsOfType[credentialsOfType.length - 1].id, - ); - return; - } - - // credential was updated - if (matchingCredential.name !== current.name) { - // credential name was changed, update it - this.onCredentialSelected(credentialType, current.id); - } - }; - - this.credentialsStore.$subscribe((mutation, state) => { - onCredentialMutation(); - previousCredentialCounts = getCounts(); - }); - }, clearSelectedCredential(credentialType: string) { const node: INodeUi = this.node; @@ -310,12 +397,19 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( this.$emit('credentialSelected', updateInformation); }, - onCredentialSelected(credentialType: string, credentialId: string | null | undefined) { + onCredentialSelected( + credentialType: string, + credentialId: string | null | undefined, + requiredCredentials = false, + ) { if (credentialId === this.NEW_CREDENTIALS_TEXT) { + // If new credential dialog is open, start listening for auth type change which should happen in the modal + // this will be handled in this component's watcher which will set subscribed credential accordingly + this.listeningForAuthChange = true; this.subscribedToCredentialType = credentialType; } if (!credentialId || credentialId === this.NEW_CREDENTIALS_TEXT) { - this.uiStore.openNewCredential(credentialType); + this.uiStore.openNewCredential(credentialType, requiredCredentials); this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', @@ -334,9 +428,10 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( }); const selectedCredentials = this.credentialsStore.getCredentialById(credentialId); + const selectedCredentialsType = this.showAll ? selectedCredentials.type : credentialType; const oldCredentials = - this.node.credentials && this.node.credentials[credentialType] - ? this.node.credentials[credentialType] + this.node.credentials && this.node.credentials[selectedCredentialsType] + ? this.node.credentials[selectedCredentialsType] : {}; const selected = { id: selectedCredentials.id, name: selectedCredentials.name }; @@ -345,13 +440,16 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( if ( oldCredentials.id === null || (oldCredentials.id && - !this.credentialsStore.getCredentialByIdAndType(oldCredentials.id, credentialType)) + !this.credentialsStore.getCredentialByIdAndType( + oldCredentials.id, + selectedCredentialsType, + )) ) { // update all nodes in the workflow with the same old/invalid credentials this.workflowsStore.replaceInvalidWorkflowCredentials({ credentials: selected, invalid: oldCredentials, - type: credentialType, + type: selectedCredentialsType, }); this.updateNodesCredentialsIssues(); this.$showMessage({ @@ -366,11 +464,27 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( }); } + // If credential is selected from mixed credential dropdown, update node's auth filed based on selected credential + if (this.showAll && this.mainNodeAuthField) { + const nodeCredentialDescription = this.nodeType?.credentials?.find( + (cred) => cred.name === selectedCredentialsType, + ); + const authOption = getAuthTypeForNodeCredential(this.nodeType, nodeCredentialDescription); + if (authOption) { + updateNodeAuthType(this.node, authOption.value); + const parameterData = { + name: `parameters.${this.mainNodeAuthField.name}`, + value: authOption.value, + }; + this.$emit('valueChanged', parameterData); + } + } + const node: INodeUi = this.node; const credentials = { ...(node.credentials || {}), - [credentialType]: selected, + [selectedCredentialsType]: selected, }; const updateInformation: INodeUpdatePropertiesInformation = { @@ -401,7 +515,6 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( if (!node.issues.credentials.hasOwnProperty(credentialTypeName)) { return []; } - return node.issues.credentials[credentialTypeName]; }, @@ -414,7 +527,7 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( return false; } const { id } = this.node.credentials[credentialType]; - const options = this.getCredentialOptions(credentialType); + const options = this.getCredentialOptions([credentialType]); return !!options.find((option: ICredentialsResponse) => option.id === id); }, @@ -431,6 +544,24 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( }); this.subscribedToCredentialType = credentialType; }, + showMixedCredentials(credentialType: INodeCredentialDescription): boolean { + const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); + const isRequired = isRequiredCredential(nodeType, credentialType); + + return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired; + }, + getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string { + const credentialTypeName = this.credentialTypeNames[credentialType.name]; + + if (!this.showMixedCredentials(credentialType)) { + return this.$locale.baseText('nodeCredentials.credentialFor', { + interpolate: { + credentialType: credentialTypeName, + }, + }); + } + return this.$locale.baseText('nodeCredentials.credentialsLabel'); + }, }, }); @@ -470,4 +601,9 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend( composes: input; --input-border-color: var(--color-danger); } + +.credentialOption { + display: flex; + flex-direction: column; +} diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 83c665ea75..0e2fb76683 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -103,7 +103,9 @@ diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index fa4a2e971d..3236190635 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -1,6 +1,11 @@