mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Use website as the main templates repository (#8591)
This commit is contained in:
committed by
GitHub
parent
5ab34fe335
commit
79b09fdf84
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
147
cypress/e2e/38-custom-template-repository.cy.ts
Normal file
147
cypress/e2e/38-custom-template-repository.cy.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user