feat(editor): Use website as the main templates repository (#8591)

This commit is contained in:
Milorad FIlipović
2024-02-09 13:47:43 +01:00
committed by GitHub
parent 5ab34fe335
commit 79b09fdf84
12 changed files with 281 additions and 154 deletions

View File

@@ -1,139 +1,44 @@
import { TemplatesPage } from '../pages/templates'; import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowsPage } from '../pages/workflows';
import { MainSidebar } from '../pages/sidebar/main-sidebar';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
import { TemplateWorkflowPage } from '../pages/template-workflow';
const templatesPage = new TemplatesPage(); const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage(); const workflowsPage = new WorkflowsPage();
const templateWorkflowPage = new TemplateWorkflowPage(); const mainSidebar = new MainSidebar();
describe('Templates', () => { describe('Workflow templates', () => {
beforeEach(() => { 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', '**/rest/settings', (req) => {
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest'); // Disable cache
cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest'); delete req.headers['if-none-match']
cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest'); 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', () => { it('Opens website when clicking templates sidebar link', () => {
templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow); cy.visit(workflowsPage.url);
cy.url().then(($url) => { mainSidebar.getters.menuItem('Templates').should('be.visible');
expect($url).to.match(/.*\/workflow\/.*?onboardingId=1234$/); // 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', () => { it('Redirects to website when visiting template by id page directly', () => {
templatesPage.actions.importTemplate(1234, OnboardingWorkflow.name, OnboardingWorkflow); cy.visit(`${templatesPage.url}/1`);
cy.origin('https://n8n.io', () => {
cy.url().then(($url) => { cy.url().should('include', 'https://n8n.io/workflows/1');
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');
});
}); });
}); });

View File

@@ -4,13 +4,11 @@ import {
testData, testData,
} from '../pages/template-collection'; } from '../pages/template-collection';
import * as templateCredentialsSetupPage from '../pages/template-credential-setup'; import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
import { TemplateWorkflowPage } from '../pages/template-workflow';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step'; import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal'; import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
const templateWorkflowPage = new TemplateWorkflowPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate; 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}`, { cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
fixture: testTemplate.fixture, fixture: testTemplate.fixture,
}); });
}); cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
it('can be opened from template workflow page', () => { delete req.headers['if-none-match']
templateWorkflowPage.actions.visit(testTemplate.id); req.reply((res) => {
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); if (res.body.data) {
templateWorkflowPage.getters.useTemplateButton().should('be.visible'); // Disable custom templates host if it has been overridden by another intercept
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
templateWorkflowPage.actions.clickUseThisWorkflowButton(); }
});
templateCredentialsSetupPage.getters }).as('settingsRequest');
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
}); });
it('can be opened from template collection page', () => { it('can be opened from template collection page', () => {

View File

@@ -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');
});
});
});

View File

@@ -1,6 +1,6 @@
{ {
"workflow": { "workflow": {
"id": 3, "id": 1,
"name": "Write HTTP query string on image", "name": "Write HTTP query string on image",
"views": 116, "views": 116,
"recentViews": 9766, "recentViews": 9766,
@@ -185,4 +185,4 @@
} }
] ]
} }
} }

View File

@@ -25,8 +25,8 @@ export class TemplateWorkflowPage extends BasePage {
user: { username: string }; user: { username: string };
image: { id: number; url: string }[]; image: { id: number; url: string }[];
}; };
}) => { }, templateHost: string) => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${template.workflow.id}`, { cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, {
statusCode: 200, statusCode: 200,
body: template, body: template,
}).as('getTemplate'); }).as('getTemplate');

View File

@@ -23,14 +23,14 @@ export class TemplatesPage extends BasePage {
cy.waitForLoad(); cy.waitForLoad();
}, },
openOnboardingFlow: (id: number, name: string, workflow: object) => { openOnboardingFlow: (id: number, name: string, workflow: object, templatesHost: string) => {
const apiResponse = { const apiResponse = {
id, id,
name, name,
workflow, workflow,
}; };
cy.intercept('POST', '/rest/workflows').as('createWorkflow'); 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, statusCode: 200,
body: apiResponse, body: apiResponse,
}).as('getTemplate'); }).as('getTemplate');
@@ -42,13 +42,13 @@ export class TemplatesPage extends BasePage {
cy.wait(['@createWorkflow', '@getWorkflow']); cy.wait(['@createWorkflow', '@getWorkflow']);
}, },
importTemplate: (id: number, name: string, workflow: object) => { importTemplate: (id: number, name: string, workflow: object, templatesHost: string) => {
const apiResponse = { const apiResponse = {
id, id,
name, name,
workflow, workflow,
}; };
cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, { cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, {
statusCode: 200, statusCode: 200,
body: apiResponse, body: apiResponse,
}).as('getTemplate'); }).as('getTemplate');

View File

