mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
feat(editor): Update suggested workflows experiment (no-changelog) (#17701)
This commit is contained in:
@@ -31,7 +31,7 @@ export declare namespace Cloud {
|
|||||||
email: string;
|
email: string;
|
||||||
hasEarlyAccess?: boolean;
|
hasEarlyAccess?: boolean;
|
||||||
role?: string;
|
role?: string;
|
||||||
selectedApps?: string;
|
selectedApps?: string[];
|
||||||
information?: {
|
information?: {
|
||||||
[key: string]: string | string[];
|
[key: string]: string | string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ export const STORES = {
|
|||||||
MODULES: 'modules',
|
MODULES: 'modules',
|
||||||
FOCUS_PANEL: 'focusPanel',
|
FOCUS_PANEL: 'focusPanel',
|
||||||
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
||||||
|
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { VIEWS } from '@/constants';
|
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const navigateTo = () => {
|
const navigateTo = () => {
|
||||||
void router.push({ name: VIEWS.TEMPLATES });
|
void router.back();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
type SuggestedWorkflow = {
|
type SuggestedWorkflow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -13,19 +12,27 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
const templatesStore = useTemplatesStore();
|
const {
|
||||||
|
dismissSuggestedWorkflow,
|
||||||
|
getTemplateRoute,
|
||||||
|
trackUserClickedOnPersonalizedTemplate,
|
||||||
|
trackUserDismissedCallout,
|
||||||
|
} = usePersonalizedTemplatesStore();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
const isVisible = ref(true);
|
const onDismissCallout = () => {
|
||||||
|
trackUserDismissedCallout(data.id);
|
||||||
|
dismissSuggestedWorkflow(data.id);
|
||||||
|
};
|
||||||
|
|
||||||
const dismissCallout = () => {
|
const onTryTemplate = () => {
|
||||||
templatesStore.experimentalDismissSuggestedWorkflow(data.id);
|
trackUserClickedOnPersonalizedTemplate(data.id);
|
||||||
|
dismissSuggestedWorkflow(data.id);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<N8nCallout
|
<N8nCallout
|
||||||
v-if="isVisible"
|
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
:iconless="true"
|
:iconless="true"
|
||||||
:class="$style['suggested-workflow-callout']"
|
:class="$style['suggested-workflow-callout']"
|
||||||
@@ -39,7 +46,8 @@ const dismissCallout = () => {
|
|||||||
<N8nLink
|
<N8nLink
|
||||||
data-test-id="suggested-workflow-button"
|
data-test-id="suggested-workflow-button"
|
||||||
size="small"
|
size="small"
|
||||||
:href="templatesStore.websiteTemplateURLById(data.id.toString())"
|
:to="getTemplateRoute(data.id)"
|
||||||
|
@click="onTryTemplate"
|
||||||
>
|
>
|
||||||
{{ locale.baseText('workflows.itemSuggestion.try') }}
|
{{ locale.baseText('workflows.itemSuggestion.try') }}
|
||||||
</N8nLink>
|
</N8nLink>
|
||||||
@@ -48,7 +56,7 @@ const dismissCallout = () => {
|
|||||||
icon="x"
|
icon="x"
|
||||||
:title="locale.baseText('generic.dismiss')"
|
:title="locale.baseText('generic.dismiss')"
|
||||||
class="clickable"
|
class="clickable"
|
||||||
@click="dismissCallout"
|
@click="onDismissCallout"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { useStorage } from '@/composables/useStorage';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import {
|
||||||
|
LOCAL_STORAGE_EXPERIMENTAL_DISMISSED_SUGGESTED_WORKFLOWS,
|
||||||
|
TEMPLATE_ONBOARDING_EXPERIMENT,
|
||||||
|
VIEWS,
|
||||||
|
} from '@/constants';
|
||||||
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
|
import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client';
|
||||||
|
import { STORES } from '@n8n/stores';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const SIMPLE_TEMPLATES = [6270, 5271, 2178];
|
||||||
|
|
||||||
|
const PREDEFINED_TEMPLATES_BY_NODE = {
|
||||||
|
gmail: [5678, 4722, 5694],
|
||||||
|
googleSheets: [5694, 5690, 5906],
|
||||||
|
telegram: [5626, 2114, 4875],
|
||||||
|
openAi: [2462, 2722, 2178],
|
||||||
|
googleGemini: [5993, 6270, 5677],
|
||||||
|
googleCalendar: [2328, 3393, 2110],
|
||||||
|
youTube: [3188, 4846, 4506],
|
||||||
|
airtable: [3053, 2700, 2579],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPredefinedFromSelected(selectedApps: string[]) {
|
||||||
|
const predefinedNodes = Object.keys(PREDEFINED_TEMPLATES_BY_NODE);
|
||||||
|
const predefinedSelected = predefinedNodes.filter((node) => selectedApps.includes(node));
|
||||||
|
|
||||||
|
return predefinedSelected.reduce<number[]>(
|
||||||
|
(acc, app) => [
|
||||||
|
...acc,
|
||||||
|
...PREDEFINED_TEMPLATES_BY_NODE[app as keyof typeof PREDEFINED_TEMPLATES_BY_NODE],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestedTemplatesForLowCodingSkill(selectedApps: string[]) {
|
||||||
|
if (selectedApps.length === 0) {
|
||||||
|
return SIMPLE_TEMPLATES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const predefinedSelected = getPredefinedFromSelected(selectedApps);
|
||||||
|
if (predefinedSelected.length > 0) {
|
||||||
|
return predefinedSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepTop3Templates(templates: ITemplatesWorkflowFull[]) {
|
||||||
|
if (templates.length <= 3) {
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Map(templates.map((t) => [t.id, t])).values())
|
||||||
|
.sort((a, b) => b.totalViews - a.totalViews)
|
||||||
|
.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePersonalizedTemplatesStore = defineStore(STORES.PERSONALIZED_TEMPLATES, () => {
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const posthogStore = usePostHog();
|
||||||
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
const templatesStore = useTemplatesStore();
|
||||||
|
|
||||||
|
const allSuggestedWorkflows = ref<ITemplatesWorkflowFull[]>([]);
|
||||||
|
const dismissedSuggestedWorkflowsStorage = useStorage(
|
||||||
|
LOCAL_STORAGE_EXPERIMENTAL_DISMISSED_SUGGESTED_WORKFLOWS,
|
||||||
|
);
|
||||||
|
const dismissedSuggestedWorkflows = computed((): number[] => {
|
||||||
|
return dismissedSuggestedWorkflowsStorage.value
|
||||||
|
? jsonParse(dismissedSuggestedWorkflowsStorage.value, { fallbackValue: [] })
|
||||||
|
: [];
|
||||||
|
});
|
||||||
|
const suggestedWorkflows = computed(() =>
|
||||||
|
allSuggestedWorkflows.value.filter(({ id }) => !dismissedSuggestedWorkflows.value.includes(id)),
|
||||||
|
);
|
||||||
|
const dismissSuggestedWorkflow = (id: number) => {
|
||||||
|
dismissedSuggestedWorkflowsStorage.value = JSON.stringify([
|
||||||
|
...(dismissedSuggestedWorkflows.value ?? []),
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFeatureEnabled = () => {
|
||||||
|
return (
|
||||||
|
posthogStore.getVariant(TEMPLATE_ONBOARDING_EXPERIMENT.name) ===
|
||||||
|
TEMPLATE_ONBOARDING_EXPERIMENT.variantSuggestedTemplates && cloudPlanStore.userIsTrialing
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackUserWasRecommendedTemplates = (templateIds: number[]) => {
|
||||||
|
telemetry.track('User was recommended personalized templates', {
|
||||||
|
templateIds,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackUserClickedOnPersonalizedTemplate = (templateId: number) => {
|
||||||
|
telemetry.track('User clicked on personalized template callout', {
|
||||||
|
templateId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackUserDismissedCallout = (templateId: number) => {
|
||||||
|
telemetry.track('User dismissed personalized template callout', {
|
||||||
|
templateId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSuggestedWorkflows = async (codingSkill: number, selectedApps: string[]) => {
|
||||||
|
if (!isFeatureEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (codingSkill === 1) {
|
||||||
|
const predefinedSelected = getSuggestedTemplatesForLowCodingSkill(selectedApps);
|
||||||
|
|
||||||
|
if (predefinedSelected.length > 0) {
|
||||||
|
const suggestedWorkflowsPromises = predefinedSelected.map(
|
||||||
|
async (id) => await templatesStore.fetchTemplateById(id.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allWorkflows = await Promise.all(suggestedWorkflowsPromises);
|
||||||
|
const top3Templates = keepTop3Templates(allWorkflows);
|
||||||
|
allSuggestedWorkflows.value = top3Templates;
|
||||||
|
trackUserWasRecommendedTemplates(top3Templates.map((t) => t.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topWorkflowsByApp = await templatesStore.getWorkflows({
|
||||||
|
categories: [],
|
||||||
|
search: '',
|
||||||
|
sort: 'rank:desc',
|
||||||
|
apps: selectedApps.length > 0 ? selectedApps : undefined,
|
||||||
|
combineWith: 'or',
|
||||||
|
});
|
||||||
|
|
||||||
|
const topWorkflowsIds = topWorkflowsByApp.slice(0, 3).map((workflow) => workflow.id);
|
||||||
|
const suggestedWorkflowsPromises = topWorkflowsIds.map(
|
||||||
|
async (id) => await templatesStore.fetchTemplateById(id.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allWorkflows = await Promise.all(suggestedWorkflowsPromises);
|
||||||
|
const top3Templates = keepTop3Templates(allWorkflows);
|
||||||
|
allSuggestedWorkflows.value = top3Templates;
|
||||||
|
trackUserWasRecommendedTemplates(top3Templates.map((t) => t.id));
|
||||||
|
} catch (error) {
|
||||||
|
// Let it fail silently
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplateRoute = (id: number) => {
|
||||||
|
return { name: VIEWS.TEMPLATE, params: { id } };
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => cloudPlanStore.currentUserCloudInfo,
|
||||||
|
async (userInfo) => {
|
||||||
|
if (!userInfo) return;
|
||||||
|
|
||||||
|
const codingSkill = cloudPlanStore.codingSkill;
|
||||||
|
const selectedApps = cloudPlanStore.selectedApps ?? [];
|
||||||
|
|
||||||
|
await fetchSuggestedWorkflows(codingSkill, selectedApps);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFeatureEnabled,
|
||||||
|
suggestedWorkflows,
|
||||||
|
dismissSuggestedWorkflow,
|
||||||
|
trackUserClickedOnPersonalizedTemplate,
|
||||||
|
trackUserDismissedCallout,
|
||||||
|
getTemplateRoute,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -41,9 +41,7 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
|||||||
|
|
||||||
const currentUsageData = computed(() => state.usage);
|
const currentUsageData = computed(() => state.usage);
|
||||||
|
|
||||||
const selectedApps = computed(
|
const selectedApps = computed(() => currentUserCloudInfo.value?.selectedApps);
|
||||||
() => currentUserCloudInfo.value?.selectedApps?.split(',').filter(Boolean) ?? [],
|
|
||||||
);
|
|
||||||
const codingSkill = computed(() => {
|
const codingSkill = computed(() => {
|
||||||
const information = currentUserCloudInfo.value?.information;
|
const information = currentUserCloudInfo.value?.information;
|
||||||
if (!information) {
|
if (!information) {
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { useStorage } from '@/composables/useStorage';
|
import { TEMPLATES_URLS } from '@/constants';
|
||||||
import {
|
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_DISMISSED_SUGGESTED_WORKFLOWS,
|
|
||||||
TEMPLATES_URLS,
|
|
||||||
} from '@/constants';
|
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import {
|
import { getTemplatePathByRole } from '@/utils/experiments';
|
||||||
getSuggestedTemplatesForLowCodingSkill,
|
|
||||||
getTemplatePathByRole,
|
|
||||||
getTop3Templates,
|
|
||||||
isPersonalizedTemplatesExperimentEnabled,
|
|
||||||
} from '@/utils/experiments';
|
|
||||||
import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
|
import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
|
||||||
import type {
|
import type {
|
||||||
ITemplatesCategory,
|
ITemplatesCategory,
|
||||||
@@ -24,7 +15,6 @@ import type {
|
|||||||
import * as templatesApi from '@n8n/rest-api-client/api/templates';
|
import * as templatesApi from '@n8n/rest-api-client/api/templates';
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from './settings.store';
|
||||||
@@ -90,27 +80,6 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
|
|||||||
`${window.location.protocol}//${window.location.host}${window.BASE_PATH}`,
|
`${window.location.protocol}//${window.location.host}${window.BASE_PATH}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const experimentalAllSuggestedWorkflows = ref<ITemplatesWorkflowFull[]>([]);
|
|
||||||
const experimentalDismissedSuggestedWorkflowsStorage = useStorage(
|
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_DISMISSED_SUGGESTED_WORKFLOWS,
|
|
||||||
);
|
|
||||||
const experimentalDismissedSuggestedWorkflows = computed((): number[] => {
|
|
||||||
return experimentalDismissedSuggestedWorkflowsStorage.value
|
|
||||||
? jsonParse(experimentalDismissedSuggestedWorkflowsStorage.value, { fallbackValue: [] })
|
|
||||||
: [];
|
|
||||||
});
|
|
||||||
const experimentalSuggestedWorkflows = computed(() =>
|
|
||||||
experimentalAllSuggestedWorkflows.value.filter(
|
|
||||||
({ id }) => !experimentalDismissedSuggestedWorkflows.value.includes(id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const experimentalDismissSuggestedWorkflow = (id: number) => {
|
|
||||||
experimentalDismissedSuggestedWorkflowsStorage.value = JSON.stringify([
|
|
||||||
...(experimentalDismissedSuggestedWorkflows.value ?? []),
|
|
||||||
id,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const userStore = useUsersStore();
|
const userStore = useUsersStore();
|
||||||
@@ -226,9 +195,6 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
|
|||||||
`${TEMPLATES_URLS.BASE_WEBSITE_URL}${getTemplatePathByRole(userRole.value)}?${websiteTemplateRepositoryParameters.value.toString()}`,
|
`${TEMPLATES_URLS.BASE_WEBSITE_URL}${getTemplatePathByRole(userRole.value)}?${websiteTemplateRepositoryParameters.value.toString()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const websiteTemplateURLById = (path: string) =>
|
|
||||||
`${TEMPLATES_URLS.BASE_WEBSITE_URL}${path}${getTemplatePathByRole(userRole.value)}?${websiteTemplateRepositoryParameters.value.toString()}`;
|
|
||||||
|
|
||||||
const constructTemplateRepositoryURL = (params: URLSearchParams): string => {
|
const constructTemplateRepositoryURL = (params: URLSearchParams): string => {
|
||||||
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?${params.toString()}`;
|
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?${params.toString()}`;
|
||||||
};
|
};
|
||||||
@@ -474,49 +440,6 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
|
|||||||
return template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|
||||||
const experimentalFetchSuggestedWorkflows = async () => {
|
|
||||||
if (!isPersonalizedTemplatesExperimentEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const codingSkill = cloudPlanStore.codingSkill;
|
|
||||||
const selectedApps = cloudPlanStore.selectedApps;
|
|
||||||
|
|
||||||
if (codingSkill === 1) {
|
|
||||||
const predefinedSelected = getSuggestedTemplatesForLowCodingSkill(selectedApps);
|
|
||||||
|
|
||||||
if (predefinedSelected.length > 0) {
|
|
||||||
const suggestedWorkflowsPromises = predefinedSelected.map(
|
|
||||||
async (id) => await fetchTemplateById(id.toString()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const suggestedWorkflows = await Promise.all(suggestedWorkflowsPromises);
|
|
||||||
experimentalAllSuggestedWorkflows.value = getTop3Templates(suggestedWorkflows);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const topWorkflowsByApp = await getWorkflows({
|
|
||||||
categories: [],
|
|
||||||
search: '',
|
|
||||||
sort: 'rank:desc',
|
|
||||||
apps: selectedApps.length > 0 ? selectedApps : undefined,
|
|
||||||
combineWith: 'or',
|
|
||||||
});
|
|
||||||
|
|
||||||
const topWorkflowsIds = topWorkflowsByApp.slice(0, 3).map((workflow) => workflow.id);
|
|
||||||
const suggestedWorkflowsPromises = topWorkflowsIds.map(
|
|
||||||
async (id) => await fetchTemplateById(id.toString()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const suggestedWorkflows = await Promise.all(suggestedWorkflowsPromises);
|
|
||||||
experimentalAllSuggestedWorkflows.value = getTop3Templates(suggestedWorkflows);
|
|
||||||
} catch (error) {
|
|
||||||
// Let it fail silently
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
collections,
|
collections,
|
||||||
@@ -557,9 +480,5 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
|
|||||||
getMoreWorkflows,
|
getMoreWorkflows,
|
||||||
getWorkflowTemplate,
|
getWorkflowTemplate,
|
||||||
getFixedWorkflowTemplate,
|
getFixedWorkflowTemplate,
|
||||||
websiteTemplateURLById,
|
|
||||||
experimentalFetchSuggestedWorkflows,
|
|
||||||
experimentalSuggestedWorkflows,
|
|
||||||
experimentalDismissSuggestedWorkflow,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { EXTRA_TEMPLATE_LINKS_EXPERIMENT, TEMPLATE_ONBOARDING_EXPERIMENT } from '@/constants';
|
import { EXTRA_TEMPLATE_LINKS_EXPERIMENT } from '@/constants';
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Extra template links
|
* Extra template links
|
||||||
@@ -60,67 +59,3 @@ export const trackTemplatesClick = (source: TemplateClickSource) => {
|
|||||||
source,
|
source,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Personalized templates
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const isPersonalizedTemplatesExperimentEnabled = () => {
|
|
||||||
return (
|
|
||||||
usePostHog().getVariant(TEMPLATE_ONBOARDING_EXPERIMENT.name) ===
|
|
||||||
TEMPLATE_ONBOARDING_EXPERIMENT.variantSuggestedTemplates && useCloudPlanStore().userIsTrialing
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserSkillCount = () => {
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SIMPLE_TEMPLATES = [6035, 1960, 2178];
|
|
||||||
|
|
||||||
const PREDEFINED_TEMPLATES_BY_NODE = {
|
|
||||||
googleSheets: [5694, 5690, 5906],
|
|
||||||
gmail: [5678, 4722, 5694],
|
|
||||||
telegram: [5626, 5784, 4875],
|
|
||||||
openAi: [2462, 2722, 2178],
|
|
||||||
googleGemini: [5993, 6035, 5677],
|
|
||||||
googleCalendar: [2328, 3393, 3657],
|
|
||||||
youTube: [3188, 4846, 4506],
|
|
||||||
airtable: [3053, 2700, 2579],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getPredefinedFromSelected(selectedApps: string[]) {
|
|
||||||
const predefinedNodes = Object.keys(PREDEFINED_TEMPLATES_BY_NODE);
|
|
||||||
const predefinedSelected = predefinedNodes.filter((node) => selectedApps.includes(node));
|
|
||||||
|
|
||||||
return predefinedSelected.reduce<number[]>(
|
|
||||||
(acc, app) => [
|
|
||||||
...acc,
|
|
||||||
...PREDEFINED_TEMPLATES_BY_NODE[app as keyof typeof PREDEFINED_TEMPLATES_BY_NODE],
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSuggestedTemplatesForLowCodingSkill(selectedApps: string[]) {
|
|
||||||
if (selectedApps.length === 0) {
|
|
||||||
return SIMPLE_TEMPLATES;
|
|
||||||
}
|
|
||||||
|
|
||||||
const predefinedSelected = getPredefinedFromSelected(selectedApps);
|
|
||||||
if (predefinedSelected.length > 0) {
|
|
||||||
return predefinedSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTop3Templates(templates: ITemplatesWorkflowFull[]) {
|
|
||||||
if (templates.length <= 3) {
|
|
||||||
return templates;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(new Map(templates.map((t) => [t.id, t])).values())
|
|
||||||
.sort((a, b) => a.totalViews - b.totalViews)
|
|
||||||
.slice(0, 3);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import {
|
|||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
|
||||||
|
import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/components/SuggestedWorkflowCard.vue';
|
||||||
|
import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue';
|
||||||
|
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import type {
|
import type {
|
||||||
@@ -47,7 +51,6 @@ import { type Project, type ProjectSharingData, ProjectTypes } from '@/types/pro
|
|||||||
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||||
import {
|
import {
|
||||||
isExtraTemplateLinksExperimentEnabled,
|
isExtraTemplateLinksExperimentEnabled,
|
||||||
isPersonalizedTemplatesExperimentEnabled,
|
|
||||||
TemplateClickSource,
|
TemplateClickSource,
|
||||||
trackTemplatesClick,
|
trackTemplatesClick,
|
||||||
} from '@/utils/experiments';
|
} from '@/utils/experiments';
|
||||||
@@ -70,7 +73,6 @@ import debounce from 'lodash/debounce';
|
|||||||
import { type IUser, PROJECT_ROOT } from 'n8n-workflow';
|
import { type IUser, PROJECT_ROOT } from 'n8n-workflow';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||||
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
|
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE_TIME = 300;
|
const SEARCH_DEBOUNCE_TIME = 300;
|
||||||
const FILTERS_DEBOUNCE_TIME = 100;
|
const FILTERS_DEBOUNCE_TIME = 100;
|
||||||
@@ -115,6 +117,7 @@ const usageStore = useUsageStore();
|
|||||||
const insightsStore = useInsightsStore();
|
const insightsStore = useInsightsStore();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
||||||
|
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
|
||||||
|
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
@@ -372,10 +375,6 @@ const showRegisteredCommunityCTA = computed(
|
|||||||
() => isSelfHostedDeployment.value && !foldersEnabled.value && canUserRegisterCommunityPlus.value,
|
() => isSelfHostedDeployment.value && !foldersEnabled.value && canUserRegisterCommunityPlus.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const experimentalShowSuggestedWorkflows = computed(() =>
|
|
||||||
isPersonalizedTemplatesExperimentEnabled(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const showAIStarterCollectionCallout = computed(() => {
|
const showAIStarterCollectionCallout = computed(() => {
|
||||||
return (
|
return (
|
||||||
!loading.value &&
|
!loading.value &&
|
||||||
@@ -389,6 +388,10 @@ const showAIStarterCollectionCallout = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showPersonalizedTemplates = computed(
|
||||||
|
() => !loading.value && personalizedTemplatesStore.isFeatureEnabled(),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS
|
* WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS
|
||||||
*/
|
*/
|
||||||
@@ -489,7 +492,6 @@ const initialize = async () => {
|
|||||||
workflowsStore.fetchActiveWorkflows(),
|
workflowsStore.fetchActiveWorkflows(),
|
||||||
usageStore.getLicenseInfo(),
|
usageStore.getLicenseInfo(),
|
||||||
foldersStore.fetchTotalWorkflowsAndFoldersCount(route.params.projectId as string | undefined),
|
foldersStore.fetchTotalWorkflowsAndFoldersCount(route.params.projectId as string | undefined),
|
||||||
templatesStore.experimentalFetchSuggestedWorkflows(),
|
|
||||||
]);
|
]);
|
||||||
breadcrumbsLoading.value = false;
|
breadcrumbsLoading.value = false;
|
||||||
workflowsAndFolders.value = resourcesPage;
|
workflowsAndFolders.value = resourcesPage;
|
||||||
@@ -1701,6 +1703,17 @@ const onNameSubmit = async (name: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</N8nCallout>
|
</N8nCallout>
|
||||||
|
<SuggestedWorkflows v-else-if="showPersonalizedTemplates">
|
||||||
|
<SuggestedWorkflowCard
|
||||||
|
v-for="workflow in personalizedTemplatesStore.suggestedWorkflows"
|
||||||
|
:key="workflow.id"
|
||||||
|
data-test-id="resource-list-item-suggested-workflow"
|
||||||
|
:data="{
|
||||||
|
id: workflow.id,
|
||||||
|
name: workflow.name,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</SuggestedWorkflows>
|
||||||
<N8nCallout
|
<N8nCallout
|
||||||
v-else-if="!loading && showEasyAIWorkflowCallout && easyAICalloutVisible"
|
v-else-if="!loading && showEasyAIWorkflowCallout && easyAICalloutVisible"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
@@ -1728,17 +1741,6 @@ const onNameSubmit = async (name: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</N8nCallout>
|
</N8nCallout>
|
||||||
<SuggestedWorkflows v-if="experimentalShowSuggestedWorkflows">
|
|
||||||
<SuggestedWorkflowCard
|
|
||||||
v-for="workflow in templatesStore.experimentalSuggestedWorkflows"
|
|
||||||
:key="workflow.id"
|
|
||||||
data-test-id="resource-list-item-suggested-workflow"
|
|
||||||
:data="{
|
|
||||||
id: workflow.id,
|
|
||||||
name: workflow.name,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</SuggestedWorkflows>
|
|
||||||
</template>
|
</template>
|
||||||
<template #breadcrumbs>
|
<template #breadcrumbs>
|
||||||
<div v-if="breadcrumbsLoading" :class="$style['breadcrumbs-loading']">
|
<div v-if="breadcrumbsLoading" :class="$style['breadcrumbs-loading']">
|
||||||
|
|||||||
Reference in New Issue
Block a user