From 01e93406219f6c1712247d9855590ea06df3e965 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:49:36 +0200 Subject: [PATCH] feat: Add onboarding flow (#7212) Github issue / Community forum post (link here to close automatically): --- cypress/e2e/29-templates.cy.ts | 34 + cypress/fixtures/Onboarding_workflow.json | 1020 +++++++++++++++++ cypress/pages/templates.ts | 50 + cypress/pages/workflow.ts | 6 + .../src/databases/entities/WorkflowEntity.ts | 9 +- .../1695128658538-AddWorkflowMetadata.ts | 11 + .../src/databases/migrations/mysqldb/index.ts | 2 + .../databases/migrations/postgresdb/index.ts | 2 + .../1695128658538-AddWorkflowMetadata.ts | 5 + .../src/databases/migrations/sqlite/index.ts | 2 + packages/cli/src/requests.ts | 1 + packages/editor-ui/src/Interface.ts | 11 +- packages/editor-ui/src/constants.ts | 1 + .../editor-ui/src/mixins/workflowHelpers.ts | 2 - .../src/plugins/i18n/locales/en.json | 1 + packages/editor-ui/src/router.ts | 23 +- .../src/stores/__tests__/workflows.spec.ts | 101 ++ .../editor-ui/src/stores/templates.store.ts | 16 + .../editor-ui/src/stores/workflows.store.ts | 3 + packages/editor-ui/src/views/NodeView.vue | 19 +- .../src/views/WorkflowOnboardingView.vue | 68 ++ packages/workflow/src/Interfaces.ts | 4 + 22 files changed, 1373 insertions(+), 18 deletions(-) create mode 100644 cypress/e2e/29-templates.cy.ts create mode 100644 cypress/fixtures/Onboarding_workflow.json create mode 100644 cypress/pages/templates.ts create mode 100644 packages/cli/src/databases/migrations/common/1695128658538-AddWorkflowMetadata.ts create mode 100644 packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts create mode 100644 packages/editor-ui/src/stores/__tests__/workflows.spec.ts create mode 100644 packages/editor-ui/src/views/WorkflowOnboardingView.vue diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts new file mode 100644 index 0000000000..609f9cac0e --- /dev/null +++ b/cypress/e2e/29-templates.cy.ts @@ -0,0 +1,34 @@ +import { TemplatesPage } from '../pages/templates'; +import { WorkflowPage } from '../pages/workflow'; + +import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; + +const templatesPage = new TemplatesPage(); +const workflowPage = new WorkflowPage(); + +describe('Templates', () => { + it('can open onboarding flow', () => { + templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow); + cy.url().then(($url) => { + expect($url).to.match(/.*\/workflow\/.*?onboardingId=1234$/); + }) + + workflowPage.actions.shouldHaveWorkflowName(`Demo: ${name}`); + + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); + }); + + it('can import template', () => { + templatesPage.actions.importTemplate(1234, OnboardingWorkflow.name, OnboardingWorkflow); + + cy.url().then(($url) => { + expect($url).to.include('/workflow/new?templateId=1234'); + }); + + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name); + }); +}); diff --git a/cypress/fixtures/Onboarding_workflow.json b/cypress/fixtures/Onboarding_workflow.json new file mode 100644 index 0000000000..8e292b59a3 --- /dev/null +++ b/cypress/fixtures/Onboarding_workflow.json @@ -0,0 +1,1020 @@ +{ + "name": "DEMO: Create a new record in Google Sheets when something happens in Hubspot", + "nodes": [ + { + "parameters": { + "eventsUi": { + "eventValues": [ + {} + ] + }, + "additionalFields": {} + }, + "id": "78395fdf-2e8b-4064-a102-c1c0335e0d94", + "name": "HubSpot Trigger", + "type": "n8n-nodes-base.hubspotTrigger", + "typeVersion": 1, + "position": [ + 580, + 320 + ], + "webhookId": "25833e56-c646-4af0-8bbe-2eea8bda4c00", + "notesInFlow": true, + "notes": "On Contact Created" + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json['identity-profiles'][0].identities[0].value }}", + "operation": "contains", + "value2": "@gmail" + } + ] + } + }, + "id": "3888d918-c140-47a1-8024-d50fddb3f8f0", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 820, + 320 + ], + "notesInFlow": true, + "notes": "Is Gmail Email?" + }, + { + "parameters": {}, + "id": "416a8876-f496-499c-a089-aad243daabc6", + "name": "Is Gmail, Don't Add to Sheet", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1140, + 240 + ] + }, + { + "parameters": { + "content": "## Demo: Creating Google Sheets records when something happens in HubSpot\nThis workflow runs each time a new Contact is added in HubSpot. It filters out Contacts with Gmail email addresses then pushes the remaining new Contacts to [this Google Sheet](https://docs.google.com/spreadsheets/d/1GeWRcu5cvNVA-0hpHZHtatjnFtyunbgWUgRur5uT08A/edit?usp=sharing).", + "height": 160.1450000000002, + "width": 480.31999999999596 + }, + "id": "cf69cda9-ba96-468f-990c-6c3ad5242053", + "name": "Sticky Note1", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 520, + 100 + ] + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1GeWRcu5cvNVA-0hpHZHtatjnFtyunbgWUgRur5uT08A", + "mode": "list", + "cachedResultName": "New HubSpot Contacts", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1GeWRcu5cvNVA-0hpHZHtatjnFtyunbgWUgRur5uT08A/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "New Contacts", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1GeWRcu5cvNVA-0hpHZHtatjnFtyunbgWUgRur5uT08A/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "Name": "={{ $json.properties.num_unique_conversion_events.versions[0]['source-type'] }}", + "Email": "={{ $json.properties.num_unique_conversion_events.versions[0]['source-label'] }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "Name", + "displayName": "Name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Email", + "displayName": "Email", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Sync timestamp", + "displayName": "Sync timestamp", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ] + }, + "options": {} + }, + "id": "1e4084bd-b7fb-41f1-a340-1414ef134468", + "name": "Google Sheets", + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4, + "position": [ + 1140, + 440 + ], + "notesInFlow": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "FrRoXgPJOrFwkeN4", + "name": "Replace me with your own Sheets credential" + } + }, + "notes": "Append new contact to sheet" + } + ], + "pinData": { + "HubSpot Trigger": [ + { + "json": { + "vid": 51, + "canonical-vid": 51, + "merged-vids": [], + "portal-id": 8924380, + "is-contact": true, + "properties": { + "hs_latest_source_data_2": { + "value": "sample-contact", + "versions": [ + { + "value": "sample-contact", + "source-type": "MIGRATION", + "source-id": "BackfillContactUpdatesKafka", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1639158301358, + "selected": false + } + ] + }, + "hs_latest_source_data_1": { + "value": "API", + "versions": [ + { + "value": "API", + "source-type": "MIGRATION", + "source-id": "BackfillContactUpdatesKafka", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1639158301358, + "selected": false + } + ] + }, + "hs_is_unworked": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045782, + "selected": false + } + ] + }, + "firstname": { + "value": "Brian", + "versions": [ + { + "value": "Brian", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "associatedcompanyid": { + "value": "4931550080", + "versions": [ + { + "value": "4931550080", + "source-type": "CALCULATED", + "source-id": "RollupProperties", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827057264, + "selected": false + } + ] + }, + "city": { + "value": "Cambridge", + "versions": [ + { + "value": "Cambridge", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "num_unique_conversion_events": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "MIGRATION", + "source-id": "BackfillReadtimeCalculatedPropertiesJob", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1629469311146, + "selected": false + } + ] + }, + "hs_latest_source": { + "value": "OFFLINE", + "versions": [ + { + "value": "OFFLINE", + "source-type": "MIGRATION", + "source-id": "BackfillContactUpdatesKafka", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1639158301358, + "selected": false + } + ] + }, + "hs_pipeline": { + "value": "contacts-lifecycle-pipeline", + "versions": [ + { + "value": "contacts-lifecycle-pipeline", + "source-type": "MIGRATION", + "source-id": "BackfillHsPipelineForContacts", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1628846625829, + "selected": false + } + ] + }, + "hs_analytics_revenue": { + "value": "0.0", + "versions": [ + { + "value": "0.0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_social_num_broadcast_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "createdate": { + "value": "1606827045698", + "versions": [ + { + "value": "1606827045698", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045698, + "selected": false + } + ] + }, + "hs_analytics_num_visits": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_social_linkedin_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_marketable_until_renewal": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045876, + "selected": false + } + ] + }, + "hs_marketable_status": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045876, + "selected": false + } + ] + }, + "hs_analytics_source": { + "value": "OFFLINE", + "versions": [ + { + "value": "OFFLINE", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_email_domain": { + "value": "hubspot.com", + "versions": [ + { + "value": "hubspot.com", + "source-type": "MIGRATION", + "source-id": "BackfillReadtimeCalculatedPropertiesJob", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1629469311146, + "selected": false + } + ] + }, + "hs_analytics_num_page_views": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_marketable_reason_id": { + "value": "Sample Contact", + "versions": [ + { + "value": "Sample Contact", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045876, + "selected": false + } + ] + }, + "company": { + "value": "HubSpot", + "versions": [ + { + "value": "HubSpot", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "state": { + "value": "MA", + "versions": [ + { + "value": "MA", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "email": { + "value": "bh@hubspot.com", + "versions": [ + { + "value": "bh@hubspot.com", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_latest_source_timestamp": { + "value": "1606827045720", + "versions": [ + { + "value": "1606827045720", + "source-type": "MIGRATION", + "source-id": "BackfillContactUpdatesKafka", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1651750884919, + "selected": false + } + ] + }, + "website": { + "value": "http://www.HubSpot.com", + "versions": [ + { + "value": "http://www.HubSpot.com", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_marketable_reason_type": { + "value": "SAMPLE_CONTACT", + "versions": [ + { + "value": "SAMPLE_CONTACT", + "source-type": "API", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045876, + "selected": false + } + ] + }, + "jobtitle": { + "value": "CEO", + "versions": [ + { + "value": "CEO", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "lastmodifieddate": { + "value": "1651750891986", + "versions": [ + { + "value": "1651750891986", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1651750891986, + "selected": false + }, + { + "value": "1639158305597", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1639158305597, + "selected": false + }, + { + "value": "1628846625829", + "source-type": "CALCULATED", + "source-id": "BackfillHsPipelineForContacts", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1628846625829, + "selected": false + } + ] + }, + "hs_analytics_first_timestamp": { + "value": "1606827045444", + "versions": [ + { + "value": "1606827045444", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_social_google_plus_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_analytics_average_page_views": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "lastname": { + "value": "Halligan (Sample Contact)", + "versions": [ + { + "value": "Halligan (Sample Contact)", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_all_contact_vids": { + "value": "51", + "versions": [ + { + "value": "51", + "source-type": "MIGRATION", + "source-id": "BackfillReadtimeCalculatedPropertiesJob", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1629469311146, + "selected": false + } + ] + }, + "twitterhandle": { + "value": "bhalligan", + "versions": [ + { + "value": "bhalligan", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_social_facebook_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_is_contact": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045698, + "selected": false + } + ] + }, + "num_conversion_events": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "MIGRATION", + "source-id": "BackfillReadtimeCalculatedPropertiesJob", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1629469311146, + "selected": false + } + ] + }, + "twitterprofilephoto": { + "value": "https://pbs.twimg.com/profile_images/3491742741/212e42c07d3348251da10872e85aa6b0.jpeg", + "versions": [ + { + "value": "https://pbs.twimg.com/profile_images/3491742741/212e42c07d3348251da10872e85aa6b0.jpeg", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_object_id": { + "value": "51", + "versions": [ + { + "value": "51", + "source-type": "MIGRATION", + "source-id": "BackfillReadtimeCalculatedPropertiesJob", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1629469311146, + "selected": false + } + ] + }, + "hs_analytics_num_event_completions": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_social_twitter_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827047569, + "selected": false + } + ] + }, + "hs_analytics_source_data_2": { + "value": "sample-contact", + "versions": [ + { + "value": "sample-contact", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827054762, + "selected": false + } + ] + }, + "hs_lifecyclestage_lead_date": { + "value": "1606827045444", + "versions": [ + { + "value": "1606827045444", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + }, + "hs_analytics_source_data_1": { + "value": "API", + "versions": [ + { + "value": "API", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827054762, + "selected": false + } + ] + }, + "lifecyclestage": { + "value": "lead", + "versions": [ + { + "value": "lead", + "source-type": "CONTACTS_WEB", + "source-id": "sample-contact", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1606827045444, + "selected": false + } + ] + } + }, + "form-submissions": [], + "list-memberships": [], + "identity-profiles": [ + { + "vid": 51, + "saved-at-timestamp": 1606827045720, + "deleted-changed-timestamp": 0, + "identities": [ + { + "type": "EMAIL", + "value": "bh@hubspot.com", + "timestamp": 1606827045444, + "is-primary": true + }, + { + "type": "LEAD_GUID", + "value": "d3749acc-06e1-4511-84fd-7b0d847f6eff", + "timestamp": 1606827045717 + } + ] + } + ], + "merge-audits": [], + "associated-company": { + "company-id": 4931550080, + "portal-id": 8924380, + "properties": { + "country": { + "value": "United States" + }, + "city": { + "value": "Cambridge" + }, + "num_associated_contacts": { + "value": "2" + }, + "timezone": { + "value": "America/New_York" + }, + "facebook_company_page": { + "value": "https://www.facebook.com/hubspot" + }, + "createdate": { + "value": "1606827053844" + }, + "description": { + "value": "HubSpot is an American developer and marketer of software products for inbound marketing, sales, and customer service." + }, + "hs_analytics_latest_source_data_2": { + "value": "sample-contact" + }, + "hs_analytics_latest_source_data_1": { + "value": "API" + }, + "hs_num_blockers": { + "value": "0" + }, + "industry": { + "value": "COMPUTER_SOFTWARE" + }, + "total_money_raised": { + "value": "100.5M" + }, + "web_technologies": { + "value": "unbounce;instagram;app_nexus;piwik;google_analytics;mixpanel;google_tag_manager;facebook_advertiser;salesforce;cloud_flare;dstillery;twitter_button;hubspot;vidyard;facebook_connect;crazy_egg;amazon__cloudfront;wistia;optimizely" + }, + "numberofemployees": { + "value": "5000" + }, + "hs_analytics_num_visits": { + "value": "0" + }, + "linkedin_company_page": { + "value": "https://www.linkedin.com/company/hubspot" + }, + "hs_analytics_latest_source_timestamp": { + "value": "1606827045720" + }, + "hs_analytics_source": { + "value": "OFFLINE" + }, + "annualrevenue": { + "value": "250000000" + }, + "founded_year": { + "value": "2006" + }, + "hs_annual_revenue_currency_code": { + "value": "USD" + }, + "hs_analytics_num_page_views": { + "value": "0" + }, + "state": { + "value": "MA" + }, + "linkedinbio": { + "value": "HubSpot is an American developer and marketer of software products for inbound marketing, sales, and customer service." + }, + "hs_num_open_deals": { + "value": "0" + }, + "zip": { + "value": "02141" + }, + "website": { + "value": "hubspot.com" + }, + "address": { + "value": "25 First Street" + }, + "hs_analytics_first_timestamp": { + "value": "1606827045444" + }, + "first_contact_createdate": { + "value": "1606827045444" + }, + "twitterhandle": { + "value": "HubSpot" + }, + "hs_target_account_probability": { + "value": "0.49565839767456055" + }, + "hs_lastmodifieddate": { + "value": "1653392172246" + }, + "hs_num_decision_makers": { + "value": "0" + }, + "phone": { + "value": "+1 888-482-7768" + }, + "domain": { + "value": "hubspot.com" + }, + "hs_num_child_companies": { + "value": "0" + }, + "hs_num_contacts_with_buying_roles": { + "value": "0" + }, + "hs_object_id": { + "value": "4931550080" + }, + "is_public": { + "value": "true" + }, + "name": { + "value": "HubSpot, Inc." + }, + "hs_analytics_source_data_2": { + "value": "sample-contact" + }, + "hs_analytics_latest_source": { + "value": "OFFLINE" + }, + "hs_analytics_source_data_1": { + "value": "API" + } + } + } + } + } + ] + }, + "connections": { + "HubSpot Trigger": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Is Gmail, Don't Add to Sheet", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Google Sheets", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "4748dbbc-75dd-400e-98d1-41bbd82c7208", + "id": "cGYp8fpTdh3LAgP5", + "meta": { + "instanceId": "dbd43d88d26a9e30d8aadc002c9e77f1400c683dd34efe3778d43d27250dde50" + }, + "tags": [ + { + "createdAt": "2023-09-21T09:36:34.726Z", + "updatedAt": "2023-09-21T09:36:52.231Z", + "id": "vh6ctEIEfFztmSF2", + "name": "release-template-version" + } + ] +} \ No newline at end of file diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts new file mode 100644 index 0000000000..d49c086a79 --- /dev/null +++ b/cypress/pages/templates.ts @@ -0,0 +1,50 @@ +import { BasePage } from './base'; +import { WorkflowPage } from './workflow'; + +const workflowPage = new WorkflowPage(); +export class TemplatesPage extends BasePage { + url = '/templates'; + + getters = { + }; + + actions = { + openOnboardingFlow: (id: number, name: string , workflow: object) => { + const apiResponse = { + id, + name, + workflow, + }; + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); + cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, { + statusCode: 200, + body: apiResponse, + }).as('getTemplate'); + cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); + + cy.visit(`/workflows/onboarding/${id}`); + + cy.wait('@getTemplate'); + cy.wait(['@createWorkflow', '@getWorkflow']); + }, + + importTemplate: (id: number, name: string, workflow: object) => { + const apiResponse = { + id, + name, + workflow, + }; + cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, { + statusCode: 200, + body: apiResponse, + }).as('getTemplate'); + cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); + + cy.visit(`/workflows/templates/${id}`); + + cy.wait('@getTemplate'); + cy.wait( '@getWorkflow'); + } + } +} + diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 183d72b9a4..2031596319 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -304,5 +304,11 @@ export class WorkflowPage extends BasePage { editSticky: (content: string) => { this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}'); }, + shouldHaveWorkflowName: (name: string) => { + this.getters + .workflowNameInputContainer() + .invoke('attr', 'title') + .should('include', name); + }, }; } diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index 0bbb70ccf2..5b0f5f29eb 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -1,6 +1,6 @@ import { Length } from 'class-validator'; -import { IConnections, IDataObject, IWorkflowSettings } from 'n8n-workflow'; +import { IConnections, IDataObject, IWorkflowSettings, WorkflowFEMeta } from 'n8n-workflow'; import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow'; import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from 'typeorm'; @@ -46,6 +46,13 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl }) staticData?: IDataObject; + @Column({ + type: jsonColumnType, + nullable: true, + transformer: objectRetriever, + }) + meta?: WorkflowFEMeta; + @ManyToMany('TagEntity', 'workflows') @JoinTable({ name: 'workflows_tags', // table name for the junction table of this relation diff --git a/packages/cli/src/databases/migrations/common/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/common/1695128658538-AddWorkflowMetadata.ts new file mode 100644 index 0000000000..84ab2ba47a --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1695128658538-AddWorkflowMetadata.ts @@ -0,0 +1,11 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { + await addColumns('workflow_entity', [column('meta').json]); + } + + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('workflow_entity', ['meta']); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index b6255dc5f1..598435a1a0 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -48,6 +48,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -99,4 +100,5 @@ export const mysqlMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index aa4906c6b2..9080cdb98f 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -46,6 +46,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -95,4 +96,5 @@ export const postgresMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts new file mode 100644 index 0000000000..15cb2b7203 --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts @@ -0,0 +1,5 @@ +import { AddWorkflowMetadata1695128658538 as BaseMigration } from '../common/1695128658538-AddWorkflowMetadata'; + +export class AddWorkflowMetadata1695128658538 extends BaseMigration { + transaction = false as const; +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index d4c4e736b9..2d2ae87dfd 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -45,6 +45,7 @@ import { AddMfaColumns1690000000030 } from './1690000000040-AddMfaColumns'; import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -93,6 +94,7 @@ const sqliteMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 82f2c6a7a1..4ce69bc81c 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -85,6 +85,7 @@ export declare namespace WorkflowRequest { active: boolean; tags: string[]; hash: string; + meta: Record; }>; type ManualRunPayload = { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index eeebd22d1d..485e8d5125 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -214,6 +214,7 @@ export interface IWorkflowDataUpdate { tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response pinData?: IPinData; versionId?: string; + meta?: WorkflowMetadata; } export interface IWorkflowToShare extends IWorkflowDataUpdate { @@ -225,10 +226,7 @@ export interface IWorkflowToShare extends IWorkflowDataUpdate { export interface IWorkflowTemplate { id: number; name: string; - workflow: { - nodes: INodeUi[]; - connections: IConnections; - }; + workflow: Pick; } export interface INewWorkflowData { @@ -236,6 +234,10 @@ export interface INewWorkflowData { onboardingFlowEnabled: boolean; } +export interface WorkflowMetadata { + onboardingId?: string; +} + // Almost identical to cli.Interfaces.ts export interface IWorkflowDb { id: string; @@ -252,6 +254,7 @@ export interface IWorkflowDb { ownedBy?: Partial; versionId: string; usedCredentials?: IUsedCredential[]; + meta?: WorkflowMetadata; } // Identical to cli.Interfaces.ts diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index a6a0694b65..f09fa9d9b0 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -361,6 +361,7 @@ export const enum VIEWS { WORKFLOW = 'NodeViewExisting', DEMO = 'WorkflowDemo', TEMPLATE_IMPORT = 'WorkflowTemplate', + WORKFLOW_ONBOARDING = 'WorkflowOnboarding', SIGNIN = 'SigninView', SIGNUP = 'SignupView', SIGNOUT = 'SignoutView', diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index 6b580c2475..4dba373ba1 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -878,8 +878,6 @@ export const workflowHelpers = defineComponent({ const workflowDataRequest: IWorkflowDataUpdate = data || (await this.getWorkflowDataToSave()); - // make sure that the new ones are not active - workflowDataRequest.active = false; const changedNodes = {} as IDataObject; if (resetNodeIds) { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ec25d2cfe6..82c2cf0974 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1560,6 +1560,7 @@ "tagsTableHeader.searchTags": "Search Tags", "tagsView.inUse": "{count} workflow | {count} workflows", "tagsView.notBeingUsed": "Not being used", + "onboarding.title": "Demo: {name}", "template.buttons.goBackButton": "Go back", "template.buttons.useThisWorkflowButton": "Use this workflow", "template.details.appsInTheCollection": "This collection features", diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index d29b858249..600bcce89e 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -41,6 +41,7 @@ import SettingsSourceControl from './views/SettingsSourceControl.vue'; import SettingsExternalSecrets from './views/SettingsExternalSecrets.vue'; import SettingsAuditLogs from './views/SettingsAuditLogs.vue'; import WorkflowHistory from '@/views/WorkflowHistory.vue'; +import WorkflowOnboardingView from '@/views/WorkflowOnboardingView.vue'; import { EnterpriseEditionFeature, VIEWS } from '@/constants'; interface IRouteConfig { @@ -57,11 +58,11 @@ interface IRouteConfig { }; } -function getTemplatesRedirect() { +function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]) { const settingsStore = useSettingsStore(); const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled; if (!isTemplatesEnabled) { - return { name: VIEWS.NOT_FOUND }; + return { name: defaultRedirect || VIEWS.NOT_FOUND }; } return false; @@ -334,6 +335,24 @@ export const routes = [ }, }, }, + { + path: '/workflows/onboarding/:id', + name: VIEWS.WORKFLOW_ONBOARDING, + components: { + default: WorkflowOnboardingView, + header: MainHeader, + sidebar: MainSidebar, + }, + meta: { + templatesEnabled: true, + getRedirect: () => getTemplatesRedirect(VIEWS.NEW_WORKFLOW), + permissions: { + allow: { + loginStatus: [LOGIN_STATUS.LoggedIn], + }, + }, + }, + }, { path: '/workflow/new', name: VIEWS.NEW_WORKFLOW, diff --git a/packages/editor-ui/src/stores/__tests__/workflows.spec.ts b/packages/editor-ui/src/stores/__tests__/workflows.spec.ts new file mode 100644 index 0000000000..78396b75ef --- /dev/null +++ b/packages/editor-ui/src/stores/__tests__/workflows.spec.ts @@ -0,0 +1,101 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { IWorkflowDataUpdate } from '@/Interface'; +import { makeRestApiRequest } from '@/utils'; +import { useRootStore } from '../n8nRoot.store'; + +vi.mock('@/utils', () => ({ + makeRestApiRequest: vi.fn(), +})); + +const MOCK_WORKFLOW_SIMPLE: IWorkflowDataUpdate = { + id: '1', + name: 'test', + nodes: [ + { + parameters: { + path: '21a77783-e050-4e0f-9915-2d2dd5b53cde', + options: {}, + }, + id: '2dbf9369-2eec-42e7-9b89-37e50af12289', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + typeVersion: 1, + position: [340, 240], + webhookId: '21a77783-e050-4e0f-9915-2d2dd5b53cde', + }, + { + parameters: { + table: 'product', + columns: 'name,ean', + additionalFields: {}, + }, + name: 'Insert Rows1', + type: 'n8n-nodes-base.postgres', + position: [580, 240], + typeVersion: 1, + id: 'a10ba62a-8792-437c-87df-0762fa53e157', + credentials: { + postgres: { + id: 'iEFl08xIegmR8xF6', + name: 'Postgres account', + }, + }, + }, + ], + connections: { + Webhook: { + main: [ + [ + { + node: 'Insert Rows1', + type: 'main', + index: 0, + }, + ], + ], + }, + }, +}; + +describe('worklfows store', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + describe('createNewWorkflow', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('creates new workflow', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE); + + expect(makeRestApiRequest).toHaveBeenCalledWith( + useRootStore().getRestApiContext, + 'POST', + '/workflows', + { + ...MOCK_WORKFLOW_SIMPLE, + active: false, + }, + ); + }); + + it('sets active to false', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow({ ...MOCK_WORKFLOW_SIMPLE, active: true }); + + expect(makeRestApiRequest).toHaveBeenCalledWith( + useRootStore().getRestApiContext, + 'POST', + '/workflows', + { + ...MOCK_WORKFLOW_SIMPLE, + active: false, + }, + ); + }); + }); +}); diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 6d40d7a98e..ab80091fdf 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia'; import { STORES } from '@/constants'; import type { + INodeUi, ITemplatesCategory, ITemplatesCollection, ITemplatesCollectionFull, @@ -19,6 +20,7 @@ import { getWorkflows, getWorkflowTemplate, } from '@/api/templates'; +import { getFixedNodesList } from '@/utils/nodeViewUtils'; const TEMPLATES_PAGE_SIZE = 10; @@ -332,5 +334,19 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { const versionCli: string = settingsStore.versionCli; return getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli }); }, + + async getFixedWorkflowTemplate(templateId: string): Promise { + const template = await this.getWorkflowTemplate(templateId); + if (template?.workflow?.nodes) { + template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; + template.workflow.nodes?.forEach((node) => { + if (node.credentials) { + delete node.credentials; + } + }); + } + + return template; + }, }, }); diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index ae35bf88c5..7892cd10c5 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -1272,6 +1272,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { // Creates a new workflow async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise { + // make sure that the new ones are not active + sendData.active = false; + const rootStore = useRootStore(); return makeRestApiRequest( rootStore.getRestApiContext, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 8a9b3d9d74..62a08af09a 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -886,7 +886,7 @@ export default defineComponent({ let data: IWorkflowTemplate | undefined; try { void this.$externalHooks().run('template.requested', { templateId }); - data = await this.templatesStore.getWorkflowTemplate(templateId); + data = await this.templatesStore.getFixedWorkflowTemplate(templateId); if (!data) { throw new Error( @@ -901,14 +901,6 @@ export default defineComponent({ return; } - data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes) as INodeUi[]; - - data.workflow.nodes?.forEach((node) => { - if (node.credentials) { - delete node.credentials; - } - }); - this.blankRedirect = true; await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } }); @@ -2635,6 +2627,15 @@ export default defineComponent({ this.titleSet(workflow.name, 'IDLE'); await this.openWorkflow(workflow); await this.checkAndInitDebugMode(); + + if (workflow.meta?.onboardingId) { + this.$telemetry.track( + `User opened workflow from onboarding template with ID ${workflow.meta.onboardingId}`, + { + workflow_id: workflow.id, + }, + ); + } } } else if (this.$route.meta?.nodeView === true) { // Create new workflow diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue new file mode 100644 index 0000000000..19d4d01e3f --- /dev/null +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 6e74abd776..188c05a159 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1816,6 +1816,10 @@ export interface IWorkflowSettings { executionOrder?: 'v0' | 'v1'; } +export interface WorkflowFEMeta { + onboardingId?: string; +} + export interface WorkflowTestData { description: string; input: {