feat(editor): Redirect users to canvas if they don't have any workflows (#5629)

* feat(editor): Bring new users to empty canvas

* Fix failing e2e tests and revert CLI implementation

* Revert editor-ui Interface changes

* Try to mock /settings and /active

* Revert canvas test changes, reload after executions in 20-workflow-executions

* Make sure we include manual executiosn before running them in 20-workflow-executions

* Make sure to re-init node view when replacing empty workflows route, show phantom loader
This commit is contained in:
OlegIvaniv 2023-03-08 15:11:13 +01:00 committed by GitHub
parent 0b6fa6b20e
commit 354edf6886
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 62 additions and 78 deletions

View file

@ -27,13 +27,12 @@ describe('Workflows', () => {
});
cy.signin({ email, password });
cy.visit(WorkflowsPage.url);
cy.visit('/');
cy.waitForLoad();
});
it('should create a new workflow using empty state card', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().click();
it('should land on empty canvas after registration', () => {
cy.url().should('include', WorkflowPage.url);
cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
@ -41,8 +40,6 @@ describe('Workflows', () => {
});
it('should create multiple new workflows using add workflow button', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('not.exist');
[...Array(multipleWorkflowsCount).keys()].forEach(() => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
@ -95,8 +92,10 @@ describe('Workflows', () => {
WorkflowsPage.getters.newWorkflowTemplateCard().should('be.visible');
});
it('should contain empty state cards', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowTemplateCard().should('be.visible');
it('should redirect to new canvas if no workflows', () => {
cy.wait(1000);
cy.visit(WorkflowsPage.url);
cy.wait(1000);
cy.url().should('include', WorkflowPage.url);
});
});

View file

@ -17,7 +17,6 @@ describe('Undo/Redo', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
cy.waitForLoad();
});
it('should undo/redo adding nodes', () => {

View file

@ -28,7 +28,6 @@ describe('Canvas Actions', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
cy.waitForLoad();
});
it('should render canvas', () => {

View file

@ -11,7 +11,6 @@ describe('Data pinning', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.waitForLoad();
});
it('Should be able to pin node output', () => {

View file

@ -16,7 +16,6 @@ describe('Data mapping', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.waitForLoad();
cy.window()
// @ts-ignore

View file

@ -8,12 +8,9 @@ describe('Schedule Trigger node', async () => {
beforeEach(() => {
cy.resetAll();
cy.skipSetup();
cy.visit(workflowsPage.url);
});
it('should execute and return the execution timestamp', () => {
workflowsPage.actions.createWorkflowFromCard();
cy.waitForLoad();
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.openNode('Schedule Trigger');
ndv.actions.execute();
@ -22,8 +19,6 @@ describe('Schedule Trigger node', async () => {
});
it('should execute once per second when activated', () => {
workflowsPage.actions.createWorkflowFromCard();
cy.waitForLoad();
workflowPage.actions.renameWorkflow('Schedule Trigger Workflow');
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.openNode('Schedule Trigger');

View file

@ -98,7 +98,6 @@ describe('Webhook Trigger node', async () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.waitForLoad();
cy.window()
// @ts-ignore

View file

@ -80,9 +80,7 @@ describe('Sharing', () => {
credentialsModal.actions.save();
credentialsModal.actions.close();
cy.visit(workflowsPage.url);
workflowsPage.getters.newWorkflowButtonCard().click();
cy.waitForLoad();
workflowPage.actions.visit();
workflowPage.actions.setWorkflowName('Workflow W1');
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
workflowPage.actions.addNodeToCanvas('Notion', true, true);

View file

@ -8,8 +8,6 @@ describe('Workflow tags', () => {
beforeEach(() => {
cy.resetAll();
cy.skipSetup();
wf.actions.visit();
cy.waitForLoad();
});
it('should create and attach tags inline', () => {

View file

@ -9,9 +9,6 @@ describe('Execution', () => {
beforeEach(() => {
cy.resetAll();
cy.skipSetup();
// Import workflow
workflowsPage.getters.newWorkflowButtonCard().click();
cy.waitForLoad();
});
it('should test manual workflow', () => {

View file

@ -112,7 +112,6 @@ describe('Credentials', () => {
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();
@ -129,7 +128,6 @@ describe('Credentials', () => {
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();
@ -155,7 +153,6 @@ describe('Credentials', () => {
it('should correctly render required and optional credentials', () => {
workflowPage.actions.visit();
cy.waitForLoad();
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
cy.get('body').type('{downArrow}');
cy.get('body').type('{enter}');
@ -180,7 +177,6 @@ describe('Credentials', () => {
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();
@ -194,7 +190,6 @@ describe('Credentials', () => {
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();
@ -214,7 +209,6 @@ describe('Credentials', () => {
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();
@ -235,7 +229,6 @@ describe('Credentials', () => {
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();

View file

@ -13,7 +13,6 @@ describe('Current Workflow Executions', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.waitForLoad();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
createMockExecutions();
});

View file

@ -37,11 +37,6 @@ describe('Default owner', () => {
it('should be able to create workflows', () => {
cy.resetAll();
cy.skipSetup();
cy.visit('/');
workflowsPage.getters.newWorkflowButtonCard().should('be.visible');
workflowsPage.getters.newWorkflowButtonCard().click();
cy.waitForLoad();
cy.createFixtureWorkflow('Test_workflow_1.json', `Test workflow`);
// reload page, ensure owner still has access

View file

@ -13,8 +13,7 @@ describe('Node Creator', () => {
});
beforeEach(() => {
cy.visit(nodeCreatorFeature.url);
cy.waitForLoad();
WorkflowPage.actions.visit();
});
it('should open node creator on trigger tab if no trigger is on canvas', () => {

View file

@ -5,12 +5,13 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('NDV', () => {
beforeEach(() => {
before(() => {
cy.resetAll();
cy.skipSetup();
cy.visit(workflowPage.url)
cy.waitForLoad();
});
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
workflowPage.actions.saveWorkflowOnButtonClick();
});

View file

@ -11,7 +11,6 @@ describe('Code node', () => {
});
it('should execute the placeholder in all-items mode successfully', () => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');

View file

@ -21,7 +21,6 @@ describe('Workflow Actions', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
cy.waitForLoad();
});
it('should be able to save on button click', () => {

View file

@ -1,6 +1,5 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
import { WorkflowPage, NDV } from '../pages';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -11,9 +10,6 @@ describe('HTTP Request node', () => {
});
it('should make a request with a URL and receive a response', () => {
cy.visit(workflowsPage.url);
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('HTTP Request');
workflowPage.actions.openNode('HTTP Request');

View file

@ -1,9 +1,9 @@
import { SettingsSidebar } from './sidebar/settings-sidebar';
import { MainSidebar } from './sidebar/main-sidebar';
import { WorkflowsPage } from './workflows';
import { WorkflowPage } from './workflow';
import { BasePage } from './base';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const mainSidebar = new MainSidebar();
const settingsSidebar = new SettingsSidebar();
@ -30,7 +30,7 @@ export class SettingsUsersPage extends BasePage {
goToOwnerSetup: () => this.getters.setUpOwnerButton().click(),
loginAndVisit: (email: string, password: string, isOwner: boolean) => {
cy.signin({ email, password });
cy.visit(workflowsPage.url);
workflowPage.actions.visit();
mainSidebar.actions.goToSettings();
if (isOwner) {
settingsSidebar.getters.menuItem('Users').click();
@ -39,7 +39,7 @@ export class SettingsUsersPage extends BasePage {
settingsSidebar.getters.menuItem('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));
cy.url().should('match', new RegExp(workflowPage.url));
}
},
opedDeleteDialog: (email: string) => {

View file

@ -256,12 +256,20 @@ export class WorkflowPage extends BasePage {
turnOnManualExecutionSaving: () => {
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
cy.get('.el-loading-mask').should('not.be.visible');
this.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li:contains("Yes")')
.click({ force: true });
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
this.getters.successToast().should('exist');
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
},
};
}

View file

@ -24,7 +24,7 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import 'cypress-real-events';
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage } from '../pages';
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages';
import { N8N_AUTH_COOKIE } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MessageBox } from '../pages/modals/message-box';
@ -54,13 +54,13 @@ Cypress.Commands.add(
);
Cypress.Commands.add('waitForLoad', () => {
cy.getByTestId('node-view-loader', { timeout: 10000 }).should('not.exist');
cy.get('.el-loading-mask', { timeout: 10000 }).should('not.exist');
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
});
Cypress.Commands.add('signin', ({ email, password }) => {
const signinPage = new SigninPage();
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
cy.session(
[email, password],
@ -74,7 +74,10 @@ Cypress.Commands.add('signin', ({ email, password }) => {
});
// we should be redirected to /workflows
cy.url().should('include', workflowsPage.url);
cy.visit(workflowPage.url);
cy.url().should('include', workflowPage.url);
cy.intercept('GET', '/rest/workflows/new').as('loading');
cy.wait('@loading');
},
{
validate() {
@ -158,7 +161,7 @@ Cypress.Commands.add('inviteUsers', ({ instanceOwner, users }) => {
Cypress.Commands.add('skipSetup', () => {
const signupPage = new SignupPage();
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const Confirmation = new MessageBox();
cy.visit(signupPage.url);
@ -171,8 +174,10 @@ Cypress.Commands.add('skipSetup', () => {
Confirmation.getters.header().should('contain.text', 'Skip owner account setup?');
Confirmation.actions.confirm();
// we should be redirected to /workflows
cy.url().should('include', workflowsPage.url);
// we should be redirected to empty canvas
cy.intercept('GET', '/rest/workflows/new').as('loading');
cy.url().should('include', workflowPage.url);
cy.wait('@loading');
} else {
cy.log('User already signed up');
}

View file

@ -22,22 +22,20 @@ import CustomCredential from '../fixtures/Custom_credential.json';
// Load custom nodes and credentials fixtures
beforeEach(() => {
cy.intercept('GET', '/types/nodes.json', (req) => {
req.continue((res) => {
const nodes = res.body;
req.on('response', (res) => {
const nodes = res.body || [];
res.headers['cache-control'] = 'no-cache, no-store';
nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture);
res.send(nodes);
});
}).as('nodesIntercept');
cy.intercept('GET', '/types/credentials.json', (req) => {
req.continue((res) => {
const credentials = res.body;
req.on('response', (res) => {
const credentials = res.body || [];
res.headers['cache-control'] = 'no-cache, no-store';
credentials.push(CustomCredential);
res.send(credentials);
});
})
}).as('credentialsIntercept');
})

View file

@ -3,11 +3,11 @@
ref="layout"
resource-key="workflows"
:resources="allWorkflows"
:initialize="initialize"
:filters="filters"
:additional-filters-handler="onFilter"
:show-aside="allWorkflows.length > 0"
:shareable="isShareable"
:initialize="initialize"
@click:add="addWorkflow"
@update:filters="filters = $event"
>
@ -148,7 +148,7 @@ const StatusFilter = {
ALL: '',
};
export default mixins(showMessage, debounceHelper, newVersions).extend({
const WorkflowsView = mixins(showMessage, debounceHelper, newVersions).extend({
name: 'WorkflowsView',
components: {
ResourcesListLayout,
@ -224,12 +224,20 @@ export default mixins(showMessage, debounceHelper, newVersions).extend({
}
},
async initialize() {
this.usersStore.fetchUsers(); // Can be loaded in the background, used for filtering
return await Promise.all([
await Promise.all([
this.usersStore.fetchUsers(),
this.workflowsStore.fetchAllWorkflows(),
this.workflowsStore.fetchActiveWorkflows(),
]);
// If the user has no workflows and is not participating in the demo experiment,
// redirect to the new workflow view
if (!this.isDemoTest && this.allWorkflows.length === 0) {
this.uiStore.nodeViewInitialized = false;
this.$router.replace({ name: VIEWS.NEW_WORKFLOW });
}
return Promise.resolve();
},
onClickTag(tagId: string, event: PointerEvent) {
if (!this.filters.tags.includes(tagId)) {
@ -273,6 +281,8 @@ export default mixins(showMessage, debounceHelper, newVersions).extend({
this.usersStore.showPersonalizationSurvey();
},
});
export default WorkflowsView;
</script>
<style lang="scss" module>