1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-11 13:15:28 +03:00

feat(editor): Clean up lead enrichment experiment (no-changelog) (#9572)

This commit is contained in:
Tomi Turtiainen 2024-06-03 10:35:55 +03:00 committed by GitHub
parent 99c15a0fd8
commit 09472fb9ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 46 additions and 1263 deletions

View File

@ -1,143 +0,0 @@
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');
});
});

View File

@ -1,655 +0,0 @@
{
"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 Test 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 Test 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 Test 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 Test 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 Test 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 Test 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 Test 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 Test 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"
}
]
}
]
}
]
}

View File

@ -34,13 +34,6 @@ export class WorkflowsPage extends BasePage {
// Not yet implemented
// myWorkflows: () => cy.getByTestId('my-workflows'),
// allWorkflows: () => cy.getByTestId('all-workflows'),
suggestedTemplatesPageContainer: () => cy.getByTestId('suggested-templates-page-container'),
suggestedTemplatesCards: () => cy.get('.agile__slides--regular [data-test-id=templates-info-card]'),
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 = {

View File

@ -193,9 +193,6 @@ function onScroll() {
</div>
</div>
</div>
<div class="post-list-container">
<slot name="postListContent" />
</div>
</div>
</template>
@ -223,7 +220,4 @@ function onScroll() {
position: relative;
width: 100%;
}
.post-list-container {
margin-top: var(--spacing-3xl);
}
</style>

View File

@ -9,6 +9,5 @@ exports[`components > N8nRecycleScroller > should render correctly 1`] = `
<div class="recycle-scroller-item"></div>
</div>
</div>
<div class="post-list-container"></div>
</div>"
`;

View File

@ -1325,7 +1325,6 @@ export interface UIState {
bannersHeight: number;
bannerStack: BannerName[];
theme: ThemeOption;
suggestedTemplates?: SuggestedTemplates;
pendingNotificationsForViews: {
[key in VIEWS]?: NotificationOptions[];
};
@ -1917,24 +1916,6 @@ export type ToggleNodeCreatorOptions = {
export type AppliedThemeOption = 'light' | 'dark';
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'>>;
};
export type NewConnectionInfo = {
sourceId: string;
index: number;

View File

@ -1,4 +1,4 @@
import type { Cloud, IRestApiContext, InstanceUsage, LeadEnrichmentTemplates } from '@/Interface';
import type { Cloud, IRestApiContext, InstanceUsage } from '@/Interface';
import { get, post } from '@/utils/apiUtils';
export async function getCurrentPlan(context: IRestApiContext): Promise<Cloud.PlanData> {
@ -20,9 +20,3 @@ export async function confirmEmail(context: IRestApiContext): Promise<Cloud.User
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
return await get(context.baseUrl, '/cloud/proxy/login/code');
}
export async function fetchSuggestedTemplates(
context: IRestApiContext,
): Promise<LeadEnrichmentTemplates> {
return await get(context.baseUrl, '/cloud/proxy/templates');
}

View File

@ -157,15 +157,6 @@
/>
</template>
</ModalRoot>
<ModalRoot :name="SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY">
<template #default="{ modalName, data }">
<SuggestedTemplatesPreviewModal
data-test-id="suggested-templates-preview-modal"
:modal-name="modalName"
:data="data"
/>
</template>
</ModalRoot>
<ModalRoot :name="SETUP_CREDENTIALS_MODAL_KEY">
<template #default="{ modalName, data }">
@ -210,7 +201,6 @@ import {
DEBUG_PAYWALL_MODAL_KEY,
MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
SETUP_CREDENTIALS_MODAL_KEY,
GENERATE_CURL_MODAL_KEY,
} from '@/constants';
@ -245,7 +235,6 @@ import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
import SuggestedTemplatesPreviewModal from '@/components/SuggestedTemplates/SuggestedTemplatesPreviewModal.vue';
import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue';
export default defineComponent({
@ -281,7 +270,6 @@ export default defineComponent({
DebugPaywallModal,
MfaSetupModal,
WorkflowHistoryVersionRestoreModal,
SuggestedTemplatesPreviewModal,
SetupWorkflowCredentialsModal,
},
data: () => ({
@ -314,7 +302,6 @@ export default defineComponent({
DEBUG_PAYWALL_MODAL_KEY,
MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
SETUP_CREDENTIALS_MODAL_KEY,
}),
});

View File

@ -1,107 +0,0 @@
<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, IUser } from '@/Interface';
import SuggestedTemplatesSection from '@/components/SuggestedTemplates/SuggestedTemplatesSection.vue';
const usersStore = useUsersStore();
const uiStore = useUIStore();
const router = useRouter();
const currentUser = computed(() => usersStore.currentUser);
const upperCaseFirstName = (user: IUser | null) => {
if (!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">
<SuggestedTemplatesSection
v-for="section in uiStore.suggestedTemplates?.sections"
:key="section.title"
:section="section"
:show-title="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>

View File

@ -1,109 +0,0 @@
<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,
},
);
}
}
function openCanvas() {
uiStore.setNotificationsForView(VIEWS.WORKFLOW, [
{
title: i18n.baseText('suggestedTemplates.notification.comingSoon.title'),
message: i18n.baseText('suggestedTemplates.notification.comingSoon.message'),
type: 'info',
duration: 10000,
onClick: showConfirmationMessage,
},
]);
localStorage.setItem(SUGGESTED_TEMPLATES_FLAG, 'false');
uiStore.deleteSuggestedTemplates();
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>
<WorkflowPreview
:loading="false"
:workflow="$props.data.workflow.preview as IWorkflowDb"
:can-open-n-d-v="false"
:hide-node-issues="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
float="right"
data-test-id="use-template-button"
:label="$locale.baseText('suggestedTemplates.modal.button.label')"
@click="openCanvas"
/>
</div>
</template>
</Modal>
</template>
<style module lang="scss">
.footerButtons {
margin-top: var(--spacing-xl);
}
</style>

View File

@ -1,82 +0,0 @@
<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">
<TemplatesInfoCarousel
:collections="sectionTemplates"
:loading="false"
:show-item-count="false"
:show-navigation="false"
cards-width="24%"
@open-collection="onOpenCollection"
/>
</div>
</div>
</template>
<style lang="scss" module>
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
</style>

View File

@ -114,9 +114,6 @@
<template #default="{ item, updateItemSize }">
<slot :data="item" :update-item-size="updateItemSize" />
</template>
<template #postListContent>
<slot name="postListContent" />
</template>
</n8n-recycle-scroller>
<n8n-datatable
v-if="type === 'datatable'"

View File

@ -63,7 +63,6 @@ export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
export const SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY = 'suggestedTemplatePreview';
export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials';
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
@ -769,8 +768,6 @@ export const TIME = {
DAY: 24 * 60 * 60 * 1000,
};
export const SUGGESTED_TEMPLATES_FLAG = 'SHOW_N8N_SUGGESTED_TEMPLATES';
/**
* Mouse button codes
*/

View File

@ -67,7 +67,6 @@
"generic.seePlans": "See plans",
"generic.loading": "Loading",
"generic.and": "and",
"generic.welcome": "Welcome",
"generic.ownedByMe": "Owned by me",
"generic.moreInfo": "More info",
"about.aboutN8n": "About n8n",
@ -817,14 +816,6 @@
"genericHelpers.minShort": "m",
"genericHelpers.sec": "sec",
"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.title": "Cannot edit execution",
"readOnlyEnv.showMessage.executions.message": "Executions are read-only.",

View File

@ -5,14 +5,9 @@ import { useRootStore } from '@/stores/n8nRoot.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import {
getAdminPanelLoginCode,
getCurrentPlan,
getCurrentUsage,
fetchSuggestedTemplates,
} from '@/api/cloudPlans';
import { getAdminPanelLoginCode, getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
import { DateTime } from 'luxon';
import { CLOUD_TRIAL_CHECK_INTERVAL, SUGGESTED_TEMPLATES_FLAG, STORES } from '@/constants';
import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants';
import { hasPermission } from '@/rbac/permissions';
const DEFAULT_STATE: CloudPlanState = {
@ -166,17 +161,6 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
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 () => {
if (state.initialized) {
return;
@ -194,12 +178,6 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
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' && hasPermission(['instanceOwner'])) {
await loadSuggestedTemplates();
}
state.initialized = true;
};

View File

@ -37,7 +37,6 @@ import {
DEBUG_PAYWALL_MODAL_KEY,
N8N_PRICING_PAGE_URL,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
SETUP_CREDENTIALS_MODAL_KEY,
GENERATE_CURL_MODAL_KEY,
} from '@/constants';
@ -54,7 +53,6 @@ import type {
NewCredentialsModal,
ThemeOption,
AppliedThemeOption,
SuggestedTemplates,
NotificationOptions,
ModalState,
} from '@/Interface';
@ -119,7 +117,6 @@ export const useUIStore = defineStore(STORES.UI, {
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
SETUP_CREDENTIALS_MODAL_KEY,
].map((modalKey) => [modalKey, { open: false }]),
),
@ -190,7 +187,6 @@ export const useUIStore = defineStore(STORES.UI, {
addFirstStepOnLoad: false,
bannersHeight: 0,
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
@ -615,12 +611,6 @@ export const useUIStore = defineStore(STORES.UI, {
clearBannerStack() {
this.bannerStack = [];
},
setSuggestedTemplates(templates: SuggestedTemplates) {
this.suggestedTemplates = templates;
},
deleteSuggestedTemplates() {
this.suggestedTemplates = undefined;
},
getNotificationsForView(view: VIEWS): NotificationOptions[] {
return this.pendingNotificationsForViews[view] ?? [];
},

View File

@ -49,71 +49,56 @@
@click:tag="onClickTag"
/>
</template>
<template #postListContent>
<SuggestedTemplatesSection
v-for="(section, key) in suggestedTemplates?.sections"
:key="key"
:section="section"
:title="
$locale.baseText('suggestedTemplates.sectionTitle', {
interpolate: { sectionName: section.name.toLocaleLowerCase() },
})
"
/>
</template>
<template #empty>
<SuggestedTemplatesPage v-if="suggestedTemplates" />
<div v-else>
<div class="text-center mt-s">
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
{{
currentUser.firstName
? $locale.baseText('workflows.empty.heading', {
interpolate: { name: currentUser.firstName },
})
: $locale.baseText('workflows.empty.heading.userNotSetup')
}}
</n8n-heading>
<n8n-text size="large" color="text-base">
{{
$locale.baseText(
readOnlyEnv
? 'workflows.empty.description.readOnlyEnv'
: 'workflows.empty.description',
)
}}
</n8n-text>
</div>
<div v-if="!readOnlyEnv" :class="['text-center', 'mt-2xl', $style.actionsContainer]">
<a
v-if="isSalesUser"
:href="getTemplateRepositoryURL()"
:class="$style.emptyStateCard"
target="_blank"
>
<n8n-card
hoverable
data-test-id="browse-sales-templates-card"
@click="trackCategoryLinkClick('Sales')"
>
<n8n-icon :class="$style.emptyStateCardIcon" icon="box-open" />
<n8n-text size="large" class="mt-xs" color="text-base">
{{ $locale.baseText('workflows.empty.browseTemplates') }}
</n8n-text>
</n8n-card>
</a>
<div class="text-center mt-s">
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
{{
currentUser.firstName
? $locale.baseText('workflows.empty.heading', {
interpolate: { name: currentUser.firstName },
})
: $locale.baseText('workflows.empty.heading.userNotSetup')
}}
</n8n-heading>
<n8n-text size="large" color="text-base">
{{
$locale.baseText(
readOnlyEnv
? 'workflows.empty.description.readOnlyEnv'
: 'workflows.empty.description',
)
}}
</n8n-text>
</div>
<div v-if="!readOnlyEnv" :class="['text-center', 'mt-2xl', $style.actionsContainer]">
<a
v-if="isSalesUser"
:href="getTemplateRepositoryURL()"
:class="$style.emptyStateCard"
target="_blank"
>
<n8n-card
:class="$style.emptyStateCard"
hoverable
data-test-id="new-workflow-card"
@click="addWorkflow"
data-test-id="browse-sales-templates-card"
@click="trackCategoryLinkClick('Sales')"
>
<n8n-icon :class="$style.emptyStateCardIcon" icon="file" />
<n8n-icon :class="$style.emptyStateCardIcon" icon="box-open" />
<n8n-text size="large" class="mt-xs" color="text-base">
{{ $locale.baseText('workflows.empty.startFromScratch') }}
{{ $locale.baseText('workflows.empty.browseTemplates') }}
</n8n-text>
</n8n-card>
</div>
</a>
<n8n-card
:class="$style.emptyStateCard"
hoverable
data-test-id="new-workflow-card"
@click="addWorkflow"
>
<n8n-icon :class="$style.emptyStateCardIcon" icon="file" />
<n8n-text size="large" class="mt-xs" color="text-base">
{{ $locale.baseText('workflows.empty.startFromScratch') }}
</n8n-text>
</n8n-card>
</div>
</template>
<template #filters="{ setKeyValue }">
@ -166,8 +151,6 @@ import WorkflowCard from '@/components/WorkflowCard.vue';
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
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 { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
@ -200,8 +183,6 @@ const WorkflowsView = defineComponent({
ResourcesListLayout,
WorkflowCard,
TagsDropdown,
SuggestedTemplatesPage,
SuggestedTemplatesSection,
ProjectTabs,
},
data() {
@ -254,9 +235,6 @@ const WorkflowsView = defineComponent({
},
];
},
suggestedTemplates() {
return this.uiStore.suggestedTemplates;
},
userRole() {
const role = this.usersStore.currentUserCloudInfo?.role;