From 2ba34dbb746c2f6371af69e5a4e5d461f2f8b7dc Mon Sep 17 00:00:00 2001 From: Svetoslav Dekov Date: Thu, 20 Nov 2025 17:33:01 +0200 Subject: [PATCH] feat(editor): Add templates quality experiment inline section on empty project layout (no-changelog) (#22078) --- .../src/app/views/WorkflowsView.test.ts | 31 --------- .../editor-ui/src/app/views/WorkflowsView.vue | 69 ++++++------------- 2 files changed, 22 insertions(+), 78 deletions(-) diff --git a/packages/frontend/editor-ui/src/app/views/WorkflowsView.test.ts b/packages/frontend/editor-ui/src/app/views/WorkflowsView.test.ts index 4f29c18436b..328690967c9 100644 --- a/packages/frontend/editor-ui/src/app/views/WorkflowsView.test.ts +++ b/packages/frontend/editor-ui/src/app/views/WorkflowsView.test.ts @@ -13,7 +13,6 @@ import { useTagsStore } from '@/features/shared/tags/tags.store'; import { useUsersStore } from '@/features/settings/users/users.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { Project } from '@/features/collaboration/projects/projects.types'; -import { TemplateClickSource } from '@/experiments/utils'; import WorkflowsView from '@/app/views/WorkflowsView.vue'; import { STORES } from '@n8n/stores'; import { createTestingPinia } from '@pinia/testing'; @@ -172,36 +171,6 @@ describe('WorkflowsView', () => { expect(router.currentRoute.value.name).toBe(VIEWS.NEW_WORKFLOW); }); - - it('should show template card', async () => { - const projectsStore = mockedStore(useProjectsStore); - projectsStore.currentProject = { scopes: ['workflow:create'] } as Project; - - settingsStore.settings.templates = { enabled: true, host: 'http://example.com' }; - const { getByTestId } = renderComponent({ pinia }); - await waitAllPromises(); - - expect(getByTestId('new-workflow-from-template-card')).toBeInTheDocument(); - }); - - it('should track template card click', async () => { - const projectsStore = mockedStore(useProjectsStore); - projectsStore.currentProject = { scopes: ['workflow:create'] } as Project; - - settingsStore.settings.templates = { enabled: true, host: 'http://example.com' }; - const { getByTestId } = renderComponent({ pinia }); - await waitAllPromises(); - - const card = getByTestId('new-workflow-from-template-card'); - await userEvent.click(card); - - expect(mockTrack).toHaveBeenCalledWith( - 'User clicked on templates', - expect.objectContaining({ - source: TemplateClickSource.emptyInstanceCard, - }), - ); - }); }); describe('fetch workflow options', () => { diff --git a/packages/frontend/editor-ui/src/app/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/app/views/WorkflowsView.vue index 1cb3a1f5f0c..d46a61719d0 100644 --- a/packages/frontend/editor-ui/src/app/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/app/views/WorkflowsView.vue @@ -21,7 +21,6 @@ import { useCalloutHelpers } from '@/app/composables/useCalloutHelpers'; import { DEFAULT_WORKFLOW_PAGE_SIZE, EnterpriseEditionFeature, - EXPERIMENT_TEMPLATES_DATA_QUALITY_KEY, MODAL_CONFIRM, VIEWS, } from '@/app/constants'; @@ -33,6 +32,7 @@ import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplat import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store'; import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue'; import TemplateRecommendationV3 from '@/experiments/personalizedTemplatesV3/components/TemplateRecommendationV3.vue'; +import TemplatesDataQualityInlineSection from '@/experiments/templatesDataQuality/components/TemplatesDataQualityInlineSection.vue'; import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store'; import { usePersonalizedTemplatesV3Store } from '@/experiments/personalizedTemplatesV3/stores/personalizedTemplatesV3.store'; import EmptyStateLayout from '@/app/components/layouts/EmptyStateLayout.vue'; @@ -55,7 +55,6 @@ import { useProjectsStore } from '@/features/collaboration/projects/projects.sto import { useSettingsStore } from '@/app/stores/settings.store'; import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store'; import { useTagsStore } from '@/features/shared/tags/tags.store'; -import { useTemplatesStore } from '@/features/workflows/templates/templates.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useUsageStore } from '@/features/settings/usage/usage.store'; import { useUsersStore } from '@/features/settings/users/users.store'; @@ -66,11 +65,6 @@ import { ProjectTypes, } from '@/features/collaboration/projects/projects.types'; import { getEasyAiWorkflowJson } from '@/features/workflows/templates/utils/workflowSamples'; -import { - isExtraTemplateLinksExperimentEnabled, - TemplateClickSource, - trackTemplatesClick, -} from '@/experiments/utils'; import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue'; import { useI18n } from '@n8n/i18n'; import { getResourcePermissions } from '@n8n/permissions'; @@ -140,7 +134,6 @@ const tagsStore = useTagsStore(); const foldersStore = useFoldersStore(); const usageStore = useUsageStore(); const insightsStore = useInsightsStore(); -const templatesStore = useTemplatesStore(); const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore(); const personalizedTemplatesStore = usePersonalizedTemplatesStore(); const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore(); @@ -390,16 +383,20 @@ const showEasyAIWorkflowCallout = computed(() => { return !easyAIWorkflowOnboardingDone; }); -const templatesCardEnabled = computed(() => { - return isExtraTemplateLinksExperimentEnabled() && settingsStore.isTemplatesEnabled; -}); - const projectPermissions = computed(() => { return getResourcePermissions( projectsStore.currentProject?.scopes ?? personalProject.value?.scopes, ); }); +const showTemplatesDataQualityInline = computed(() => { + return ( + templatesDataQualityStore.isFeatureEnabled() && + !readOnlyEnv.value && + projectPermissions.value.workflow.create + ); +}); + const showReadyToRunWorkflowsCallout = computed(() => { const isEnabled = readyToRunWorkflowsStore.isFeatureEnabled; const isDismissed = readyToRunWorkflowsStore.isCalloutDismissed; @@ -921,22 +918,6 @@ const addWorkflow = () => { trackEmptyCardClick('blank'); }; -const openTemplatesRepository = async () => { - trackTemplatesClick(TemplateClickSource.emptyInstanceCard); - - if (templatesStore.hasCustomTemplatesHost) { - await router.push({ name: VIEWS.TEMPLATES }); - return; - } - - if (templatesDataQualityStore.isFeatureEnabled()) { - uiStore.openModal(EXPERIMENT_TEMPLATES_DATA_QUALITY_KEY); - return; - } - - window.open(templatesStore.websiteTemplateRepositoryURL, '_blank'); -}; - const trackEmptyCardClick = (option: 'blank' | 'templates' | 'courses') => { telemetry.track('User clicked empty page option', { option, @@ -2247,25 +2228,9 @@ const onNameSubmit = async (name: string) => { - -
- - - {{ i18n.baseText('workflows.empty.startWithTemplate') }} - -
-
+ +
+
@@ -2369,6 +2334,16 @@ const onNameSubmit = async (name: string) => { justify-content: center; } +.templatesContainer { + display: flex; + justify-content: center; + width: 100%; + + > section { + margin-top: var(--spacing--3xl); + } +} + .easy-ai-workflow-callout { // Make the callout padding in line with workflow cards margin-top: var(--spacing--xs);