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 {