diff --git a/.npmrc b/.npmrc index 0d9bdb6234..688ccc8857 100644 --- a/.npmrc +++ b/.npmrc @@ -7,4 +7,5 @@ prefer-workspace-packages = true link-workspace-packages = deep hoist = true shamefully-hoist = true +hoist-workspace-packages = false loglevel = warn diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a58cc333..451497cb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# [1.42.0](https://github.com/n8n-io/n8n/compare/n8n@1.41.0...n8n@1.42.0) (2024-05-15) + + +### Bug Fixes + +* **Code Node:** Bind helper methods to the correct context ([#9380](https://github.com/n8n-io/n8n/issues/9380)) ([82c8801](https://github.com/n8n-io/n8n/commit/82c8801f25446085bc8da5055d9932eed4321f47)) +* **Cortex Node:** Fix issue with analyzer response not working for file observables ([#9374](https://github.com/n8n-io/n8n/issues/9374)) ([ed22dcd](https://github.com/n8n-io/n8n/commit/ed22dcd88ac7f8433b9ed5dc2139d8779b0e1d4c)) +* **editor:** Render backticks as code segments in error view ([#9352](https://github.com/n8n-io/n8n/issues/9352)) ([4ed5850](https://github.com/n8n-io/n8n/commit/4ed585040b20c50919e2ec2252216639c85194cb)) +* **Mattermost Node:** Fix issue when fetching reactions ([#9375](https://github.com/n8n-io/n8n/issues/9375)) ([78e7c7a](https://github.com/n8n-io/n8n/commit/78e7c7a9da96a293262cea5304509261ad10020c)) + + +### Features + +* **AI Agent Node:** Implement Tool calling agent ([#9339](https://github.com/n8n-io/n8n/issues/9339)) ([677f534](https://github.com/n8n-io/n8n/commit/677f534661634c74340f50723e55e241570d5a56)) +* **core:** Allow using a custom certificates in docker containers ([#8705](https://github.com/n8n-io/n8n/issues/8705)) ([6059722](https://github.com/n8n-io/n8n/commit/6059722fbfeeca31addfc31ed287f79f40aaad18)) +* **core:** Node hints(warnings) system ([#8954](https://github.com/n8n-io/n8n/issues/8954)) ([da6088d](https://github.com/n8n-io/n8n/commit/da6088d0bbb952fcdf595a650e1e01b7b02a2b7e)) +* **core:** Node version available in expression ([#9350](https://github.com/n8n-io/n8n/issues/9350)) ([a00467c](https://github.com/n8n-io/n8n/commit/a00467c9fa57d740de9eccfcd136267bc9e9559d)) +* **editor:** Add examples for number & boolean, add new methods ([#9358](https://github.com/n8n-io/n8n/issues/9358)) ([7b45dc3](https://github.com/n8n-io/n8n/commit/7b45dc313f42317f894469c6aa8abecc55704e3a)) +* **editor:** Add examples for object and array expression methods ([#9360](https://github.com/n8n-io/n8n/issues/9360)) ([5293663](https://github.com/n8n-io/n8n/commit/52936633af9c71dff1957ee43a5eda48f7fc1bf1)) +* **editor:** Add item selector to expression output ([#9281](https://github.com/n8n-io/n8n/issues/9281)) ([dc5994b](https://github.com/n8n-io/n8n/commit/dc5994b18580b9326574c5208d9beaf01c746f33)) +* **editor:** Autocomplete info box: improve structure and add examples ([#9019](https://github.com/n8n-io/n8n/issues/9019)) ([c92c870](https://github.com/n8n-io/n8n/commit/c92c870c7335f4e2af63fa1c6bcfd086b2957ef8)) +* **editor:** Remove AI Error Debugging ([#9337](https://github.com/n8n-io/n8n/issues/9337)) ([cda062b](https://github.com/n8n-io/n8n/commit/cda062bde63bcbfdd599d0662ddbe89c27a71686)) +* **Slack Node:** Add block support for message updates ([#8925](https://github.com/n8n-io/n8n/issues/8925)) ([1081429](https://github.com/n8n-io/n8n/commit/1081429a4d0f7e2d1fc1841303448035b46e44d1)) + + +### Performance Improvements + +* Add tailwind to editor and design system ([#9032](https://github.com/n8n-io/n8n/issues/9032)) ([1c1e444](https://github.com/n8n-io/n8n/commit/1c1e4443f41dd39da8d5fa3951c8dffb0fbfce10)) + + + # [1.41.0](https://github.com/n8n-io/n8n/compare/n8n@1.40.0...n8n@1.41.0) (2024-05-08) diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts new file mode 100644 index 0000000000..dd25c3f20c --- /dev/null +++ b/cypress/composables/projects.ts @@ -0,0 +1,18 @@ +export const getHomeButton = () => cy.getByTestId('project-home-menu-item'); +export const getMenuItems = () => cy.getByTestId('project-menu-item'); +export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item'); +export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a'); +export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]'); +export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]'); +export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]'); +export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button'); +export const getProjectSettingsCancelButton = () => + cy.getByTestId('project-settings-cancel-button'); +export const getProjectSettingsDeleteButton = () => + cy.getByTestId('project-settings-delete-button'); +export const getProjectMembersSelect = () => cy.getByTestId('project-members-select'); + +export const addProjectMember = (email: string) => { + getProjectMembersSelect().click(); + getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click(); +}; diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 71f41250ec..7908e8d128 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -30,7 +30,7 @@ const workflowSharingModal = new WorkflowSharingModal(); const ndv = new NDV(); describe('Sharing', { disableAutoLogin: true }, () => { - before(() => cy.enableFeature('sharing', true)); + before(() => cy.enableFeature('sharing')); let workflowW2Url = ''; it('should create C1, W1, W2, share W1 with U3, as U2', () => { @@ -171,11 +171,11 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.get('input').should('not.have.length'); credentialsModal.actions.changeTab('Sharing'); cy.contains( - 'You can view this credential because you have permission to read and share', + 'Sharing a credential allows people to use it in their workflows. They cannot access credential details.', ).should('be.visible'); credentialsModal.getters.usersSelect().click(); - cy.getByTestId('user-email') + cy.getByTestId('project-sharing-info') .filter(':visible') .should('have.length', 3) .contains(INSTANCE_ADMIN.email) diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 1855bdb43b..98c0909b4d 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -501,7 +501,7 @@ describe('Execution', () => { workflowPage.getters.clearExecutionDataButton().should('be.visible'); - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.getters .canvasNodeByName('do something with them') @@ -525,7 +525,7 @@ describe('Execution', () => { workflowPage.getters.zoomToFitButton().click(); - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.getters .canvasNodeByName('If') @@ -545,7 +545,7 @@ describe('Execution', () => { workflowPage.getters.clearExecutionDataButton().should('be.visible'); - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.getters .canvasNodeByName('NoOp2') @@ -576,7 +576,7 @@ describe('Execution', () => { 'My test workflow', ); - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.executeWorkflowButton().click(); @@ -599,7 +599,7 @@ describe('Execution', () => { 'My test workflow', ); - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.executeWorkflowButton().click(); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 008758aef2..c4cdcb280b 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -254,8 +254,9 @@ describe('Credentials', () => { }); workflowPage.actions.visit(true); - workflowPage.actions.addNodeToCanvas('Slack'); - workflowPage.actions.openNode('Slack'); + workflowPage.actions.addNodeToCanvas('Manual'); + workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel'); + workflowPage.getters.nodeCredentialsSelect().should('exist'); workflowPage.getters.nodeCredentialsSelect().click(); getVisibleSelect().find('li').last().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index ce6a49fb99..c481f25128 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -4,7 +4,7 @@ const variablesPage = new VariablesPage(); describe('Variables', () => { it('should show the unlicensed action box when the feature is disabled', () => { - cy.disableFeature('variables', false); + cy.disableFeature('variables'); cy.visit(variablesPage.url); variablesPage.getters.unavailableResourcesList().should('be.visible'); @@ -18,14 +18,15 @@ describe('Variables', () => { beforeEach(() => { cy.intercept('GET', '/rest/variables').as('loadVariables'); + cy.intercept('GET', '/rest/login').as('login'); cy.visit(variablesPage.url); - cy.wait(['@loadVariables', '@loadSettings']); + cy.wait(['@loadVariables', '@loadSettings', '@login']); }); it('should show the licensed action box when the feature is enabled', () => { variablesPage.getters.emptyResourcesList().should('be.visible'); - variablesPage.getters.createVariableButton().should('be.visible'); + variablesPage.getters.emptyResourcesListNewVariableButton().should('be.visible'); }); it('should create a new variable using empty state row', () => { diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index 955d33ce28..71c733c254 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -19,7 +19,7 @@ describe('Debug', () => { it('should be able to debug executions', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions/*').as('getExecution'); - cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun'); cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index 34762b12fc..d5f0a67f7e 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -10,7 +10,7 @@ describe('Workflow templates', () => { beforeEach(() => { cy.intercept('GET', '**/rest/settings', (req) => { // Disable cache - delete req.headers['if-none-match'] + delete req.headers['if-none-match']; req.reply((res) => { if (res.body.data) { // Disable custom templates host if it has been overridden by another intercept @@ -22,18 +22,27 @@ describe('Workflow templates', () => { it('Opens website when clicking templates sidebar link', () => { cy.visit(workflowsPage.url); - mainSidebar.getters.menuItem('Templates').should('be.visible'); + mainSidebar.getters.templates().should('be.visible'); // Templates should be a link to the website - mainSidebar.getters.templates().parent('a').should('have.attr', 'href').and('include', 'https://n8n.io/workflows'); + mainSidebar.getters + .templates() + .parent('a') + .should('have.attr', 'href') + .and('include', 'https://n8n.io/workflows'); // Link should contain instance address and n8n version - mainSidebar.getters.templates().parent('a').then(($a) => { - const href = $a.attr('href'); - const params = new URLSearchParams(href); - // Link should have all mandatory parameters expected on the website - expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(window.location.origin); - expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); - expect(params.get('utm_awc')).to.match(/[0-9]+/); - }); + mainSidebar.getters + .templates() + .parent('a') + .then(($a) => { + const href = $a.attr('href'); + const params = new URLSearchParams(href); + // Link should have all mandatory parameters expected on the website + expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include( + window.location.origin, + ); + expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); + expect(params.get('utm_awc')).to.match(/[0-9]+/); + }); mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank'); }); @@ -41,6 +50,6 @@ describe('Workflow templates', () => { cy.visit(templatesPage.url); cy.origin('https://n8n.io', () => { cy.url().should('include', 'https://n8n.io/workflows'); - }) + }); }); }); diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index 727078e735..a502d3577c 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -148,7 +148,7 @@ describe('Editor actions should work', () => { it('after switching between Editor and Debug', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions/*').as('getExecution'); - cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun'); editWorkflowAndDeactivate(); workflowPage.actions.executeWorkflow(); @@ -196,9 +196,9 @@ describe('Editor zoom should work after route changes', () => { cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion'); cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory'); cy.intercept('GET', '/rest/users').as('getUsers'); - cy.intercept('GET', '/rest/workflows').as('getWorkflows'); + cy.intercept('GET', '/rest/workflows?*').as('getWorkflows'); cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows'); - cy.intercept('GET', '/rest/credentials').as('getCredentials'); + cy.intercept('GET', '/rest/credentials?*').as('getCredentials'); switchBetweenEditorAndHistory(); zoomInAndCheckNodes(); diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts new file mode 100644 index 0000000000..5cf1ac1fdc --- /dev/null +++ b/cypress/e2e/39-projects.cy.ts @@ -0,0 +1,151 @@ +import { INSTANCE_ADMIN, INSTANCE_MEMBERS } from '../constants'; +import { WorkflowsPage, WorkflowPage, CredentialsModal, CredentialsPage } from '../pages'; +import * as projects from '../composables/projects'; + +const workflowsPage = new WorkflowsPage(); +const workflowPage = new WorkflowPage(); +const credentialsPage = new CredentialsPage(); +const credentialsModal = new CredentialsModal(); + +describe('Projects', () => { + beforeEach(() => { + cy.resetDatabase(); + cy.enableFeature('advancedPermissions'); + cy.enableFeature('projectRole:admin'); + cy.enableFeature('projectRole:editor'); + cy.changeQuota('maxTeamProjects', -1); + }); + + it('should handle workflows and credentials', () => { + cy.signin(INSTANCE_ADMIN); + cy.visit(workflowsPage.url); + workflowsPage.getters.workflowCards().should('not.have.length'); + + workflowsPage.getters.newWorkflowButtonCard().click(); + + cy.intercept('POST', '/rest/workflows').as('workflowSave'); + workflowPage.actions.saveWorkflowOnButtonClick(); + + cy.wait('@workflowSave').then((interception) => { + expect(interception.request.body).not.to.have.property('projectId'); + }); + + projects.getHomeButton().click(); + projects.getProjectTabs().should('have.length', 2); + + projects.getProjectTabCredentials().click(); + credentialsPage.getters.credentialCards().should('not.have.length'); + + credentialsPage.getters.emptyListCreateCredentialButton().click(); + credentialsModal.getters.newCredentialModal().should('be.visible'); + credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); + credentialsModal.getters.newCredentialTypeOption('Notion API').click(); + credentialsModal.getters.newCredentialTypeButton().click(); + credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); + credentialsModal.actions.setName('My awesome Notion account'); + + cy.intercept('POST', '/rest/credentials').as('credentialSave'); + credentialsModal.actions.save(); + cy.wait('@credentialSave').then((interception) => { + expect(interception.request.body).not.to.have.property('projectId'); + }); + + credentialsModal.actions.close(); + credentialsPage.getters.credentialCards().should('have.length', 1); + + projects.getProjectTabWorkflows().click(); + workflowsPage.getters.workflowCards().should('have.length', 1); + + projects.getMenuItems().should('not.have.length'); + + cy.intercept('POST', '/rest/projects').as('projectCreate'); + projects.getAddProjectButton().click(); + cy.wait('@projectCreate'); + projects.getMenuItems().should('have.length', 1); + projects.getProjectTabs().should('have.length', 3); + + cy.get('input[name="name"]').type('Development'); + projects.addProjectMember(INSTANCE_MEMBERS[0].email); + + cy.intercept('PATCH', '/rest/projects/*').as('projectSettingsSave'); + projects.getProjectSettingsSaveButton().click(); + cy.wait('@projectSettingsSave').then((interception) => { + expect(interception.request.body).to.have.property('name').and.to.equal('Development'); + expect(interception.request.body).to.have.property('relations').to.have.lengthOf(2); + }); + + projects.getMenuItems().first().click(); + workflowsPage.getters.workflowCards().should('not.have.length'); + projects.getProjectTabs().should('have.length', 3); + + workflowsPage.getters.newWorkflowButtonCard().click(); + + cy.intercept('POST', '/rest/workflows').as('workflowSave'); + workflowPage.actions.saveWorkflowOnButtonClick(); + + cy.wait('@workflowSave').then((interception) => { + expect(interception.request.body).to.have.property('projectId'); + }); + + projects.getMenuItems().first().click(); + + projects.getProjectTabCredentials().click(); + credentialsPage.getters.credentialCards().should('not.have.length'); + + credentialsPage.getters.emptyListCreateCredentialButton().click(); + credentialsModal.getters.newCredentialModal().should('be.visible'); + credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); + credentialsModal.getters.newCredentialTypeOption('Notion API').click(); + credentialsModal.getters.newCredentialTypeButton().click(); + credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); + credentialsModal.actions.setName('My awesome Notion account'); + + cy.intercept('POST', '/rest/credentials').as('credentialSave'); + credentialsModal.actions.save(); + cy.wait('@credentialSave').then((interception) => { + expect(interception.request.body).to.have.property('projectId'); + }); + credentialsModal.actions.close(); + + projects.getAddProjectButton().click(); + projects.getMenuItems().should('have.length', 2); + + let projectId: string; + projects.getMenuItems().first().click(); + cy.intercept('GET', '/rest/credentials*').as('credentialsList'); + projects.getProjectTabCredentials().click(); + cy.wait('@credentialsList').then((interception) => { + const url = new URL(interception.request.url); + const queryParams = new URLSearchParams(url.search); + const filter = queryParams.get('filter'); + expect(filter).to.be.a('string').and.to.contain('projectId'); + + if (filter) { + projectId = JSON.parse(filter).projectId; + } + }); + + projects.getMenuItems().last().click(); + cy.intercept('GET', '/rest/credentials*').as('credentialsList'); + projects.getProjectTabCredentials().click(); + cy.wait('@credentialsList').then((interception) => { + const url = new URL(interception.request.url); + const queryParams = new URLSearchParams(url.search); + const filter = queryParams.get('filter'); + expect(filter).to.be.a('string').and.to.contain('projectId'); + + if (filter) { + expect(JSON.parse(filter).projectId).not.to.equal(projectId); + } + }); + + projects.getHomeButton().click(); + workflowsPage.getters.workflowCards().should('have.length', 2); + + cy.intercept('GET', '/rest/credentials*').as('credentialsList'); + projects.getProjectTabCredentials().click(); + cy.wait('@credentialsList').then((interception) => { + expect(interception.request.url).not.to.contain('filter'); + }); + }); +}); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index cdac202f48..6513a80cb6 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -697,7 +697,7 @@ describe('NDV', () => { }); it('Stop listening for trigger event from NDV', () => { - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', { keepNdvOpen: true, action: 'On Changes To A Specific File', diff --git a/cypress/fixtures/Multiple_trigger_node_rerun.json b/cypress/fixtures/Multiple_trigger_node_rerun.json index 39d231a894..f956be3742 100644 --- a/cypress/fixtures/Multiple_trigger_node_rerun.json +++ b/cypress/fixtures/Multiple_trigger_node_rerun.json @@ -14,7 +14,7 @@ }, { "parameters": { - "url": "https://random-data-api.com/api/v2/users?size=5", + "url": "https://internal.users.n8n.cloud/webhook/random-data-api", "options": {} }, "id": "22511d75-ab54-49e1-b8af-08b8b3372373", @@ -28,7 +28,7 @@ }, { "parameters": { - "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();" + "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.firstname,\n firstnNameReversed: item.json.firstname.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();" }, "id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21", "name": "do something with them", @@ -130,4 +130,4 @@ }, "id": "PymcwIrbqgNh3O0K", "tags": [] -} \ No newline at end of file +} diff --git a/cypress/pages/credentials.ts b/cypress/pages/credentials.ts index 24ec88565d..7ae2d0f3b4 100644 --- a/cypress/pages/credentials.ts +++ b/cypress/pages/credentials.ts @@ -1,7 +1,7 @@ import { BasePage } from './base'; export class CredentialsPage extends BasePage { - url = '/credentials'; + url = '/home/credentials'; getters = { emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'), createCredentialButton: () => cy.getByTestId('resources-list-add'), diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 08a258a057..2275ea5e4c 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -25,7 +25,7 @@ export class CredentialsModal extends BasePage { credentialInputs: () => cy.getByTestId('credential-connection-parameter'), menu: () => this.getters.editCredentialModal().get('.menu-container'), menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name), - usersSelect: () => cy.getByTestId('credential-sharing-modal-users-select'), + usersSelect: () => cy.getByTestId('project-sharing-select').filter(':visible'), testSuccessTag: () => cy.getByTestId('credentials-config-container-test-success'), }; actions = { diff --git a/cypress/pages/modals/workflow-sharing-modal.ts b/cypress/pages/modals/workflow-sharing-modal.ts index c013093286..fc4ba8dada 100644 --- a/cypress/pages/modals/workflow-sharing-modal.ts +++ b/cypress/pages/modals/workflow-sharing-modal.ts @@ -3,7 +3,7 @@ import { BasePage } from '../base'; export class WorkflowSharingModal extends BasePage { getters = { modal: () => cy.getByTestId('workflowShare-modal', { timeout: 5000 }), - usersSelect: () => cy.getByTestId('workflow-sharing-modal-users-select'), + usersSelect: () => cy.getByTestId('project-sharing-select'), saveButton: () => cy.getByTestId('workflow-sharing-modal-save-button'), closeButton: () => this.getters.modal().find('.el-dialog__close').first(), }; diff --git a/cypress/pages/settings-users.ts b/cypress/pages/settings-users.ts index e3c80e5bcc..a16eb4ab6f 100644 --- a/cypress/pages/settings-users.ts +++ b/cypress/pages/settings-users.ts @@ -41,10 +41,10 @@ export class SettingsUsersPage extends BasePage { workflowPage.actions.visit(); mainSidebar.actions.goToSettings(); if (isOwner) { - settingsSidebar.getters.menuItem('Users').click(); + settingsSidebar.getters.users().click(); cy.url().should('match', new RegExp(this.url)); } else { - settingsSidebar.getters.menuItem('Users').should('not.exist'); + settingsSidebar.getters.users().should('not.exist'); // Should be redirected to workflows page if trying to access UM url cy.visit('/settings/users'); cy.url().should('match', new RegExp(workflowsPage.url)); diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index 5379b1f889..348d4aa148 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -5,14 +5,13 @@ const workflowsPage = new WorkflowsPage(); export class MainSidebar extends BasePage { getters = { - menuItem: (menuLabel: string) => - cy.getByTestId('menu-item').filter(`:contains("${menuLabel}")`), - settings: () => this.getters.menuItem('Settings'), - templates: () => this.getters.menuItem('Templates'), - workflows: () => this.getters.menuItem('Workflows'), - credentials: () => this.getters.menuItem('Credentials'), - executions: () => this.getters.menuItem('Executions'), - adminPanel: () => this.getters.menuItem('Admin Panel'), + menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), + settings: () => this.getters.menuItem('settings'), + templates: () => this.getters.menuItem('templates'), + workflows: () => this.getters.menuItem('workflows'), + credentials: () => this.getters.menuItem('credentials'), + executions: () => this.getters.menuItem('executions'), + adminPanel: () => this.getters.menuItem('cloud-admin'), userMenu: () => cy.get('div[class="action-dropdown-container"]'), logo: () => cy.getByTestId('n8n-logo'), }; diff --git a/cypress/pages/sidebar/settings-sidebar.ts b/cypress/pages/sidebar/settings-sidebar.ts index 6d519d6c31..886a0a3c1e 100644 --- a/cypress/pages/sidebar/settings-sidebar.ts +++ b/cypress/pages/sidebar/settings-sidebar.ts @@ -2,9 +2,8 @@ import { BasePage } from '../base'; export class SettingsSidebar extends BasePage { getters = { - menuItem: (menuLabel: string) => - cy.getByTestId('menu-item').filter(`:contains("${menuLabel}")`), - users: () => this.getters.menuItem('Users'), + menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), + users: () => this.getters.menuItem('settings-users'), back: () => cy.getByTestId('settings-back'), }; actions = { diff --git a/cypress/pages/variables.ts b/cypress/pages/variables.ts index 6091e5cf1b..6d9e9eb134 100644 --- a/cypress/pages/variables.ts +++ b/cypress/pages/variables.ts @@ -35,7 +35,7 @@ export class VariablesPage extends BasePage { deleteVariable: (key: string) => { const row = this.getters.variableRow(key); row.within(() => { - cy.getByTestId('variable-row-delete-button').click(); + cy.getByTestId('variable-row-delete-button').should('not.be.disabled').click(); }); const modal = cy.get('[role="dialog"]'); @@ -53,7 +53,7 @@ export class VariablesPage extends BasePage { editRow: (key: string) => { const row = this.getters.variableRow(key); row.within(() => { - cy.getByTestId('variable-row-edit-button').click(); + cy.getByTestId('variable-row-edit-button').should('not.be.disabled').click(); }); }, setRowValue: (row: Chainable>, field: 'key' | 'value', value: string) => { diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index eb855f026f..cf9665a8b8 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -32,7 +32,7 @@ export class WorkflowExecutionsTab extends BasePage { }, createManualExecutions: (count: number) => { for (let i = 0; i < count; i++) { - cy.intercept('POST', '/rest/workflows/run').as('workflowExecution'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowExecution'); workflowPage.actions.executeWorkflow(); cy.wait('@workflowExecution'); } diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 56a3c44923..fd65a426a4 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -1,7 +1,7 @@ import { BasePage } from './base'; export class WorkflowsPage extends BasePage { - url = '/workflows'; + url = '/home/workflows'; getters = { newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'), newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'), diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index a92dc2ce06..bd33a8f21f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -65,7 +65,7 @@ Cypress.Commands.add('signout', () => { cy.request({ method: 'POST', url: `${BACKEND_BASE_URL}/rest/logout`, - headers: { 'browser-id': localStorage.getItem('n8n-browserId') } + headers: { 'browser-id': localStorage.getItem('n8n-browserId') }, }); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); }); @@ -80,12 +80,19 @@ const setFeature = (feature: string, enabled: boolean) => enabled, }); +const setQuota = (feature: string, value: number) => + cy.request('PATCH', `${BACKEND_BASE_URL}/rest/e2e/quota`, { + feature: `quota:${feature}`, + value, + }); + const setQueueMode = (enabled: boolean) => cy.request('PATCH', `${BACKEND_BASE_URL}/rest/e2e/queue-mode`, { enabled, }); Cypress.Commands.add('enableFeature', (feature: string) => setFeature(feature, true)); +Cypress.Commands.add('changeQuota', (feature: string, value: number) => setQuota(feature, value)); Cypress.Commands.add('disableFeature', (feature: string) => setFeature(feature, false)); Cypress.Commands.add('enableQueueMode', () => setQueueMode(true)); Cypress.Commands.add('disableQueueMode', () => setQueueMode(false)); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index f31e50c578..411b732250 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -30,6 +30,7 @@ declare global { disableFeature(feature: string): void; enableQueueMode(): void; disableQueueMode(): void; + changeQuota(feature: string, value: number): void; waitForLoad(waitForIntercepts?: boolean): void; grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable; diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 81748af505..d88b58ea9b 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -29,7 +29,7 @@ export function createMockNodeExecutionData( ]; return acc; - }, {}) + }, {}) : data, source: [null], ...rest, @@ -88,7 +88,7 @@ export function runMockWorkflowExcution({ }) { const executionId = Math.random().toString(36).substring(4); - cy.intercept('POST', '/rest/workflows/run', { + cy.intercept('POST', '/rest/workflows/**/run', { statusCode: 201, body: { data: { diff --git a/docker/images/n8n/docker-entrypoint.sh b/docker/images/n8n/docker-entrypoint.sh index 63a7c1dca6..2205826e4c 100755 --- a/docker/images/n8n/docker-entrypoint.sh +++ b/docker/images/n8n/docker-entrypoint.sh @@ -1,4 +1,11 @@ #!/bin/sh +if [ -d /opt/custom-certificates ]; then + echo "Trusting custom certificates from /opt/custom-certificates." + export NODE_OPTIONS=--use-openssl-ca $NODE_OPTIONS + export SSL_CERT_DIR=/opt/custom-certificates + c_rehash /opt/custom-certificates +fi + if [ "$#" -gt 0 ]; then # Got started with arguments exec n8n "$@" diff --git a/package.json b/package.json index 7c30c3e250..db1f0d70a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.41.0", + "version": "1.42.0", "private": true, "homepage": "https://n8n.io", "engines": { @@ -38,9 +38,6 @@ "test:e2e:dev": "scripts/run-e2e.js dev", "test:e2e:all": "scripts/run-e2e.js all" }, - "dependencies": { - "n8n": "workspace:*" - }, "devDependencies": { "@n8n_io/eslint-config": "workspace:*", "@ngneat/falso": "^6.4.0", @@ -95,7 +92,8 @@ "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", - "vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch" + "vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch", + "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch" } } } diff --git a/packages/@n8n/chat/src/components/Chat.vue b/packages/@n8n/chat/src/components/Chat.vue index c6b0a08618..685b0805bc 100644 --- a/packages/@n8n/chat/src/components/Chat.vue +++ b/packages/@n8n/chat/src/components/Chat.vue @@ -1,5 +1,4 @@