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:
Milorad FIlipović
2023-12-19 15:10:03 +01:00
committed by GitHub
parent c170dd1da3
commit 36a923cf7b
26 changed files with 1387 additions and 87 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>