From 390841bbf0fdd4d536101593711a6658ea2784e4 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Wed, 26 Apr 2023 09:18:10 +0200 Subject: [PATCH] feat(editor): Enhance Node Creator actions view (#5954) * WIP * WIP * Extract actions into composable * WIP: Preserve categories when searching * WIP * WIP: Tweak styles * WIP: Refactor node creator * WIP: Finish Node Creator node view/subcategories refactor * WIP: Finished actions refactor * Cleanup & Lintfix * WIP: Improve memory managment * Fix interactions * WIP * WIP: Keyboard navigation * Improve keyboard navigation and memory managment * Finished view refactor * FIx custom api calls and activation callouts * Fix actions tracking and cleanup * Product review fixes * Telemetry fixes * Fix node creator e2es * Set action name font size and actionsEmpty font weight * Fix failing credentials spec * Make sure to select first action item when switching from nodes panel to actions panel * Add actions panel e2e tests * Cleanup * Fix actions generation and cleanup * Add correct Learn More link and adjust displaying of trigger icon * Change trigger icon condition to use nodeType group * Cleanup nodeTypesUtils and snapshots and lintfixes * Lint fixes * Refine logic to show trigger icon in node creator * Add unit tests & clean up * Add `003_auto_insert_action` experiment, hide empty sections for opposite root view * Lintfix * Do not show empty category tooltips and only show activation callout in triger root view * Fix no-results node creator view * Spacings tweaks and root rendering logic adjustment * Add unit tests * Lint and e2e fixes * Revert CLI changes, fix unit tests * Remove useless comments * Sync master, replace $externalHooks mixin * Lint fix * Focus first action when panel slides in, not category * Address PR comments * Lint fix * Remove `setAddedNodeActionParameters` optional track param * Further simplify setAddedNodeActionParameters * Fix pnpn lock file * Fix types imports * Fix 13-pinning spec --- cypress/e2e/11-inline-expression-editor.cy.ts | 2 +- cypress/e2e/13-pinning.cy.ts | 4 +- cypress/e2e/4-node-creator.cy.ts | 109 ++- cypress/e2e/9-expression-editor-modal.cy.ts | 2 +- cypress/pages/features/node-creator.ts | 6 +- cypress/pages/workflow.ts | 3 +- .../src/components/N8nCallout/Callout.vue | 24 +- .../__snapshots__/Callout.spec.ts.snap | 14 +- .../N8nNodeCreatorNode/NodeCreatorNode.vue | 11 +- .../src/components/N8nNodeIcon/NodeIcon.vue | 7 +- packages/editor-ui/package.json | 3 +- packages/editor-ui/src/Interface.ts | 95 ++- .../Node/NodeCreator/CategorizedItems.vue | 690 ------------------ .../Node/NodeCreator/CategoryItem.vue | 41 -- .../Node/NodeCreator/ItemIterator.vue | 210 ------ .../{ => ItemTypes}/ActionItem.vue | 70 +- .../NodeCreator/ItemTypes/CategoryItem.vue | 81 ++ .../Node/NodeCreator/ItemTypes/LabelItem.vue | 31 + .../NodeCreator/{ => ItemTypes}/NodeItem.vue | 100 +-- .../{ => ItemTypes}/SubcategoryItem.vue | 11 +- .../NodeCreator/{ => ItemTypes}/ViewItem.vue | 22 +- .../components/Node/NodeCreator/MainPanel.vue | 405 ---------- .../Node/NodeCreator/Modes/ActionsMode.vue | 366 ++++++++++ .../Node/NodeCreator/Modes/NodesMode.vue | 213 ++++++ .../Node/NodeCreator/NodeCreator.vue | 78 +- .../NodeCreator/{ => Panel}/NoResults.vue | 25 +- .../NodeCreator/{ => Panel}/NoResultsIcon.vue | 0 .../Node/NodeCreator/Panel/NodesListPanel.vue | 258 +++++++ .../NodeCreator/{ => Panel}/SearchBar.vue | 9 +- .../Renderers/CategorizedItemsRenderer.vue | 153 ++++ .../NodeCreator/Renderers/ItemsRenderer.vue | 215 ++++++ .../__tests__/CategoryItem.test.ts | 31 + .../__tests__/ItemsRenderer.test.ts | 82 +++ .../__tests__/NodesListPanel.test.ts | 286 ++++++++ .../__tests__/useKeyboardNavigation.test.ts | 90 +++ .../Node/NodeCreator/__tests__/utils.ts | 126 ++++ .../NodeCreator/composables/useActions.ts | 218 ++++++ .../composables/useActionsGeneration.ts | 279 +++++++ .../composables/useKeyboardNavigation.ts | 156 ++++ .../NodeCreator/composables/useViewStacks.ts | 188 +++++ .../Node/NodeCreator/useMainPanelView.ts | 198 ----- .../src/components/Node/NodeCreator/utils.ts | 73 ++ .../components/Node/NodeCreator/viewsData.ts | 168 +++++ .../transitions/SlideTransition.vue | 2 +- packages/editor-ui/src/constants.ts | 35 +- packages/editor-ui/src/main.ts | 1 + .../src/plugins/i18n/locales/en.json | 34 +- .../editor-ui/src/plugins/telemetry/index.ts | 9 +- packages/editor-ui/src/stores/nodeCreator.ts | 427 +---------- packages/editor-ui/src/stores/nodeTypes.ts | 22 +- .../editor-ui/src/utils/nodeTypesUtils.ts | 193 ----- packages/editor-ui/src/views/NodeView.vue | 12 +- packages/workflow/src/Interfaces.ts | 7 - pnpm-lock.yaml | 44 +- 54 files changed, 3489 insertions(+), 2450 deletions(-) delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/CategoryItem.vue delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/ActionItem.vue (73%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/CategoryItem.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LabelItem.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/NodeItem.vue (66%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/SubcategoryItem.vue (77%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/ViewItem.vue (52%) delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/NoResults.vue (66%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/NoResultsIcon.vue (100%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/SearchBar.vue (91%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/CategoryItem.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/ItemsRenderer.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/useKeyboardNavigation.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/utils.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useKeyboardNavigation.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/useMainPanelView.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/utils.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index de5594a4f4..88fea311d9 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -69,6 +69,6 @@ describe('Inline expression editor', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^getAll$/); + WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^get$/); }); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index 6f1c328fd9..c278231017 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -1,6 +1,6 @@ import { HTTP_REQUEST_NODE_NAME, - MANUAL_TRIGGER_NODE_DISPLAY_NAME, + MANUAL_TRIGGER_NODE_NAME, PIPEDRIVE_NODE_NAME, SET_NODE_NAME, } from '../constants'; @@ -75,7 +75,7 @@ describe('Data pinning', () => { }); it('Should be able to reference paired items in a node located before pinned data', () => { - workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_DISPLAY_NAME); + workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); ndv.actions.setPinnedData([{ http: 123 }]); ndv.actions.close(); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 0057afd93c..0ad0306cb6 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -58,8 +58,8 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.getCreatorItem('On app event').click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image'); - nodeCreatorFeature.getters.getCreatorItem('Results in other categories (1)').should('exist'); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.getCategoryItem('Results in other categories').should('exist'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 1); nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image123123'); nodeCreatorFeature.getters.creatorItem().should('have.length', 0); @@ -101,7 +101,7 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('file'); // Navigate to rename action which should be the 4th item - nodeCreatorFeature.getters.searchBar().find('input').type('{downarrow} {downarrow} {downarrow} {rightarrow}'); + nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}'); NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename'); }) @@ -127,9 +127,107 @@ describe('Node Creator', () => { }) nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode); nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click(); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.creatorItem().should('have.length', 4); }) + it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('ActiveCampaign'); + nodeCreatorFeature.getters.getCreatorItem('ActiveCampaign').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed', 'true'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.getCreatorItem('Manually').click(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); + nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').click() + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should show callout and two suggested nodes if node has no trigger actions', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + + it('should show intro callout if user has not made a production execution', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('be.visible'); + nodeCreatorFeature.getters.activeSubcategory().find('button').click(); + nodeCreatorFeature.getters.searchBar().find('input').clear() + + nodeCreatorFeature.getters.getCreatorItem('On a schedule').click(); + + // Setup 1s interval execution + cy.getByTestId('parameter-input-field').click(); + cy.getByTestId('parameter-input-field') + .find('.el-select-dropdown') + .find('.option-headline') + .contains('Seconds') + .click(); + cy.getByTestId('parameter-input-secondsInterval').clear().type('1'); + + NDVModal.actions.close(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + nodeCreatorFeature.getters.getCreatorItem('Get All People').click(); + NDVModal.actions.close(); + + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.activatorSwitch().should('have.class', 'is-checked'); + + // Wait for schedule 1s execution to mark user as having made a production execution + cy.wait(1500); + cy.reload() + + // Action callout should not be visible after user has made a production execution + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('not.exist'); + }); + + it('should show Trigger and Actions sections during search', () => { + nodeCreatorFeature.actions.openNodeCreator(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('be.visible'); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('be.visible'); + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + describe('should correctly append manual trigger for regular actions', () => { // For these sources, manual node should be added const sourcesWithAppend = [ @@ -152,6 +250,7 @@ describe('Node Creator', () => { source.handler() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); @@ -162,12 +261,14 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.canvasAddButton().click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"') WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 957c0505f5..dd4e01128b 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -60,6 +60,6 @@ describe('Expression editor modal', () => { it('should resolve $parameter[]', () => { WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]'); - WorkflowPage.getters.expressionModalOutput().contains(/^getAll$/); + WorkflowPage.getters.expressionModalOutput().contains(/^get$/); }); }); diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index 8ebe6db702..6686de25ff 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -7,6 +7,7 @@ export class NodeCreator extends BasePage { plusButton: () => cy.getByTestId('node-creator-plus-button'), canvasAddButton: () => cy.getByTestId('canvas-add-button'), searchBar: () => cy.getByTestId('search-bar'), + getCategoryItem: (label: string) => cy.get(`[data-keyboard-nav-id="${label}"]`), getCreatorItem: (label: string) => this.getters.creatorItem().contains(label).parents('[data-test-id="item-iterator-item"]'), getNthCreatorItem: (n: number) => this.getters.creatorItem().eq(n), @@ -15,10 +16,11 @@ export class NodeCreator extends BasePage { selectedTab: () => this.getters.nodeCreatorTabs().find('.is-active'), categorizedItems: () => cy.getByTestId('categorized-items'), creatorItem: () => cy.getByTestId('item-iterator-item'), + categoryItem: () => cy.getByTestId('node-creator-category-item'), communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), - noResults: () => cy.getByTestId('categorized-no-results'), + noResults: () => cy.getByTestId('node-creator-no-results'), nodeItemName: () => cy.getByTestId('node-creator-item-name'), - activeSubcategory: () => cy.getByTestId('categorized-items-subcategory'), + activeSubcategory: () => cy.getByTestId('nodes-list-header'), expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'), }; diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index d42b2850c4..8b6d195105 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -140,7 +140,8 @@ export class WorkflowPage extends BasePage { if(action) { cy.contains(action).click() } else { - cy.getByTestId('item-iterator-item').eq(1).click() + // Select the first action + cy.get('[data-keyboard-nav-type="action"]').eq(0).click() } } }) diff --git a/packages/design-system/src/components/N8nCallout/Callout.vue b/packages/design-system/src/components/N8nCallout/Callout.vue index 97c15296e0..604cec6a25 100644 --- a/packages/design-system/src/components/N8nCallout/Callout.vue +++ b/packages/design-system/src/components/N8nCallout/Callout.vue @@ -1,7 +1,7 @@