feat(editor): Update suggested workflows experiment (no-changelog) (#17701)

This commit is contained in:
Romeo Balta
2025-07-28 09:45:58 +01:00
committed by GitHub
parent 980878398e
commit 2043c6da53
10 changed files with 228 additions and 182 deletions

View File

@@ -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[];
}; };

View File

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

View File

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

View File

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

View File

@@ -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,
};
});

View File

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

View File

@@ -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,
}; };
}); });

View File

@@ -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);
}

View File

@@ -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']">