From 79b09fdf84860eee4c80e0c7eb7e320f6e05eabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 9 Feb 2024 13:47:43 +0100 Subject: [PATCH] feat(editor): Use website as the main templates repository (#8591) --- cypress/e2e/29-templates.cy.ts | 159 ++++-------------- .../e2e/34-template-credentials-setup.cy.ts | 24 ++- .../e2e/38-custom-template-repository.cy.ts | 147 ++++++++++++++++ .../Workflow_template_write_http_query.json | 4 +- cypress/pages/template-workflow.ts | 4 +- cypress/pages/templates.ts | 8 +- .../editor-ui/src/components/MainSidebar.vue | 19 ++- .../components/Node/NodeCreator/viewsData.ts | 6 +- packages/editor-ui/src/constants.ts | 9 + .../src/plugins/i18n/locales/en.json | 2 +- packages/editor-ui/src/router.ts | 19 ++- .../editor-ui/src/stores/templates.store.ts | 34 +++- 12 files changed, 281 insertions(+), 154 deletions(-) create mode 100644 cypress/e2e/38-custom-template-repository.cy.ts diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index 1df5a7c8c7..8879e5d74e 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -1,139 +1,44 @@ import { TemplatesPage } from '../pages/templates'; -import { WorkflowPage } from '../pages/workflow'; - -import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; -import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json'; -import { TemplateWorkflowPage } from '../pages/template-workflow'; +import { WorkflowsPage } from '../pages/workflows'; +import { MainSidebar } from '../pages/sidebar/main-sidebar'; const templatesPage = new TemplatesPage(); -const workflowPage = new WorkflowPage(); -const templateWorkflowPage = new TemplateWorkflowPage(); +const workflowsPage = new WorkflowsPage(); +const mainSidebar = new MainSidebar(); -describe('Templates', () => { +describe('Workflow templates', () => { beforeEach(() => { - cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=&search=', { fixture: 'templates_search/all_templates_search_response.json' }).as('searchRequest'); - cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest'); - cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest'); - cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest'); + cy.intercept('GET', '**/rest/settings', (req) => { + // Disable cache + 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 + res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' }; + } + }); + }).as('settingsRequest'); }); - it('can open onboarding flow', () => { - templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow); - cy.url().then(($url) => { - expect($url).to.match(/.*\/workflow\/.*?onboardingId=1234$/); + it('Opens website when clicking templates sidebar link', () => { + cy.visit(workflowsPage.url); + mainSidebar.getters.menuItem('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', 'target', '_blank'); + }); + + it('Redirects to website when visiting templates page directly', () => { + cy.visit(templatesPage.url); + cy.origin('https://n8n.io', () => { + cy.url().should('include', 'https://n8n.io/workflows'); }) - - 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); - }); - - it('should save template id with the workflow', () => { - cy.visit(templatesPage.url); - cy.get('.el-skeleton.n8n-loading').should('not.exist'); - templatesPage.getters.firstTemplateCard().should('exist'); - templatesPage.getters.templatesLoadingContainer().should('not.exist'); - templatesPage.getters.firstTemplateCard().click(); - cy.url().should('include', '/templates/'); - - cy.url().then(($url) => { - const templateId = $url.split('/').pop(); - - templatesPage.getters.useTemplateButton().click(); - cy.url().should('include', '/workflow/new'); - workflowPage.actions.saveWorkflowOnButtonClick(); - - workflowPage.actions.selectAll(); - workflowPage.actions.hitCopy(); - - cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); - // Check workflow JSON by copying it to clipboard - cy.readClipboard().then((workflowJSON) => { - expect(workflowJSON).to.contain(`"templateId": "${templateId}"`); - }); - }); - }); - - it('can open template with images and hides workflow screenshots', () => { - templateWorkflowPage.actions.openTemplate(WorkflowTemplate); - - templateWorkflowPage.getters.description().find('img').should('have.length', 1); - }); - - - it('renders search elements correctly', () => { - cy.visit(templatesPage.url); - templatesPage.getters.searchInput().should('exist'); - templatesPage.getters.allCategoriesFilter().should('exist'); - templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1); - templatesPage.getters.templateCards().should('have.length.greaterThan', 0); - }); - - it('can filter templates by category', () => { - cy.visit(templatesPage.url); - templatesPage.getters.templatesLoadingContainer().should('not.exist'); - templatesPage.getters.expandCategoriesButton().click(); - templatesPage.getters.categoryFilter('sales').should('exist'); - let initialTemplateCount = 0; - let initialCollectionCount = 0; - - templatesPage.getters.templateCountLabel().then(($el) => { - initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10); - templatesPage.getters.collectionCountLabel().then(($el) => { - initialCollectionCount = parseInt($el.text().replace(/\D/g, ''), 10); - - templatesPage.getters.categoryFilter('sales').click(); - templatesPage.getters.templatesLoadingContainer().should('not.exist'); - - // Should have less templates and collections after selecting a category - templatesPage.getters.templateCountLabel().should(($el) => { - expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialTemplateCount); - }); - templatesPage.getters.collectionCountLabel().should(($el) => { - expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialCollectionCount); - }); - }); - }); - }); - - it('should preserve search query in URL', () => { - cy.visit(templatesPage.url); - templatesPage.getters.templatesLoadingContainer().should('not.exist'); - templatesPage.getters.expandCategoriesButton().click(); - templatesPage.getters.categoryFilter('sales').should('exist'); - templatesPage.getters.categoryFilter('sales').click(); - templatesPage.getters.searchInput().type('auto'); - - cy.url().should('include', '?categories='); - cy.url().should('include', '&search='); - - cy.reload(); - - // Should preserve search query in URL - cy.url().should('include', '?categories='); - cy.url().should('include', '&search='); - - // Sales category should still be selected - templatesPage.getters.categoryFilter('sales').find('label').should('have.class', 'is-checked'); - // Search input should still have the search query - templatesPage.getters.searchInput().should('have.value', 'auto'); - // Sales checkbox should be pushed to the top - templatesPage.getters.categoryFilters().eq(1).then(($el) => { - expect($el.text()).to.equal('Sales'); - }); + it('Redirects to website when visiting template by id page directly', () => { + cy.visit(`${templatesPage.url}/1`); + cy.origin('https://n8n.io', () => { + cy.url().should('include', 'https://n8n.io/workflows/1'); + }) }); }); diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index 863cc61c18..7de435c4fa 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -4,13 +4,11 @@ import { testData, } from '../pages/template-collection'; import * as templateCredentialsSetupPage from '../pages/template-credential-setup'; -import { TemplateWorkflowPage } from '../pages/template-workflow'; import { WorkflowPage } from '../pages/workflow'; import * as formStep from '../composables/setup-template-form-step'; import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal'; -const templateWorkflowPage = new TemplateWorkflowPage(); const workflowPage = new WorkflowPage(); const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate; @@ -34,18 +32,16 @@ describe('Template credentials setup', () => { cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, { fixture: testTemplate.fixture, }); - }); - - it('can be opened from template workflow page', () => { - templateWorkflowPage.actions.visit(testTemplate.id); - templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); - templateWorkflowPage.getters.useTemplateButton().should('be.visible'); - templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); - templateWorkflowPage.actions.clickUseThisWorkflowButton(); - - templateCredentialsSetupPage.getters - .title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`) - .should('be.visible'); + cy.intercept('GET', '**/rest/settings', (req) => { + // Disable cache + 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 + res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' }; + } + }); + }).as('settingsRequest'); }); it('can be opened from template collection page', () => { diff --git a/cypress/e2e/38-custom-template-repository.cy.ts b/cypress/e2e/38-custom-template-repository.cy.ts new file mode 100644 index 0000000000..6b974f14e5 --- /dev/null +++ b/cypress/e2e/38-custom-template-repository.cy.ts @@ -0,0 +1,147 @@ +import { TemplatesPage } from '../pages/templates'; +import { WorkflowPage } from '../pages/workflow'; +import { TemplateWorkflowPage } from '../pages/template-workflow'; +import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; +import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json'; + +const templatesPage = new TemplatesPage(); +const workflowPage = new WorkflowPage(); +const templateWorkflowPage = new TemplateWorkflowPage(); + + +describe('In-app templates repository', () => { + beforeEach(() => { + cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=&search=', { fixture: 'templates_search/all_templates_search_response.json' }).as('searchRequest'); + cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest'); + cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest'); + cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest'); + cy.intercept('GET', '**/rest/settings', (req) => { + // Disable cache + delete req.headers['if-none-match'] + req.reply((res) => { + if (res.body.data) { + // Enable in-app templates by setting a custom host + res.body.data.templates = { enabled: true, host: 'https://api-staging.n8n.io/api/' }; + } + }); + }).as('settingsRequest'); + }); + + it('can open onboarding flow', () => { + templatesPage.actions.openOnboardingFlow(1, OnboardingWorkflow.name, OnboardingWorkflow, 'https://api-staging.n8n.io'); + cy.url().then(($url) => { + expect($url).to.match(/.*\/workflow\/.*?onboardingId=1$/); + }) + + 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(1, OnboardingWorkflow.name, OnboardingWorkflow, 'https://api-staging.n8n.io'); + + cy.url().then(($url) => { + expect($url).to.include('/workflow/new?templateId=1'); + }); + + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name); + }); + + it('should save template id with the workflow', () => { + cy.visit(templatesPage.url); + cy.get('.el-skeleton.n8n-loading').should('not.exist'); + templatesPage.getters.firstTemplateCard().should('exist'); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.firstTemplateCard().click(); + cy.url().should('include', '/templates/'); + + cy.url().then(($url) => { + const templateId = $url.split('/').pop(); + + templatesPage.getters.useTemplateButton().click(); + cy.url().should('include', '/workflow/new'); + workflowPage.actions.saveWorkflowOnButtonClick(); + + workflowPage.actions.selectAll(); + workflowPage.actions.hitCopy(); + + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + // Check workflow JSON by copying it to clipboard + cy.readClipboard().then((workflowJSON) => { + expect(workflowJSON).to.contain(`"templateId": "${templateId}"`); + }); + }); + }); + + it('can open template with images and hides workflow screenshots', () => { + templateWorkflowPage.actions.openTemplate(WorkflowTemplate, 'https://api-staging.n8n.io'); + + templateWorkflowPage.getters.description().find('img').should('have.length', 1); + }); + + + it('renders search elements correctly', () => { + cy.visit(templatesPage.url); + templatesPage.getters.searchInput().should('exist'); + templatesPage.getters.allCategoriesFilter().should('exist'); + templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1); + templatesPage.getters.templateCards().should('have.length.greaterThan', 0); + }); + + it('can filter templates by category', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + let initialTemplateCount = 0; + let initialCollectionCount = 0; + + templatesPage.getters.templateCountLabel().then(($el) => { + initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10); + templatesPage.getters.collectionCountLabel().then(($el) => { + initialCollectionCount = parseInt($el.text().replace(/\D/g, ''), 10); + + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + + // Should have less templates and collections after selecting a category + templatesPage.getters.templateCountLabel().should(($el) => { + expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialTemplateCount); + }); + templatesPage.getters.collectionCountLabel().should(($el) => { + expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialCollectionCount); + }); + }); + }); + }); + + it('should preserve search query in URL', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.searchInput().type('auto'); + + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + cy.reload(); + + // Should preserve search query in URL + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + // Sales category should still be selected + templatesPage.getters.categoryFilter('sales').find('label').should('have.class', 'is-checked'); + // Search input should still have the search query + templatesPage.getters.searchInput().should('have.value', 'auto'); + // Sales checkbox should be pushed to the top + templatesPage.getters.categoryFilters().eq(1).then(($el) => { + expect($el.text()).to.equal('Sales'); + }); + }); +}); diff --git a/cypress/fixtures/Workflow_template_write_http_query.json b/cypress/fixtures/Workflow_template_write_http_query.json index a0a3eba649..3187bccd61 100644 --- a/cypress/fixtures/Workflow_template_write_http_query.json +++ b/cypress/fixtures/Workflow_template_write_http_query.json @@ -1,6 +1,6 @@ { "workflow": { - "id": 3, + "id": 1, "name": "Write HTTP query string on image", "views": 116, "recentViews": 9766, @@ -185,4 +185,4 @@ } ] } -} \ No newline at end of file +} diff --git a/cypress/pages/template-workflow.ts b/cypress/pages/template-workflow.ts index ff54e1e3d4..d1e8630a12 100644 --- a/cypress/pages/template-workflow.ts +++ b/cypress/pages/template-workflow.ts @@ -25,8 +25,8 @@ export class TemplateWorkflowPage extends BasePage { user: { username: string }; image: { id: number; url: string }[]; }; - }) => { - cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${template.workflow.id}`, { + }, templateHost: string) => { + cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, { statusCode: 200, body: template, }).as('getTemplate'); diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts index e72c450312..4c0225be48 100644 --- a/cypress/pages/templates.ts +++ b/cypress/pages/templates.ts @@ -23,14 +23,14 @@ export class TemplatesPage extends BasePage { cy.waitForLoad(); }, - openOnboardingFlow: (id: number, name: string, workflow: object) => { + openOnboardingFlow: (id: number, name: string, workflow: object, templatesHost: string) => { const apiResponse = { id, name, workflow, }; cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, { + cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { statusCode: 200, body: apiResponse, }).as('getTemplate'); @@ -42,13 +42,13 @@ export class TemplatesPage extends BasePage { cy.wait(['@createWorkflow', '@getWorkflow']); }, - importTemplate: (id: number, name: string, workflow: object) => { + importTemplate: (id: number, name: string, workflow: object, templatesHost: string) => { const apiResponse = { id, name, workflow, }; - cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, { + cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { statusCode: 200, body: apiResponse, }).as('getTemplate'); diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 78be40f2ad..6eb77c2883 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -118,6 +118,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { useVersionsStore } from '@/stores/versions.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useTemplatesStore } from '@/stores/templates.store'; import ExecutionsUsage from '@/components/ExecutionsUsage.vue'; import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue'; import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue'; @@ -162,6 +163,7 @@ export default defineComponent({ useCloudPlanStore, useSourceControlStore, useBecomeTemplateCreatorStore, + useTemplatesStore, ), logoPath(): string { if (this.isCollapsed) return this.basePath + 'n8n-logo-collapsed.svg'; @@ -225,13 +227,28 @@ export default defineComponent({ const regularItems: IMenuItem[] = [ workflows, { + // Link to in-app templates, available if custom templates are enabled id: 'templates', icon: 'box-open', label: this.$locale.baseText('mainSidebar.templates'), position: 'top', - available: this.settingsStore.isTemplatesEnabled, + available: + this.settingsStore.isTemplatesEnabled && this.templatesStore.hasCustomTemplatesHost, route: { to: { name: VIEWS.TEMPLATES } }, }, + { + // Link to website templates, available if custom templates are not enabled + id: 'templates', + icon: 'box-open', + label: this.$locale.baseText('mainSidebar.templates'), + position: 'top', + available: + this.settingsStore.isTemplatesEnabled && !this.templatesStore.hasCustomTemplatesHost, + link: { + href: this.templatesStore.getWebsiteTemplateRepositoryURL, + target: '_blank', + }, + }, { id: 'credentials', icon: 'key', diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index 109f21ad62..c85f36aa11 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -58,6 +58,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import type { SimplifiedNodeType } from '@/Interface'; import type { INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; +import { useTemplatesStore } from '@/stores/templates.store'; export interface NodeViewItemSection { key: string; @@ -116,6 +117,7 @@ function getAiNodesBySubcategory(nodes: INodeTypeDescription[], subcategory: str export function AIView(_nodes: SimplifiedNodeType[]): NodeView { const i18n = useI18n(); const nodeTypesStore = useNodeTypesStore(); + const templatesStore = useTemplatesStore(); const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS); const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS); @@ -124,7 +126,9 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView { value: AI_NODE_CREATOR_VIEW, title: i18n.baseText('nodeCreator.aiPanel.aiNodes'), subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'), - info: i18n.baseText('nodeCreator.aiPanel.infoBox'), + info: i18n.baseText('nodeCreator.aiPanel.infoBox', { + interpolate: { link: templatesStore.getWebsiteCategoryURL('ai') }, + }), items: [ ...chainNodes, ...agentNodes, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 251bbef520..8ea66af180 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -737,3 +737,12 @@ export const MOUSE_EVENT_BUTTONS = { BROWSER_BACK: 8, BROWSER_FORWARD: 16, } as const; + +/** + * Urls used to route users to the right template repository + */ +export const TEMPLATES_URLS = { + DEFAULT_API_HOST: 'https://api.n8n.io/api/', + BASE_WEBSITE_URL: 'https://n8n.io/workflows', + UTM_QUERY: 'utm_source=n8n_app&utm_medium=template_library', +}; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ded8ee96ff..f6ea41e2b6 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -946,7 +946,7 @@ "nodeCreator.aiPanel.newTag": "New", "nodeCreator.aiPanel.langchainAiNodes": "Advanced AI", "nodeCreator.aiPanel.title": "When should this workflow run?", - "nodeCreator.aiPanel.infoBox": "Check out our templates for workflow examples and inspiration.", + "nodeCreator.aiPanel.infoBox": "Check out our templates for workflow examples and inspiration.", "nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule", "nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval", "nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call", diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 568db9f63a..45dec9791e 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -78,7 +78,7 @@ export const routes = [ { path: '/', name: VIEWS.HOMEPAGE, - redirect: (to) => { + redirect: () => { return { name: VIEWS.WORKFLOWS }; }, meta: { @@ -130,6 +130,15 @@ export const routes = [ }, middleware: ['authenticated'], }, + beforeEnter: (to, _from, next) => { + const templatesStore = useTemplatesStore(); + if (!templatesStore.hasCustomTemplatesHost) { + const id = Array.isArray(to.params.id) ? to.params.id[0] : to.params.id; + window.location.href = templatesStore.getWebsiteTemplatePageURL(id); + } else { + next(); + } + }, }, { path: '/templates/:id/setup', @@ -180,6 +189,14 @@ export const routes = [ }, middleware: ['authenticated'], }, + beforeEnter: (_to, _from, next) => { + const templatesStore = useTemplatesStore(); + if (!templatesStore.hasCustomTemplatesHost) { + window.location.href = templatesStore.getWebsiteTemplateRepositoryURL; + } else { + next(); + } + }, }, { path: '/credentials', diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 6bb1d3be76..d1ddf42a2e 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { STORES } from '@/constants'; +import { STORES, TEMPLATES_URLS } from '@/constants'; import type { INodeUi, ITemplatesCategory, @@ -109,6 +109,38 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { ); }; }, + hasCustomTemplatesHost(): boolean { + const settingsStore = useSettingsStore(); + return settingsStore.templatesHost !== TEMPLATES_URLS.DEFAULT_API_HOST; + }, + /** + * Construct the URL for the template repository on the website + * @returns {string} + */ + getWebsiteTemplateRepositoryURL(): string { + return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?${TEMPLATES_URLS.UTM_QUERY}&utm_instance=${this.getCurrentN8nPath}`; + }, + /** + * Construct the URL for the template page on the website for a given template id + * @returns {function(string): string} + */ + getWebsiteTemplatePageURL() { + return (id: string) => { + return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/${id}?${TEMPLATES_URLS.UTM_QUERY}&utm_instance=${this.getCurrentN8nPath}`; + }; + }, + /** + * Construct the URL for the template category page on the website for a given category id + * @returns {function(string): string} + */ + getWebsiteCategoryURL() { + return (id: string) => { + return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?categories=${id}&${TEMPLATES_URLS.UTM_QUERY}&utm_instance=${this.getCurrentN8nPath}`; + }; + }, + getCurrentN8nPath(): string { + return `${window.location.host}${window.BASE_PATH}`; + }, }, actions: { addCategories(categories: ITemplatesCategory[]): void {