diff --git a/cypress/e2e/5-workflow-actions.cy.ts b/cypress/e2e/5-workflow-actions.cy.ts deleted file mode 100644 index 20d6924801666..0000000000000 --- a/cypress/e2e/5-workflow-actions.cy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { randFirstName, randLastName } from "@ngneat/falso"; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from "../constants"; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; - -const NEW_WORKFLOW_NAME = 'Something else'; -const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; -const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; - -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); -const WorkflowPage = new WorkflowPageClass(); - -describe('Workflow Actions', () => { - before(() => { - cy.resetAll(); - cy.setup({ email, firstName, lastName, password }); - }); - - beforeEach(() => { - cy.on('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('Not logged in'); - - return false; - }) - - cy.signin({ email, password }); - - WorkflowPage.actions.visit(); - }); - - it('should be able to save on button click', () => { - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should save workflow on keyboard shortcut', () => { - WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should not be able to activate unsaved workflow', () => { - WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); - }); - - it('should not be able to activate workflow without trigger node', () => { - // Manual trigger is not enough to activate the workflow - WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); - }); - - it('should be able to activate workflow', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.actions.activateWorkflow(); - WorkflowPage.getters.isWorkflowActivated(); - }); - - it('should save new workflow after renaming', () => { - WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should rename workflow', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); - WorkflowPage.getters.isWorkflowSaved(); - WorkflowPage.getters.workflowNameInputContainer().invoke('attr', 'title').should('eq', NEW_WORKFLOW_NAME); - }); - -}); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts new file mode 100644 index 0000000000000..9891d6a387de7 --- /dev/null +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -0,0 +1,107 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const NEW_WORKFLOW_NAME = 'Something else'; +const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; +const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; +const CODE_NODE = 'Code' +const TEST_WF_TAGS = ['Tag 1', 'Tag 2', 'Tag 3']; + +const WorkflowPage = new WorkflowPageClass(); + +describe('Workflow Actions', () => { + beforeEach(() => { + cy.resetAll(); + cy.skipSetup(); + WorkflowPage.actions.visit(); + }); + + it('should be able to save on button click', () => { + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should save workflow on keyboard shortcut', () => { + WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should not be able to activate unsaved workflow', () => { + WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); + }); + + it('should not be able to activate workflow without trigger node', () => { + // Manual trigger is not enough to activate the workflow + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); + }); + + it('should be able to activate workflow', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.isWorkflowActivated(); + }); + + it('should save new workflow after renaming', () => { + WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should rename workflow', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); + WorkflowPage.getters.isWorkflowSaved(); + WorkflowPage.getters.workflowNameInputContainer().invoke('attr', 'title').should('eq', NEW_WORKFLOW_NAME); + }); + + it('should add tags', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.isWorkflowSaved(); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length); + }); + + it('should add more tags', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.actions.addTags(['Another one']); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length + 1); + }); + + it('should remove tags by clicking X in tag', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.getters.workflowTagsContainer().find('.el-tag__close').first().click(); + cy.get('body').type('{enter}'); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1); + }); + + it('should remove tags from dropdown', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.getters.workflowTagsDropdown().find('li').first().click(); + cy.get('body').type('{enter}'); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1); + }); + + it('should copy nodes', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE); + cy.get('body').type('{meta}', { release: false }).type('a'); + cy.get('body').type('{meta}', { release: false }).type('c'); + WorkflowPage.getters.successToast().should('exist'); + }); + + it('should paste nodes', () => { + cy.fixture('Test_workflow-actions_paste-data.json').then(data => { + cy.get('body').paste(JSON.stringify(data)); + WorkflowPage.getters.canvasNodes().should('have.have.length', 2); + }); + }); + +}); diff --git a/cypress/fixtures/Test_workflow-actions_paste-data.json b/cypress/fixtures/Test_workflow-actions_paste-data.json new file mode 100644 index 0000000000000..1846939545527 --- /dev/null +++ b/cypress/fixtures/Test_workflow-actions_paste-data.json @@ -0,0 +1,37 @@ +{ + "meta": { + "instanceId": "1a30c82b98a30444ad25bce513655a5e02be772d361403542c23172be6062f04" + }, + "nodes": [{ + "parameters": { + "rule": { + "interval": [{}] + } + }, + "id": "a898563b-d2a4-4b15-a979-366872e801b0", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [420, 260] + }, { + "parameters": { + "options": {} + }, + "id": "b9a13e3d-bfa5-4873-959f-fd3d67e380d9", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [640, 260] + }], + "connections": { + "Schedule Trigger": { + "main": [ + [{ + "node": "Set", + "type": "main", + "index": 0 + }] + ] + } + } +} diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index d42998c7dc03a..ed68f676856ce 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -3,12 +3,14 @@ import { BasePage } from './base'; export class WorkflowPage extends BasePage { url = '/workflow/new'; getters = { - workflowNameInputContainer: () => cy - .getByTestId('workflow-name-input', { timeout: 5000 }), + workflowNameInputContainer: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }), workflowNameInput: () => this.getters.workflowNameInputContainer().then(($el) => cy.wrap($el.find('input'))), workflowImportInput: () => cy.getByTestId('workflow-import-input'), workflowTags: () => cy.getByTestId('workflow-tags'), workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'), + workflowTagsInput: () => this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())), + workflowTagElements: () => this.getters.workflowTagsContainer().find('span.tags').children(), + workflowTagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'), newTagLink: () => cy.getByTestId('new-tag-link'), saveButton: () => cy.getByTestId('workflow-save-button'), nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'), @@ -29,6 +31,10 @@ export class WorkflowPage extends BasePage { isWorkflowActivated: () => this.getters.activatorSwitch().should('have.class', 'is-checked'), expressionModalInput: () => cy.getByTestId('expression-modal-input'), expressionModalOutput: () => cy.getByTestId('expression-modal-output'), + + nodeViewRoot: () => cy.getByTestId('node-view-root'), + copyPasteInput: () => cy.getByTestId('hidden-copy-paste'), + canvasNodes: () => cy.getByTestId('canvas-node'), }; actions = { visit: () => { @@ -86,10 +92,9 @@ export class WorkflowPage extends BasePage { cy.get('body').type('{enter}'); }, addTags: (tags: string[]) => { - this.getters.newTagLink().click(); tags.forEach(tag => { - cy.get('body').type(tag); - cy.get('body').type('{enter}'); + this.getters.workflowTagsInput().type(tag); + this.getters.workflowTagsInput().type('{enter}'); }); cy.get('body').type('{enter}'); }, diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7637577e0d925..8a3d135901a5f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -128,3 +128,15 @@ Cypress.Commands.add('resetAll', () => { Cypress.Commands.add('setupOwner', (payload) => { cy.task('setup-owner', payload); }); + +Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => { + // https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event + cy.wrap(selector).then($destination => { + const pasteEvent = Object.assign(new Event('paste', { bubbles: true, cancelable: true }), { + clipboardData: { + getData: () => pastePayload + } + }); + $destination[0].dispatchEvent(pasteEvent); + }); +}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index afb2dee0c1272..40d7c0840a4a2 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -24,6 +24,7 @@ declare global { setupOwner(payload: SetupPayload): void; skipSetup(): void; resetAll(): void; + paste(pastePayload: string): void, } } } diff --git a/packages/cli/src/credentials/oauth2Credential.api.ts b/packages/cli/src/credentials/oauth2Credential.api.ts index e3fb2c0d2e674..e3767a2330270 100644 --- a/packages/cli/src/credentials/oauth2Credential.api.ts +++ b/packages/cli/src/credentials/oauth2Credential.api.ts @@ -78,12 +78,14 @@ oauth2CredentialController.get( throw new ResponseHelper.InternalServerError((error as Error).message); } + const credentialType = (credential as unknown as ICredentialsEncrypted).type; + const mode: WorkflowExecuteMode = 'internal'; const timezone = config.getEnv('generic.timezone'); const credentialsHelper = new CredentialsHelper(encryptionKey); const decryptedDataOriginal = await credentialsHelper.getDecrypted( credential as INodeCredentialsDetails, - (credential as unknown as ICredentialsEncrypted).type, + credentialType, mode, timezone, true, @@ -91,13 +93,17 @@ oauth2CredentialController.get( // At some point in the past we saved hidden scopes to credentials (but shouldn't) // Delete scope before applying defaults to make sure new scopes are present on reconnect - if (decryptedDataOriginal?.scope) { + if ( + decryptedDataOriginal?.scope && + credentialType.includes('OAuth2') && + !['oAuth2Api'].includes(credentialType) + ) { delete decryptedDataOriginal.scope; } const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( decryptedDataOriginal, - (credential as unknown as ICredentialsEncrypted).type, + credentialType, mode, timezone, ); @@ -128,7 +134,7 @@ oauth2CredentialController.get( // Encrypt the data const credentials = new Credentials( credential as INodeCredentialsDetails, - (credential as unknown as ICredentialsEncrypted).type, + credentialType, (credential as unknown as ICredentialsEncrypted).nodesAccess, ); decryptedDataOriginal.csrfSecret = csrfSecret; diff --git a/packages/design-system/src/components/N8nCheckbox/Checkbox.vue b/packages/design-system/src/components/N8nCheckbox/Checkbox.vue index cb655ab8d7d48..06105faf4ce5b 100644 --- a/packages/design-system/src/components/N8nCheckbox/Checkbox.vue +++ b/packages/design-system/src/components/N8nCheckbox/Checkbox.vue @@ -1,6 +1,7 @@ @@ -58,6 +60,14 @@ export default Vue.extend({ onChange(event: Event) { this.$emit('input', event); }, + onLabelClick() { + const checkboxComponent = this.$refs.checkbox as ElCheckbox; + if (!checkboxComponent) { + return; + } + + (checkboxComponent.$el as HTMLElement).click(); + }, }, }); @@ -70,5 +80,9 @@ export default Vue.extend({ span { white-space: normal; } + + label { + cursor: pointer; + } } diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue index 4ba6974d5dd2d..e600a80cb5c7c 100644 --- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue +++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue @@ -1,5 +1,5 @@