mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
feat(editor): Add lead enrichment suggestions to workflow list (#8042)
## Summary We want to show lead enrichment template suggestions to cloud users that agreed to this. This PR introduces the front-end part of this feature - Handoff document - Figma Hi-fi - [How to test](https://linear.app/n8n/issue/ADO-1549/[n8n-fe]-update-workflows-list-page-to-show-fake-door-templates#comment-b6644c99) Tests are being worked on in a separate PR ## Related tickets and issues Fixes ADO-1546 Fixes ADO-1549 Fixes ADO-1604 ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --------- Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
parent
c170dd1da3
commit
36a923cf7b
143
cypress/e2e/36-suggested-templates.cy.ts
Normal file
143
cypress/e2e/36-suggested-templates.cy.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||||
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
||||||
|
type SuggestedTemplatesStub = {
|
||||||
|
sections: SuggestedTemplatesSectionStub[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuggestedTemplatesSectionStub = {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
workflows: Array<Object>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowsListPage = new WorkflowsPageClass();
|
||||||
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
|
||||||
|
let fixtureSections: SuggestedTemplatesStub = { sections: [] };;
|
||||||
|
|
||||||
|
describe('Suggested templates - Should render', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.fixture('Suggested_Templates.json').then((data) => {
|
||||||
|
fixtureSections = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
|
||||||
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { ...res.body.data, deployment: { type: 'cloud' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).as('loadSettings');
|
||||||
|
cy.intercept('GET', '/rest/cloud/proxy/templates', {
|
||||||
|
fixture: 'Suggested_Templates.json',
|
||||||
|
});
|
||||||
|
cy.visit(WorkflowsListPage.url);
|
||||||
|
cy.wait('@loadSettings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render suggested templates page in empty workflow list', () => {
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionDescription().should('contain', fixtureSections.sections[0].description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render suggested templates when there are workflows in the list', () => {
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesNewWorkflowButton().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||||
|
cy.visit(WorkflowsListPage.url);
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('exist');
|
||||||
|
cy.contains(`Explore ${fixtureSections.sections[0].name.toLocaleLowerCase()} workflow templates`).should('exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable users to signup for suggested templates templates', () => {
|
||||||
|
// Test the whole flow
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesCards().first().click();
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPreviewModal().should('exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesUseTemplateButton().click();
|
||||||
|
cy.url().should('include', '/workflow/new');
|
||||||
|
WorkflowPage.getters.infoToast().should('contain', 'Template coming soon!');
|
||||||
|
WorkflowPage.getters.infoToast().contains('Notify me when it\'s available').click();
|
||||||
|
WorkflowPage.getters.successToast().should('contain', 'We will contact you via email once this template is released.');
|
||||||
|
cy.visit(WorkflowsListPage.url);
|
||||||
|
// Once users have signed up for a template, suggestions should not be shown again
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Suggested templates - Should not render', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
|
||||||
|
cy.visit(WorkflowsListPage.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render suggested templates templates if not in cloud deployment', () => {
|
||||||
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { ...res.body.data, deployment: { type: 'notCloud' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render suggested templates templates if endpoint throws error', () => {
|
||||||
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { ...res.body.data, deployment: { type: 'cloud' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.intercept('GET', '/rest/cloud/proxy/templates', { statusCode: 500 }).as('loadTemplates');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render suggested templates templates if endpoint returns empty list', () => {
|
||||||
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { ...res.body.data, deployment: { type: 'cloud' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { collections: [] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render suggested templates templates if endpoint returns invalid response', () => {
|
||||||
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { ...res.body.data, deployment: { type: 'cloud' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
|
||||||
|
req.on('response', (res) => {
|
||||||
|
res.send({
|
||||||
|
data: { somethingElse: [] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
|
||||||
|
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
655
cypress/fixtures/Suggested_Templates.json
Normal file
655
cypress/fixtures/Suggested_Templates.json
Normal file
|
@ -0,0 +1,655 @@
|
||||||
|
{
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"name": "Lead enrichment",
|
||||||
|
"description": "Explore curated lead enrichment workflows or start fresh with a blank canvas",
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"title": "Score new leads with AI from Facebook Lead Ads with AI and get notifications for high scores on Slack",
|
||||||
|
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
|
||||||
|
"preview": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "create",
|
||||||
|
"base": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"mappingMode": "defineBelow",
|
||||||
|
"value": {},
|
||||||
|
"matchingColumns": [],
|
||||||
|
"schema": []
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
|
||||||
|
"name": "Airtable",
|
||||||
|
"type": "n8n-nodes-base.airtable",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1800,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
920,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
|
||||||
|
"name": "Get image",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.1,
|
||||||
|
"position": [
|
||||||
|
1140,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "extractHtmlContent",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
|
||||||
|
"name": "Extract Information",
|
||||||
|
"type": "n8n-nodes-base.html",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
|
||||||
|
"name": "Set Information",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get image",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get image": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Extract Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Extract Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Set Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Airtable",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"icon": "fa:code-branch",
|
||||||
|
"defaults": {
|
||||||
|
"color": "#00bbcc"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code-branch",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"displayName": "Merge"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Verify the email address every time a contact is created in HubSpot",
|
||||||
|
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
|
||||||
|
"preview": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "create",
|
||||||
|
"base": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"mappingMode": "defineBelow",
|
||||||
|
"value": {},
|
||||||
|
"matchingColumns": [],
|
||||||
|
"schema": []
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
|
||||||
|
"name": "Airtable",
|
||||||
|
"type": "n8n-nodes-base.airtable",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1800,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
920,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
|
||||||
|
"name": "Get image",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.1,
|
||||||
|
"position": [
|
||||||
|
1140,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "extractHtmlContent",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
|
||||||
|
"name": "Extract Information",
|
||||||
|
"type": "n8n-nodes-base.html",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
|
||||||
|
"name": "Set Information",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get image",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get image": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Extract Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Extract Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Set Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Airtable",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"icon": "fa:code",
|
||||||
|
"name": "n8n-nodes-base.function",
|
||||||
|
"defaults": {
|
||||||
|
"name": "Function",
|
||||||
|
"color": "#FF9922"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Core Nodes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"displayName": "Function",
|
||||||
|
"typeVersion": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"icon": "fa:code-branch",
|
||||||
|
"name": "n8n-nodes-base.merge",
|
||||||
|
"defaults": {
|
||||||
|
"name": "Merge",
|
||||||
|
"color": "#00bbcc"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code-branch",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Core Nodes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"displayName": "Merge",
|
||||||
|
"typeVersion": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Enrich leads from HubSpot with company information via OpenAi",
|
||||||
|
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
|
||||||
|
"preview": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "create",
|
||||||
|
"base": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"mappingMode": "defineBelow",
|
||||||
|
"value": {},
|
||||||
|
"matchingColumns": [],
|
||||||
|
"schema": []
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
|
||||||
|
"name": "Airtable",
|
||||||
|
"type": "n8n-nodes-base.airtable",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1800,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
920,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
|
||||||
|
"name": "Get image",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.1,
|
||||||
|
"position": [
|
||||||
|
1140,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "extractHtmlContent",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
|
||||||
|
"name": "Extract Information",
|
||||||
|
"type": "n8n-nodes-base.html",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
|
||||||
|
"name": "Set Information",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get image",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get image": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Extract Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Extract Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Set Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Airtable",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"icon": "fa:code",
|
||||||
|
"defaults": {
|
||||||
|
"name": "Function",
|
||||||
|
"color": "#FF9922"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"displayName": "Function"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Score new lead submissions from Facebook Lead Ads with AI and notify me on Slack when it is a high score lead",
|
||||||
|
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
|
||||||
|
"preview": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "create",
|
||||||
|
"base": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "list",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"mappingMode": "defineBelow",
|
||||||
|
"value": {},
|
||||||
|
"matchingColumns": [],
|
||||||
|
"schema": []
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
|
||||||
|
"name": "Airtable",
|
||||||
|
"type": "n8n-nodes-base.airtable",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1800,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
920,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
|
||||||
|
"name": "Get image",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.1,
|
||||||
|
"position": [
|
||||||
|
1140,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "extractHtmlContent",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
|
||||||
|
"name": "Extract Information",
|
||||||
|
"type": "n8n-nodes-base.html",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
|
||||||
|
"name": "Set Information",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
740
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get image",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get image": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Extract Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Extract Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set Information",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Set Information": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Airtable",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"icon": "fa:code",
|
||||||
|
"defaults": {
|
||||||
|
"name": "Function",
|
||||||
|
"color": "#FF9922"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"displayName": "Function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"icon": "fa:code-branch",
|
||||||
|
"defaults": {
|
||||||
|
"name": "Merge",
|
||||||
|
"color": "#00bbcc"
|
||||||
|
},
|
||||||
|
"iconData": {
|
||||||
|
"icon": "code-branch",
|
||||||
|
"type": "icon"
|
||||||
|
},
|
||||||
|
"displayName": "Merge"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ export class WorkflowPage extends BasePage {
|
||||||
successToast: () => cy.get('.el-notification:has(.el-notification--success)'),
|
successToast: () => cy.get('.el-notification:has(.el-notification--success)'),
|
||||||
warningToast: () => cy.get('.el-notification:has(.el-notification--warning)'),
|
warningToast: () => cy.get('.el-notification:has(.el-notification--warning)'),
|
||||||
errorToast: () => cy.get('.el-notification:has(.el-notification--error)'),
|
errorToast: () => cy.get('.el-notification:has(.el-notification--error)'),
|
||||||
|
infoToast: () => cy.get('.el-notification:has(.el-notification--info)'),
|
||||||
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
||||||
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
||||||
firstStepButton: () => cy.getByTestId('canvas-add-button'),
|
firstStepButton: () => cy.getByTestId('canvas-add-button'),
|
||||||
|
|
|
@ -34,6 +34,13 @@ export class WorkflowsPage extends BasePage {
|
||||||
// Not yet implemented
|
// Not yet implemented
|
||||||
// myWorkflows: () => cy.getByTestId('my-workflows'),
|
// myWorkflows: () => cy.getByTestId('my-workflows'),
|
||||||
// allWorkflows: () => cy.getByTestId('all-workflows'),
|
// allWorkflows: () => cy.getByTestId('all-workflows'),
|
||||||
|
suggestedTemplatesPageContainer: () => cy.getByTestId('suggested-templates-page-container'),
|
||||||
|
suggestedTemplatesCards: () => cy.getByTestId('templates-info-card').filter(':visible'),
|
||||||
|
suggestedTemplatesNewWorkflowButton: () => cy.getByTestId('suggested-templates-new-workflow-button'),
|
||||||
|
suggestedTemplatesSectionContainer: () => cy.getByTestId('suggested-templates-section-container'),
|
||||||
|
suggestedTemplatesPreviewModal: () => cy.getByTestId('suggested-templates-preview-modal'),
|
||||||
|
suggestedTemplatesUseTemplateButton: () => cy.getByTestId('use-template-button'),
|
||||||
|
suggestedTemplatesSectionDescription: () => cy.getByTestId('suggested-template-section-description'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
|
|
@ -236,6 +236,9 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="post-list-container">
|
||||||
|
<slot name="postListContent" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -263,4 +266,7 @@ export default defineComponent({
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.post-list-container {
|
||||||
|
margin-top: var(--spacing-3xl);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,5 +9,6 @@ exports[`components > N8nRecycleScroller > should render correctly 1`] = `
|
||||||
<div class=\\"recycle-scroller-item\\"></div>
|
<div class=\\"recycle-scroller-item\\"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class=\\"post-list-container\\"></div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -6,46 +6,47 @@ import type {
|
||||||
TRIGGER_NODE_CREATOR_VIEW,
|
TRIGGER_NODE_CREATOR_VIEW,
|
||||||
REGULAR_NODE_CREATOR_VIEW,
|
REGULAR_NODE_CREATOR_VIEW,
|
||||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||||
|
VIEWS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import type { IMenuItem } from 'n8n-design-system';
|
import type { IMenuItem } from 'n8n-design-system';
|
||||||
import type {
|
import {
|
||||||
GenericValue,
|
type GenericValue,
|
||||||
IConnections,
|
type IConnections,
|
||||||
ICredentialsDecrypted,
|
type ICredentialsDecrypted,
|
||||||
ICredentialsEncrypted,
|
type ICredentialsEncrypted,
|
||||||
ICredentialType,
|
type ICredentialType,
|
||||||
IDataObject,
|
type IDataObject,
|
||||||
INode,
|
type INode,
|
||||||
INodeIssues,
|
type INodeIssues,
|
||||||
INodeParameters,
|
type INodeParameters,
|
||||||
INodeTypeDescription,
|
type INodeTypeDescription,
|
||||||
IPinData,
|
type IPinData,
|
||||||
IRunExecutionData,
|
type IRunExecutionData,
|
||||||
IRun,
|
type IRun,
|
||||||
IRunData,
|
type IRunData,
|
||||||
ITaskData,
|
type ITaskData,
|
||||||
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
type IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||||
WorkflowExecuteMode,
|
type WorkflowExecuteMode,
|
||||||
PublicInstalledPackage,
|
type PublicInstalledPackage,
|
||||||
INodeTypeNameVersion,
|
type INodeTypeNameVersion,
|
||||||
ILoadOptions,
|
type ILoadOptions,
|
||||||
INodeCredentials,
|
type INodeCredentials,
|
||||||
INodeListSearchItems,
|
type INodeListSearchItems,
|
||||||
NodeParameterValueType,
|
type NodeParameterValueType,
|
||||||
IDisplayOptions,
|
type IDisplayOptions,
|
||||||
IExecutionsSummary,
|
type IExecutionsSummary,
|
||||||
FeatureFlags,
|
type FeatureFlags,
|
||||||
ExecutionStatus,
|
type ExecutionStatus,
|
||||||
ITelemetryTrackProperties,
|
type ITelemetryTrackProperties,
|
||||||
IUserManagementSettings,
|
type IUserManagementSettings,
|
||||||
WorkflowSettings,
|
type WorkflowSettings,
|
||||||
IUserSettings,
|
type IUserSettings,
|
||||||
IN8nUISettings,
|
type IN8nUISettings,
|
||||||
BannerName,
|
type BannerName,
|
||||||
INodeExecutionData,
|
type INodeExecutionData,
|
||||||
INodeProperties,
|
type INodeProperties,
|
||||||
NodeConnectionType,
|
type NodeConnectionType,
|
||||||
INodeCredentialsDetails,
|
type INodeCredentialsDetails,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { BulkCommand, Undoable } from '@/models/history';
|
import type { BulkCommand, Undoable } from '@/models/history';
|
||||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||||
|
@ -1259,6 +1260,10 @@ export interface UIState {
|
||||||
bannersHeight: number;
|
bannersHeight: number;
|
||||||
bannerStack: BannerName[];
|
bannerStack: BannerName[];
|
||||||
theme: ThemeOption;
|
theme: ThemeOption;
|
||||||
|
suggestedTemplates?: SuggestedTemplates;
|
||||||
|
pendingNotificationsForViews: {
|
||||||
|
[key in VIEWS]?: NotificationOptions[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IFakeDoor = {
|
export type IFakeDoor = {
|
||||||
|
@ -1836,3 +1841,21 @@ export type ToggleNodeCreatorOptions = {
|
||||||
|
|
||||||
export type AppliedThemeOption = 'light' | 'dark';
|
export type AppliedThemeOption = 'light' | 'dark';
|
||||||
export type ThemeOption = AppliedThemeOption | 'system';
|
export type ThemeOption = AppliedThemeOption | 'system';
|
||||||
|
|
||||||
|
export type SuggestedTemplates = {
|
||||||
|
sections: SuggestedTemplatesSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SuggestedTemplatesSection = {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
workflows: SuggestedTemplatesWorkflowPreview[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SuggestedTemplatesWorkflowPreview = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
preview: IWorkflowData;
|
||||||
|
nodes: Array<Pick<ITemplatesNode, 'id' | 'displayName' | 'icon' | 'defaults' | 'iconData'>>;
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Cloud, IRestApiContext, InstanceUsage } from '@/Interface';
|
import type { Cloud, IRestApiContext, InstanceUsage, LeadEnrichmentTemplates } from '@/Interface';
|
||||||
import { get, post } from '@/utils/apiUtils';
|
import { get, post } from '@/utils/apiUtils';
|
||||||
|
|
||||||
export async function getCurrentPlan(context: IRestApiContext): Promise<Cloud.PlanData> {
|
export async function getCurrentPlan(context: IRestApiContext): Promise<Cloud.PlanData> {
|
||||||
|
@ -20,3 +20,9 @@ export async function confirmEmail(context: IRestApiContext): Promise<Cloud.User
|
||||||
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
|
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
|
||||||
return get(context.baseUrl, '/cloud/proxy/login/code');
|
return get(context.baseUrl, '/cloud/proxy/login/code');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSuggestedTemplates(
|
||||||
|
context: IRestApiContext,
|
||||||
|
): Promise<LeadEnrichmentTemplates> {
|
||||||
|
return get(context.baseUrl, '/cloud/proxy/templates');
|
||||||
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.card {
|
.card {
|
||||||
width: 240px !important;
|
min-width: 180px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
margin-right: var(--spacing-2xs);
|
margin-right: var(--spacing-2xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -149,6 +149,15 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
<ModalRoot :name="SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY">
|
||||||
|
<template #default="{ modalName, data }">
|
||||||
|
<SuggestedTemplatesPreviewModal
|
||||||
|
data-test-id="suggested-templates-preview-modal"
|
||||||
|
:modalName="modalName"
|
||||||
|
:data="data"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -183,6 +192,7 @@ import {
|
||||||
DEBUG_PAYWALL_MODAL_KEY,
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
MFA_SETUP_MODAL_KEY,
|
MFA_SETUP_MODAL_KEY,
|
||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
|
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import AboutModal from './AboutModal.vue';
|
import AboutModal from './AboutModal.vue';
|
||||||
|
@ -214,6 +224,7 @@ import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
|
||||||
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
|
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
|
||||||
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
|
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
|
||||||
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
|
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
|
||||||
|
import SuggestedTemplatesPreviewModal from '@/components/SuggestedTemplates/SuggestedTemplatesPreviewModal.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modals',
|
name: 'Modals',
|
||||||
|
@ -247,6 +258,7 @@ export default defineComponent({
|
||||||
DebugPaywallModal,
|
DebugPaywallModal,
|
||||||
MfaSetupModal,
|
MfaSetupModal,
|
||||||
WorkflowHistoryVersionRestoreModal,
|
WorkflowHistoryVersionRestoreModal,
|
||||||
|
SuggestedTemplatesPreviewModal,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
CHAT_EMBED_MODAL_KEY,
|
CHAT_EMBED_MODAL_KEY,
|
||||||
|
@ -277,6 +289,7 @@ export default defineComponent({
|
||||||
DEBUG_PAYWALL_MODAL_KEY,
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
MFA_SETUP_MODAL_KEY,
|
MFA_SETUP_MODAL_KEY,
|
||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
|
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
v-if="!data.disabled"
|
v-if="!data.disabled"
|
||||||
:class="{ 'node-info-icon': true, 'shift-icon': shiftOutputCount }"
|
:class="{ 'node-info-icon': true, 'shift-icon': shiftOutputCount }"
|
||||||
>
|
>
|
||||||
<div v-if="hasIssues" class="node-issues" data-test-id="node-issues">
|
<div v-if="hasIssues && !hideNodeIssues" class="node-issues" data-test-id="node-issues">
|
||||||
<n8n-tooltip :show-after="500" placement="bottom">
|
<n8n-tooltip :show-after="500" placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<titled-list :title="`${$locale.baseText('node.issues')}:`" :items="nodeIssues" />
|
<titled-list :title="`${$locale.baseText('node.issues')}:`" :items="nodeIssues" />
|
||||||
|
@ -212,6 +212,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
hideNodeIssues: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
|
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
|
||||||
|
@ -487,7 +491,7 @@ export default defineComponent({
|
||||||
if (this.data.disabled) {
|
if (this.data.disabled) {
|
||||||
borderColor = '--color-foreground-base';
|
borderColor = '--color-foreground-base';
|
||||||
} else if (!this.isExecuting) {
|
} else if (!this.isExecuting) {
|
||||||
if (this.hasIssues) {
|
if (this.hasIssues && !this.hideNodeIssues) {
|
||||||
// Do not set red border if there is an issue with the configuration node
|
// Do not set red border if there is an issue with the configuration node
|
||||||
if (
|
if (
|
||||||
(this.nodeRunData?.[0]?.error as NodeOperationError)?.functionality !==
|
(this.nodeRunData?.[0]?.error as NodeOperationError)?.functionality !==
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import type { ITemplatesCollection } from '@/Interface';
|
||||||
|
import SuggestedTemplatesSection from '@/components/SuggestedTemplates/SuggestedTemplatesSection.vue';
|
||||||
|
import type { IUser } from '@/Interface';
|
||||||
|
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentUser = computed(() => usersStore.currentUser);
|
||||||
|
|
||||||
|
const upperCaseFirstName = (user: IUser | null) => {
|
||||||
|
if (!user || !user.firstName) return;
|
||||||
|
return user.firstName?.charAt(0)?.toUpperCase() + user?.firstName?.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSection = computed(() => {
|
||||||
|
if (!uiStore.suggestedTemplates) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return uiStore.suggestedTemplates.sections[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const suggestedTemplates = computed(() => {
|
||||||
|
const carouselCollections = Array<ITemplatesCollection>();
|
||||||
|
if (!uiStore.suggestedTemplates || !defaultSection.value) {
|
||||||
|
return carouselCollections;
|
||||||
|
}
|
||||||
|
defaultSection.value.workflows.forEach((workflow, index) => {
|
||||||
|
carouselCollections.push({
|
||||||
|
id: index,
|
||||||
|
name: workflow.title,
|
||||||
|
workflows: [{ id: index }],
|
||||||
|
nodes: workflow.nodes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return carouselCollections;
|
||||||
|
});
|
||||||
|
|
||||||
|
function openCanvas() {
|
||||||
|
uiStore.nodeViewInitialized = false;
|
||||||
|
void router.push({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
currentUser,
|
||||||
|
openCanvas,
|
||||||
|
suggestedTemplates,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.container" data-test-id="suggested-templates-page-container">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<n8n-heading tag="h1" size="2xlarge" class="mb-2xs">
|
||||||
|
{{
|
||||||
|
$locale.baseText('suggestedTemplates.heading', {
|
||||||
|
interpolate: {
|
||||||
|
name: upperCaseFirstName(currentUser) || $locale.baseText('generic.welcome'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</n8n-heading>
|
||||||
|
<n8n-text
|
||||||
|
size="large"
|
||||||
|
color="text-base"
|
||||||
|
data-test-id="suggested-template-section-description"
|
||||||
|
>
|
||||||
|
{{ defaultSection?.description }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.content">
|
||||||
|
<suggested-templates-section
|
||||||
|
v-for="section in uiStore.suggestedTemplates?.sections"
|
||||||
|
:key="section.title"
|
||||||
|
:section="section"
|
||||||
|
:showTitle="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<n8n-button
|
||||||
|
:label="$locale.baseText('suggestedTemplates.newWorkflowButton')"
|
||||||
|
type="secondary"
|
||||||
|
size="medium"
|
||||||
|
icon="plus"
|
||||||
|
data-test-id="suggested-templates-new-workflow-button"
|
||||||
|
@click="openCanvas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import {
|
||||||
|
SUGGESTED_TEMPLATES_FLAG,
|
||||||
|
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
|
||||||
|
VIEWS,
|
||||||
|
} from '@/constants';
|
||||||
|
import type { IWorkflowDb, SuggestedTemplatesWorkflowPreview } from '@/Interface';
|
||||||
|
import Modal from '@/components/Modal.vue';
|
||||||
|
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modalName: string;
|
||||||
|
data: {
|
||||||
|
workflow: SuggestedTemplatesWorkflowPreview;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const toast = useToast();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
|
function showConfirmationMessage(event: PointerEvent) {
|
||||||
|
if (event.target instanceof HTMLAnchorElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
// @ts-expect-error Additional parameters are not necessary for this function
|
||||||
|
toast.showMessage({
|
||||||
|
title: i18n.baseText('suggestedTemplates.notification.confirmation.title'),
|
||||||
|
message: i18n.baseText('suggestedTemplates.notification.confirmation.message'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
telemetry.track(
|
||||||
|
'User wants to be notified once template is ready',
|
||||||
|
{ templateName: props.data.workflow.title, email: usersStore.currentUser?.email },
|
||||||
|
{
|
||||||
|
withPostHog: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
localStorage.setItem(SUGGESTED_TEMPLATES_FLAG, 'false');
|
||||||
|
uiStore.deleteSuggestedTemplates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCanvas() {
|
||||||
|
uiStore.setNotificationsForView(VIEWS.WORKFLOW, [
|
||||||
|
{
|
||||||
|
title: i18n.baseText('suggestedTemplates.notification.comingSoon.title'),
|
||||||
|
message: i18n.baseText('suggestedTemplates.notification.comingSoon.message'),
|
||||||
|
type: 'info',
|
||||||
|
onClick: showConfirmationMessage,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
uiStore.closeModal(SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY);
|
||||||
|
uiStore.nodeViewInitialized = false;
|
||||||
|
void router.push({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
telemetry.track(
|
||||||
|
'User clicked Use Template button',
|
||||||
|
{ templateName: props.data.workflow.title },
|
||||||
|
{ withPostHog: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal width="900px" height="640px" :name="props.modalName">
|
||||||
|
<template #header>
|
||||||
|
<n8n-heading tag="h2" size="xlarge">
|
||||||
|
{{ $props.data.workflow.title }}
|
||||||
|
</n8n-heading>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<workflow-preview
|
||||||
|
:loading="false"
|
||||||
|
:workflow="$props.data.workflow.preview as IWorkflowDb"
|
||||||
|
:canOpenNDV="false"
|
||||||
|
:hideNodeIssues="true"
|
||||||
|
@close="uiStore.closeModal(SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<n8n-text> {{ $props.data.workflow.description }} </n8n-text>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.footerButtons">
|
||||||
|
<n8n-button
|
||||||
|
@click="openCanvas"
|
||||||
|
float="right"
|
||||||
|
data-test-id="use-template-button"
|
||||||
|
:label="$locale.baseText('suggestedTemplates.modal.button.label')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.footerButtons {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type PropType, computed } from 'vue';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import type { ITemplatesCollection, ITemplatesNode, SuggestedTemplatesSection } from '@/Interface';
|
||||||
|
import TemplatesInfoCarousel from '@/components/TemplatesInfoCarousel.vue';
|
||||||
|
import { SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
section: {
|
||||||
|
type: Object as PropType<SuggestedTemplatesSection>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
showTitle: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionTemplates = computed(() => {
|
||||||
|
const carouselCollections = Array<ITemplatesCollection>();
|
||||||
|
if (!uiStore.suggestedTemplates) {
|
||||||
|
return carouselCollections;
|
||||||
|
}
|
||||||
|
props.section.workflows.forEach((workflow, index) => {
|
||||||
|
carouselCollections.push({
|
||||||
|
id: index,
|
||||||
|
name: workflow.title,
|
||||||
|
workflows: [{ id: index }],
|
||||||
|
nodes: workflow.nodes as ITemplatesNode[],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return carouselCollections;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOpenCollection({ id }: { event: Event; id: number }) {
|
||||||
|
uiStore.openModalWithData({
|
||||||
|
name: SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
|
||||||
|
data: { workflow: props.section.workflows[id] },
|
||||||
|
});
|
||||||
|
telemetry.track(
|
||||||
|
'User clicked template recommendation',
|
||||||
|
{ templateName: props.section.workflows[id].title },
|
||||||
|
{ withPostHog: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.container" data-test-id="suggested-templates-section-container">
|
||||||
|
<div v-if="showTitle" :class="$style.header">
|
||||||
|
<n8n-text size="large" color="text-base" :bold="true">
|
||||||
|
{{ props.title ?? section.title }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.content">
|
||||||
|
<templates-info-carousel
|
||||||
|
:collections="sectionTemplates"
|
||||||
|
:loading="false"
|
||||||
|
:showItemCount="false"
|
||||||
|
:showNavigation="false"
|
||||||
|
cardsWidth="24%"
|
||||||
|
@openCollection="onOpenCollection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Card :loading="loading" :title="collection.name">
|
<Card :loading="loading" :title="collection.name" :style="{ width }">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<n8n-text v-show="showItemCount" size="small" color="text-light">
|
<n8n-text v-show="showItemCount" size="small" color="text-light">
|
||||||
|
@ -32,6 +32,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Card,
|
Card,
|
||||||
|
|
|
@ -11,15 +11,26 @@
|
||||||
<Card v-for="n in loading ? 4 : 0" :key="`loading-${n}`" :loading="loading" />
|
<Card v-for="n in loading ? 4 : 0" :key="`loading-${n}`" :loading="loading" />
|
||||||
<TemplatesInfoCard
|
<TemplatesInfoCard
|
||||||
v-for="collection in loading ? [] : collections"
|
v-for="collection in loading ? [] : collections"
|
||||||
|
data-test-id="templates-info-card"
|
||||||
:key="collection.id"
|
:key="collection.id"
|
||||||
:collection="collection"
|
:collection="collection"
|
||||||
|
:showItemCount="showItemCount"
|
||||||
|
:width="cardsWidth"
|
||||||
@click="(e) => onCardClick(e, collection.id)"
|
@click="(e) => onCardClick(e, collection.id)"
|
||||||
/>
|
/>
|
||||||
</agile>
|
</agile>
|
||||||
<button v-show="carouselScrollPosition > 0" :class="$style.leftButton" @click="scrollLeft">
|
<button
|
||||||
|
v-show="showNavigation && carouselScrollPosition > 0"
|
||||||
|
:class="{ [$style.leftButton]: true }"
|
||||||
|
@click="scrollLeft"
|
||||||
|
>
|
||||||
<font-awesome-icon icon="chevron-left" />
|
<font-awesome-icon icon="chevron-left" />
|
||||||
</button>
|
</button>
|
||||||
<button v-show="!scrollEnd" :class="$style.rightButton" @click="scrollRight">
|
<button
|
||||||
|
v-show="showNavigation && !scrollEnd"
|
||||||
|
:class="{ [$style.rightButton]: true }"
|
||||||
|
@click="scrollRight"
|
||||||
|
>
|
||||||
<font-awesome-icon icon="chevron-right" />
|
<font-awesome-icon icon="chevron-right" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,6 +58,18 @@ export default defineComponent({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
showItemCount: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showNavigation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
cardsWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '240px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
collections() {
|
collections() {
|
||||||
|
@ -68,7 +91,8 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
carouselScrollPosition: 0,
|
carouselScrollPosition: 0,
|
||||||
cardWidth: 240,
|
cardWidth: parseInt(this.cardsWidth, 10),
|
||||||
|
sliderWidth: 0,
|
||||||
scrollEnd: false,
|
scrollEnd: false,
|
||||||
listElement: null as null | Element,
|
listElement: null as null | Element,
|
||||||
};
|
};
|
||||||
|
@ -175,6 +199,7 @@ export default defineComponent({
|
||||||
.rightButton {
|
.rightButton {
|
||||||
composes: button;
|
composes: button;
|
||||||
right: -30px;
|
right: -30px;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
right: 27px;
|
right: 27px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
|
@ -204,9 +229,5 @@ export default defineComponent({
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
transition: all 1s ease-in-out;
|
transition: all 1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__track {
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -38,12 +38,14 @@ const props = withDefaults(
|
||||||
executionMode?: string;
|
executionMode?: string;
|
||||||
loaderType?: 'image' | 'spinner';
|
loaderType?: 'image' | 'spinner';
|
||||||
canOpenNDV?: boolean;
|
canOpenNDV?: boolean;
|
||||||
|
hideNodeIssues?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
loading: false,
|
loading: false,
|
||||||
mode: 'workflow',
|
mode: 'workflow',
|
||||||
loaderType: 'image',
|
loaderType: 'image',
|
||||||
canOpenNDV: true,
|
canOpenNDV: true,
|
||||||
|
hideNodeIssues: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -85,6 +87,7 @@ const loadWorkflow = () => {
|
||||||
command: 'openWorkflow',
|
command: 'openWorkflow',
|
||||||
workflow: props.workflow,
|
workflow: props.workflow,
|
||||||
canOpenNDV: props.canOpenNDV,
|
canOpenNDV: props.canOpenNDV,
|
||||||
|
hideNodeIssues: props.hideNodeIssues,
|
||||||
}),
|
}),
|
||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,6 +101,7 @@ describe('WorkflowPreview', () => {
|
||||||
command: 'openWorkflow',
|
command: 'openWorkflow',
|
||||||
workflow,
|
workflow,
|
||||||
canOpenNDV: true,
|
canOpenNDV: true,
|
||||||
|
hideNodeIssues: false,
|
||||||
}),
|
}),
|
||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
|
@ -207,6 +208,7 @@ describe('WorkflowPreview', () => {
|
||||||
command: 'openWorkflow',
|
command: 'openWorkflow',
|
||||||
workflow,
|
workflow,
|
||||||
canOpenNDV: true,
|
canOpenNDV: true,
|
||||||
|
hideNodeIssues: false,
|
||||||
}),
|
}),
|
||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
|
@ -225,10 +227,10 @@ describe('WorkflowPreview', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass the "Disable NDV" flag to using PostMessage', async () => {
|
it('should pass the "Disable NDV" & "Hide issues" flags to using PostMessage', async () => {
|
||||||
const nodes = [{ name: 'Start' }] as INodeUi[];
|
const nodes = [{ name: 'Start' }] as INodeUi[];
|
||||||
const workflow = { nodes } as IWorkflowDb;
|
const workflow = { nodes } as IWorkflowDb;
|
||||||
const { container } = renderComponent({
|
renderComponent({
|
||||||
pinia,
|
pinia,
|
||||||
props: {
|
props: {
|
||||||
workflow,
|
workflow,
|
||||||
|
@ -242,6 +244,7 @@ describe('WorkflowPreview', () => {
|
||||||
command: 'openWorkflow',
|
command: 'openWorkflow',
|
||||||
workflow,
|
workflow,
|
||||||
canOpenNDV: false,
|
canOpenNDV: false,
|
||||||
|
hideNodeIssues: false,
|
||||||
}),
|
}),
|
||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
|
|
|
@ -134,6 +134,9 @@
|
||||||
<template #default="{ item, updateItemSize }">
|
<template #default="{ item, updateItemSize }">
|
||||||
<slot :data="item" :updateItemSize="updateItemSize" />
|
<slot :data="item" :updateItemSize="updateItemSize" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #postListContent>
|
||||||
|
<slot name="postListContent" />
|
||||||
|
</template>
|
||||||
</n8n-recycle-scroller>
|
</n8n-recycle-scroller>
|
||||||
<n8n-datatable
|
<n8n-datatable
|
||||||
v-if="typeProps.columns"
|
v-if="typeProps.columns"
|
||||||
|
|
|
@ -3,8 +3,10 @@ import type { NotificationInstance, NotificationOptions, MessageBoxState } from
|
||||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useI18n } from './useI18n';
|
import { useI18n } from './useI18n';
|
||||||
import { useExternalHooks } from './useExternalHooks';
|
import { useExternalHooks } from './useExternalHooks';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
|
@ -16,6 +18,7 @@ const stickyNotificationQueue: NotificationInstance[] = [];
|
||||||
export function useToast() {
|
export function useToast() {
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
@ -155,11 +158,30 @@ export function useToast() {
|
||||||
stickyNotificationQueue.length = 0;
|
stickyNotificationQueue.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick up and display notifications for the given list of views
|
||||||
|
function showNotificationForViews(views: VIEWS[]) {
|
||||||
|
const notifications: NotificationOptions[] = [];
|
||||||
|
views.forEach((view) => {
|
||||||
|
notifications.push(...uiStore.getNotificationsForView(view));
|
||||||
|
});
|
||||||
|
if (notifications.length) {
|
||||||
|
notifications.forEach(async (notification) => {
|
||||||
|
// Notifications show on top of each other without this timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
showMessage(notification);
|
||||||
|
}, 5);
|
||||||
|
});
|
||||||
|
// Clear the queue once all notifications are shown
|
||||||
|
uiStore.setNotificationsForView(VIEWS.WORKFLOW, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showMessage,
|
showMessage,
|
||||||
showToast,
|
showToast,
|
||||||
showError,
|
showError,
|
||||||
showAlert,
|
showAlert,
|
||||||
clearAllStickyNotifications,
|
clearAllStickyNotifications,
|
||||||
|
showNotificationForViews,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
|
||||||
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
|
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
|
||||||
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
||||||
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
|
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
|
||||||
|
export const SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY = 'suggestedTemplatePreview';
|
||||||
|
|
||||||
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
|
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
|
||||||
|
|
||||||
|
@ -684,3 +685,5 @@ export const TIME = {
|
||||||
HOUR: 60 * 60 * 1000,
|
HOUR: 60 * 60 * 1000,
|
||||||
DAY: 24 * 60 * 60 * 1000,
|
DAY: 24 * 60 * 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SUGGESTED_TEMPLATES_FLAG = 'SHOW_N8N_SUGGESTED_TEMPLATES';
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"generic.seePlans": "See plans",
|
"generic.seePlans": "See plans",
|
||||||
"generic.loading": "Loading",
|
"generic.loading": "Loading",
|
||||||
"generic.and": "and",
|
"generic.and": "and",
|
||||||
|
"generic.welcome": "Welcome",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
|
@ -692,6 +693,14 @@
|
||||||
"genericHelpers.minShort": "m",
|
"genericHelpers.minShort": "m",
|
||||||
"genericHelpers.sec": "sec",
|
"genericHelpers.sec": "sec",
|
||||||
"genericHelpers.secShort": "s",
|
"genericHelpers.secShort": "s",
|
||||||
|
"suggestedTemplates.heading": "{name}, get started with n8n 👇",
|
||||||
|
"suggestedTemplates.sectionTitle": "Explore {sectionName} workflow templates",
|
||||||
|
"suggestedTemplates.newWorkflowButton": "Create blank workflow",
|
||||||
|
"suggestedTemplates.modal.button.label": "Use Template",
|
||||||
|
"suggestedTemplates.notification.comingSoon.title": "Template coming soon!",
|
||||||
|
"suggestedTemplates.notification.confirmation.title": "Got it!",
|
||||||
|
"suggestedTemplates.notification.confirmation.message": "We will contact you via email once this template is released.",
|
||||||
|
"suggestedTemplates.notification.comingSoon.message": "This template is still in the works. <b><a href=\"#\">Notify me when it's available</a></b>",
|
||||||
"readOnly.showMessage.executions.message": "Executions are read-only. Make changes from the <b>Workflow</b> tab.",
|
"readOnly.showMessage.executions.message": "Executions are read-only. Make changes from the <b>Workflow</b> tab.",
|
||||||
"readOnly.showMessage.executions.title": "Cannot edit execution",
|
"readOnly.showMessage.executions.title": "Cannot edit execution",
|
||||||
"readOnlyEnv.showMessage.executions.message": "Executions are read-only.",
|
"readOnlyEnv.showMessage.executions.message": "Executions are read-only.",
|
||||||
|
|
|
@ -5,9 +5,14 @@ import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getAdminPanelLoginCode, getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
import {
|
||||||
|
getAdminPanelLoginCode,
|
||||||
|
getCurrentPlan,
|
||||||
|
getCurrentUsage,
|
||||||
|
fetchSuggestedTemplates,
|
||||||
|
} from '@/api/cloudPlans';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants';
|
import { CLOUD_TRIAL_CHECK_INTERVAL, SUGGESTED_TEMPLATES_FLAG, STORES } from '@/constants';
|
||||||
import { hasPermission } from '@/rbac/permissions';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
|
|
||||||
const DEFAULT_STATE: CloudPlanState = {
|
const DEFAULT_STATE: CloudPlanState = {
|
||||||
|
@ -163,6 +168,17 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
||||||
window.location.href = `https://${adminPanelHost}/login?code=${code}`;
|
window.location.href = `https://${adminPanelHost}/login?code=${code}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSuggestedTemplates = async () => {
|
||||||
|
try {
|
||||||
|
const additionalTemplates = await fetchSuggestedTemplates(rootStore.getRestApiContext);
|
||||||
|
if (additionalTemplates.sections && additionalTemplates.sections.length > 0) {
|
||||||
|
useUIStore().setSuggestedTemplates(additionalTemplates);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error checking for lead enrichment templates:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
if (state.initialized) {
|
if (state.initialized) {
|
||||||
return;
|
return;
|
||||||
|
@ -180,6 +196,12 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
||||||
console.warn('Error fetching user cloud account:', error);
|
console.warn('Error fetching user cloud account:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localStorageFlag = localStorage.getItem(SUGGESTED_TEMPLATES_FLAG);
|
||||||
|
// Don't show if users already opted in
|
||||||
|
if (localStorageFlag !== 'false') {
|
||||||
|
await loadSuggestedTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
state.initialized = true;
|
state.initialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
DEBUG_PAYWALL_MODAL_KEY,
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
N8N_PRICING_PAGE_URL,
|
N8N_PRICING_PAGE_URL,
|
||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
|
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
CloudUpdateLinkSourceType,
|
CloudUpdateLinkSourceType,
|
||||||
|
@ -53,6 +54,7 @@ import type {
|
||||||
NewCredentialsModal,
|
NewCredentialsModal,
|
||||||
ThemeOption,
|
ThemeOption,
|
||||||
AppliedThemeOption,
|
AppliedThemeOption,
|
||||||
|
SuggestedTemplates,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
@ -181,6 +183,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
[WORKFLOW_HISTORY_VERSION_RESTORE]: {
|
[WORKFLOW_HISTORY_VERSION_RESTORE]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modalStack: [],
|
modalStack: [],
|
||||||
sidebarMenuCollapsed: true,
|
sidebarMenuCollapsed: true,
|
||||||
|
@ -217,6 +222,11 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
executionSidebarAutoRefresh: true,
|
executionSidebarAutoRefresh: true,
|
||||||
bannersHeight: 0,
|
bannersHeight: 0,
|
||||||
bannerStack: [],
|
bannerStack: [],
|
||||||
|
suggestedTemplates: undefined,
|
||||||
|
// Notifications that should show when a view is initialized
|
||||||
|
// This enables us to set a queue of notifications form outside (another component)
|
||||||
|
// and then show them when the view is initialized
|
||||||
|
pendingNotificationsForViews: {},
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
appliedTheme(): AppliedThemeOption {
|
appliedTheme(): AppliedThemeOption {
|
||||||
|
@ -642,5 +652,20 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
clearBannerStack() {
|
clearBannerStack() {
|
||||||
this.bannerStack = [];
|
this.bannerStack = [];
|
||||||
},
|
},
|
||||||
|
setSuggestedTemplates(templates: SuggestedTemplates) {
|
||||||
|
this.suggestedTemplates = templates;
|
||||||
|
},
|
||||||
|
deleteSuggestedTemplates() {
|
||||||
|
this.suggestedTemplates = undefined;
|
||||||
|
},
|
||||||
|
getNotificationsForView(view: VIEWS): NotificationOptions[] {
|
||||||
|
return this.pendingNotificationsForViews[view] ?? [];
|
||||||
|
},
|
||||||
|
setNotificationsForView(view: VIEWS, notifications: NotificationOptions[]) {
|
||||||
|
this.pendingNotificationsForViews[view] = notifications;
|
||||||
|
},
|
||||||
|
deleteNotificationsForView(view: VIEWS) {
|
||||||
|
delete this.pendingNotificationsForViews[view];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
:isProductionExecutionPreview="isProductionExecutionPreview"
|
:isProductionExecutionPreview="isProductionExecutionPreview"
|
||||||
:workflow="currentWorkflowObject"
|
:workflow="currentWorkflowObject"
|
||||||
:disablePointerEvents="!canOpenNDV"
|
:disablePointerEvents="!canOpenNDV"
|
||||||
|
:hideNodeIssues="hideNodeIssues"
|
||||||
>
|
>
|
||||||
<template #custom-tooltip>
|
<template #custom-tooltip>
|
||||||
<span
|
<span
|
||||||
|
@ -746,6 +747,7 @@ export default defineComponent({
|
||||||
eventsAttached: false,
|
eventsAttached: false,
|
||||||
unloadTimeout: undefined as undefined | ReturnType<typeof setTimeout>,
|
unloadTimeout: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||||
canOpenNDV: true,
|
canOpenNDV: true,
|
||||||
|
hideNodeIssues: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1028,7 +1030,6 @@ export default defineComponent({
|
||||||
if (data.workflow.pinData) {
|
if (data.workflow.pinData) {
|
||||||
this.workflowsStore.setWorkflowPinData(data.workflow.pinData);
|
this.workflowsStore.setWorkflowPinData(data.workflow.pinData);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.canvasStore.zoomToFit();
|
this.canvasStore.zoomToFit();
|
||||||
},
|
},
|
||||||
|
@ -3304,6 +3305,9 @@ export default defineComponent({
|
||||||
|
|
||||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||||
window.addEventListener('unload', this.onUnload);
|
window.addEventListener('unload', this.onUnload);
|
||||||
|
// Once view is initialized, pick up all toast notifications
|
||||||
|
// waiting in the store and display them
|
||||||
|
this.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);
|
||||||
},
|
},
|
||||||
getOutputEndpointUUID(
|
getOutputEndpointUUID(
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
|
@ -4356,6 +4360,7 @@ export default defineComponent({
|
||||||
try {
|
try {
|
||||||
await this.importWorkflowExact(json);
|
await this.importWorkflowExact(json);
|
||||||
this.canOpenNDV = json.canOpenNDV ?? true;
|
this.canOpenNDV = json.canOpenNDV ?? true;
|
||||||
|
this.hideNodeIssues = json.hideNodeIssues ?? false;
|
||||||
this.isExecutionPreview = false;
|
this.isExecutionPreview = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (window.top) {
|
if (window.top) {
|
||||||
|
@ -4381,6 +4386,7 @@ export default defineComponent({
|
||||||
|
|
||||||
await this.openExecution(json.executionId);
|
await this.openExecution(json.executionId);
|
||||||
this.canOpenNDV = json.canOpenNDV ?? true;
|
this.canOpenNDV = json.canOpenNDV ?? true;
|
||||||
|
this.hideNodeIssues = json.hideNodeIssues ?? false;
|
||||||
this.isExecutionPreview = true;
|
this.isExecutionPreview = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (window.top) {
|
if (window.top) {
|
||||||
|
|
|
@ -41,7 +41,21 @@
|
||||||
:readOnly="readOnlyEnv"
|
:readOnly="readOnlyEnv"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #postListContent>
|
||||||
|
<suggested-templates-section
|
||||||
|
v-for="(section, key) in suggestedTemplates?.sections"
|
||||||
|
:key="key"
|
||||||
|
:section="section"
|
||||||
|
:title="
|
||||||
|
$locale.baseText('suggestedTemplates.sectionTitle', {
|
||||||
|
interpolate: { sectionName: section.name.toLocaleLowerCase() },
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
<suggested-templates-page v-if="suggestedTemplates" />
|
||||||
|
<div v-else>
|
||||||
<div class="text-center mt-s">
|
<div class="text-center mt-s">
|
||||||
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
|
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
|
||||||
{{
|
{{
|
||||||
|
@ -76,6 +90,7 @@
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</n8n-card>
|
</n8n-card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #filters="{ setKeyValue }">
|
<template #filters="{ setKeyValue }">
|
||||||
<div class="mb-s" v-if="settingsStore.areTagsEnabled">
|
<div class="mb-s" v-if="settingsStore.areTagsEnabled">
|
||||||
|
@ -127,6 +142,8 @@ import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
|
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
|
||||||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||||
|
import SuggestedTemplatesPage from '@/components/SuggestedTemplates/SuggestedTemplatesPage.vue';
|
||||||
|
import SuggestedTemplatesSection from '@/components/SuggestedTemplates/SuggestedTemplatesSection.vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
@ -152,6 +169,8 @@ const WorkflowsView = defineComponent({
|
||||||
ResourcesListLayout,
|
ResourcesListLayout,
|
||||||
WorkflowCard,
|
WorkflowCard,
|
||||||
TagsDropdown,
|
TagsDropdown,
|
||||||
|
SuggestedTemplatesPage,
|
||||||
|
SuggestedTemplatesSection,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -200,6 +219,9 @@ const WorkflowsView = defineComponent({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
suggestedTemplates() {
|
||||||
|
return this.uiStore.suggestedTemplates;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addWorkflow() {
|
addWorkflow() {
|
||||||
|
|
Loading…
Reference in a new issue