@@ -118,6 +118,7 @@ import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useVersionsStore } from '@/stores/versions.store'; import { useVersionsStore } from '@/stores/versions.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useTemplatesStore } from '@/stores/templates.store';
import ExecutionsUsage from '@/components/ExecutionsUsage.vue'; import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue'; import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue';
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue'; import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
@@ -162,6 +163,7 @@ export default defineComponent({
useCloudPlanStore, useCloudPlanStore,
useSourceControlStore, useSourceControlStore,
useBecomeTemplateCreatorStore, useBecomeTemplateCreatorStore,
useTemplatesStore,
), ),
logoPath(): string { logoPath(): string {
if (this.isCollapsed) return this.basePath + 'n8n-logo-collapsed.svg'; if (this.isCollapsed) return this.basePath + 'n8n-logo-collapsed.svg';
@@ -225,13 +227,28 @@ export default defineComponent({
const regularItems: IMenuItem[] = [ const regularItems: IMenuItem[] = [
workflows, workflows,
{ {
// Link to in-app templates, available if custom templates are enabled
id: 'templates', id: 'templates',
icon: 'box-open', icon: 'box-open',
label: this.$locale.baseText('mainSidebar.templates'), label: this.$locale.baseText('mainSidebar.templates'),
position: 'top', position: 'top',
available: this.settingsStore.isTemplatesEnabled, available:
this.settingsStore.isTemplatesEnabled && this.templatesStore.hasCustomTemplatesHost,
route: { to: { name: VIEWS.TEMPLATES } }, 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', id: 'credentials',
icon: 'key', icon: 'key',

View File

@@ -58,6 +58,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { SimplifiedNodeType } from '@/Interface'; import type { SimplifiedNodeType } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { useTemplatesStore } from '@/stores/templates.store';
export interface NodeViewItemSection { export interface NodeViewItemSection {
key: string; key: string;
@@ -116,6 +117,7 @@ function getAiNodesBySubcategory(nodes: INodeTypeDescription[], subcategory: str
export function AIView(_nodes: SimplifiedNodeType[]): NodeView { export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
const i18n = useI18n(); const i18n = useI18n();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const templatesStore = useTemplatesStore();
const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS); const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS);
const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS); const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS);
@@ -124,7 +126,9 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
value: AI_NODE_CREATOR_VIEW, value: AI_NODE_CREATOR_VIEW,
title: i18n.baseText('nodeCreator.aiPanel.aiNodes'), title: i18n.baseText('nodeCreator.aiPanel.aiNodes'),
subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'), subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'),
info: i18n.baseText('nodeCreator.aiPanel.infoBox'), info: i18n.baseText('nodeCreator.aiPanel.infoBox', {
interpolate: { link: templatesStore.getWebsiteCategoryURL('ai') },
}),
items: [ items: [
...chainNodes, ...chainNodes,
...agentNodes, ...agentNodes,

View File

@@ -737,3 +737,12 @@ export const MOUSE_EVENT_BUTTONS = {
BROWSER_BACK: 8, BROWSER_BACK: 8,
BROWSER_FORWARD: 16, BROWSER_FORWARD: 16,
} as const; } 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',
};

View File

@@ -946,7 +946,7 @@
"nodeCreator.aiPanel.newTag": "New", "nodeCreator.aiPanel.newTag": "New",
"nodeCreator.aiPanel.langchainAiNodes": "Advanced AI", "nodeCreator.aiPanel.langchainAiNodes": "Advanced AI",
"nodeCreator.aiPanel.title": "When should this workflow run?", "nodeCreator.aiPanel.title": "When should this workflow run?",
"nodeCreator.aiPanel.infoBox": "Check out our <a href=\"/collections/8\" target=\"_blank\">templates</a> for workflow examples and inspiration.", "nodeCreator.aiPanel.infoBox": "Check out our <a href=\"{link}\" target=\"_blank\">templates</a> for workflow examples and inspiration.",
"nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule", "nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule",
"nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval", "nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
"nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call", "nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call",

View File

@@ -78,7 +78,7 @@ export const routes = [
{ {
path: '/', path: '/',
name: VIEWS.HOMEPAGE, name: VIEWS.HOMEPAGE,
redirect: (to) => { redirect: () => {
return { name: VIEWS.WORKFLOWS }; return { name: VIEWS.WORKFLOWS };
}, },
meta: { meta: {
@@ -130,6 +130,15 @@ export const routes = [
}, },
middleware: ['authenticated'], 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', path: '/templates/:id/setup',
@@ -180,6 +189,14 @@ export const routes = [
}, },
middleware: ['authenticated'], middleware: ['authenticated'],
}, },
beforeEnter: (_to, _from, next) => {
const templatesStore = useTemplatesStore();
if (!templatesStore.hasCustomTemplatesHost) {
window.location.href = templatesStore.getWebsiteTemplateRepositoryURL;
} else {
next();
}
},
}, },
{ {
path: '/credentials', path: '/credentials',

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { STORES } from '@/constants'; import { STORES, TEMPLATES_URLS } from '@/constants';
import type { import type {
INodeUi, INodeUi,
ITemplatesCategory, 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: { actions: {
addCategories(categories: ITemplatesCategory[]): void { addCategories(categories: ITemplatesCategory[]): void {