diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 1d4eb8ba3fc..63d46c3f0ab 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3558,6 +3558,7 @@ "workflows.empty.learnN8n": "Learn n8n", "workflows.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create workflows", "workflows.empty.easyAI": "Test a simple AI Agent example", + "workflows.empty.tryAiWorkflow": "Run live demo", "workflows.empty.shared-with-me": "No {resource} has been shared with you", "workflows.empty.shared-with-me.link": "Back to Personal", "workflows.empty.readyToRun": "Run live demo", diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts index 8bc267ee03e..97acab0f2a5 100644 --- a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts +++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts @@ -73,7 +73,7 @@ describe('MainSidebar', () => { // Default experiment store values personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false); personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => false); - recommendedTemplatesStore.isFeatureEnabled = vi.fn(() => false); + recommendedTemplatesStore.isFeatureEnabled = false; }); it('renders the sidebar without error', () => { @@ -123,7 +123,7 @@ describe('MainSidebar', () => { templatesStore.hasCustomTemplatesHost = false; personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false); personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => false); - recommendedTemplatesStore.isFeatureEnabled = vi.fn(() => false); + recommendedTemplatesStore.isFeatureEnabled = false; const { getAllByTestId } = renderComponent(); @@ -136,7 +136,7 @@ describe('MainSidebar', () => { settingsStore.isTemplatesEnabled = true; personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => true); personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false); - recommendedTemplatesStore.isFeatureEnabled = vi.fn(() => false); + recommendedTemplatesStore.isFeatureEnabled = false; const { getAllByTestId } = renderComponent(); diff --git a/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.test.ts b/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.test.ts index 78ebd19a20e..2176f7207f0 100644 --- a/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.test.ts +++ b/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.test.ts @@ -76,6 +76,9 @@ describe('EmptyStateLayout', () => { } as unknown as ReturnType['preferences']; bannersStore.bannersHeight = 0; + + // Default: feature disabled (control variant) + recommendedTemplatesStore.isFeatureEnabled = false; }); afterEach(() => { @@ -84,7 +87,7 @@ describe('EmptyStateLayout', () => { describe('when recommended templates feature is enabled', () => { beforeEach(() => { - recommendedTemplatesStore.isFeatureEnabled.mockReturnValue(true); + recommendedTemplatesStore.isFeatureEnabled = true; }); it('should render welcome heading with user name', () => { @@ -129,7 +132,7 @@ describe('EmptyStateLayout', () => { describe('when recommended templates feature is disabled', () => { beforeEach(() => { - recommendedTemplatesStore.isFeatureEnabled.mockReturnValue(false); + recommendedTemplatesStore.isFeatureEnabled = false; }); it('should render heading with user name', () => { @@ -174,7 +177,7 @@ describe('EmptyStateLayout', () => { describe('when in read-only environment', () => { beforeEach(() => { - recommendedTemplatesStore.isFeatureEnabled.mockReturnValue(true); + recommendedTemplatesStore.isFeatureEnabled = true; sourceControlStore.preferences = { branchReadOnly: true, } as unknown as ReturnType['preferences']; @@ -195,7 +198,7 @@ describe('EmptyStateLayout', () => { describe('when user does not have workflow create permission', () => { beforeEach(() => { - recommendedTemplatesStore.isFeatureEnabled.mockReturnValue(true); + recommendedTemplatesStore.isFeatureEnabled = true; projectsStore.personalProject = { id: 'personal-project-1', name: 'Personal Project', diff --git a/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.vue b/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.vue index a4f7f550175..42ef30b4286 100644 --- a/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.vue +++ b/packages/frontend/editor-ui/src/app/components/layouts/EmptyStateLayout.vue @@ -8,6 +8,7 @@ import { useProjectsStore } from '@/features/collaboration/projects/projects.sto import { useProjectPages } from '@/features/collaboration/projects/composables/useProjectPages'; import { useWorkflowsEmptyState } from '@/features/workflows/composables/useWorkflowsEmptyState'; import { useEmptyStateBuilderPromptStore } from '@/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store'; +import { useReadyToRunStore } from '@/features/workflows/readyToRun/stores/readyToRun.store'; import RecommendedTemplatesSection from '@/features/workflows/templates/recommendations/components/RecommendedTemplatesSection.vue'; import ReadyToRunButton from '@/features/workflows/readyToRun/components/ReadyToRunButton.vue'; import EmptyStateBuilderPrompt from '@/experiments/emptyStateBuilderPrompt/components/EmptyStateBuilderPrompt.vue'; @@ -22,6 +23,7 @@ const bannersStore = useBannersStore(); const projectsStore = useProjectsStore(); const projectPages = useProjectPages(); const emptyStateBuilderPromptStore = useEmptyStateBuilderPromptStore(); +const readyToRunStore = useReadyToRunStore(); const { showBuilderPrompt, @@ -36,6 +38,23 @@ const addWorkflow = () => { emit('click:add'); }; +// Check if user can claim credits for ready-to-run +const showReadyToRunCard = computed(() => { + return readyToRunStore.userCanClaimOpenAiCredits && canCreateWorkflow.value; +}); + +const handleReadyToRunClick = async () => { + try { + await readyToRunStore.claimCreditsAndOpenWorkflow( + 'card', + builderParentFolderId.value, + builderProjectId.value, + ); + } catch { + // Error already shown by store + } +}; + const containerStyle = computed(() => ({ minHeight: `calc(100vh - ${bannersStore.bannersHeight}px)`, })); @@ -115,7 +134,7 @@ const handleBuilderPromptSubmit = async (prompt: string) => { - + @@ -206,6 +253,24 @@ const handleBuilderPromptSubmit = async (prompt: string) => { text-align: center; } +.actionCardsContainer { + display: grid; + grid-template-columns: repeat(2, 192px); + gap: var(--spacing--lg); + margin-top: var(--spacing--2xl); + justify-content: center; + + &.singleCard { + grid-template-columns: 192px; + } + + @media (max-width: vars.$breakpoint-xs) { + grid-template-columns: 1fr; + gap: var(--spacing--md); + margin-top: var(--spacing--lg); + } +} + .actionCard { width: 192px; height: 230px; @@ -213,7 +278,6 @@ const handleBuilderPromptSubmit = async (prompt: string) => { display: flex; align-items: center; justify-content: center; - margin-top: var(--spacing--2xl); transition: transform 0.2s ease, box-shadow 0.2s ease; diff --git a/packages/frontend/editor-ui/src/app/constants/experiments.ts b/packages/frontend/editor-ui/src/app/constants/experiments.ts index 7c3cd02cb88..b9351e129c8 100644 --- a/packages/frontend/editor-ui/src/app/constants/experiments.ts +++ b/packages/frontend/editor-ui/src/app/constants/experiments.ts @@ -57,17 +57,17 @@ export const EXECUTION_LOGIC_V2_EXPERIMENT = { export const TAMPER_PROOF_INVITE_LINKS = createExperiment('061_tamper_proof_invite_links'); -export const EMPTY_STATE_BUILDER_PROMPT_EXPERIMENT = createExperiment( - '063_empty_state_builder_prompt', -); - export const RESOURCE_CENTER_EXPERIMENT = createExperiment('063_resource_center_0', { control: 'control', variantResources: 'variant-resources', variantInspiration: 'variant-inspiration', }); -export const DYNAMIC_TEMPLATES_EXPERIMENT = createExperiment('068_dynamic_templates'); +export const EMPTY_STATE_EXPERIMENT = createExperiment('070_empty_screen_layout', { + control: 'control', + variantBuilderPrompt: 'variant-builder-prompt', + variantTemplates: 'variant-templates', +}); export const EXPERIMENTS_TO_TRACK = [ EXTRA_TEMPLATE_LINKS_EXPERIMENT.name, @@ -81,6 +81,5 @@ export const EXPERIMENTS_TO_TRACK = [ EXECUTION_LOGIC_V2_EXPERIMENT.name, COLLECTION_OVERHAUL_EXPERIMENT.name, TAMPER_PROOF_INVITE_LINKS.name, - EMPTY_STATE_BUILDER_PROMPT_EXPERIMENT.name, - DYNAMIC_TEMPLATES_EXPERIMENT.name, + EMPTY_STATE_EXPERIMENT.name, ]; diff --git a/packages/frontend/editor-ui/src/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store.ts b/packages/frontend/editor-ui/src/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store.ts index 16f9e1a1aaf..b361acf4f7c 100644 --- a/packages/frontend/editor-ui/src/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store.ts +++ b/packages/frontend/editor-ui/src/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store.ts @@ -1,9 +1,5 @@ import { useTelemetry } from '@/app/composables/useTelemetry'; -import { - DEFAULT_NEW_WORKFLOW_NAME, - EMPTY_STATE_BUILDER_PROMPT_EXPERIMENT, - VIEWS, -} from '@/app/constants'; +import { DEFAULT_NEW_WORKFLOW_NAME, EMPTY_STATE_EXPERIMENT, VIEWS } from '@/app/constants'; import { useCloudPlanStore } from '@/app/stores/cloudPlan.store'; import { usePostHog } from '@/app/stores/posthog.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; @@ -53,12 +49,10 @@ export const useEmptyStateBuilderPromptStore = defineStore( const pendingPrompt = ref(null); // Experiment variant detection - const currentVariant = computed(() => - posthogStore.getVariant(EMPTY_STATE_BUILDER_PROMPT_EXPERIMENT.name), - ); + const currentVariant = computed(() => posthogStore.getVariant(EMPTY_STATE_EXPERIMENT.name)); const isVariant = computed( - () => currentVariant.value === EMPTY_STATE_BUILDER_PROMPT_EXPERIMENT.variant, + () => currentVariant.value === EMPTY_STATE_EXPERIMENT.variantBuilderPrompt, ); const isFeatureEnabled = computed(() => cloudPlanStore.userIsTrialing && isVariant.value); diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeAddNodes.test.ts b/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeAddNodes.test.ts index d5665b87f28..992ebc8be7f 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeAddNodes.test.ts +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeAddNodes.test.ts @@ -121,7 +121,9 @@ describe('CanvasNodeAddNodes', () => { it('should track user click', async () => { settingsStore.settings.templates = { enabled: true, host: '' }; - recommendedTemplatesStore.isFeatureEnabled = vi.fn(() => false); + Object.defineProperty(recommendedTemplatesStore, 'isFeatureEnabled', { + get: vi.fn(() => false), + }); const { getByTestId } = renderComponent({ global: { @@ -159,7 +161,9 @@ describe('CanvasNodeAddNodes', () => { it('should open window to template repository when no custom host and feature disabled', async () => { settingsStore.settings.templates = { enabled: true, host: '' }; - recommendedTemplatesStore.isFeatureEnabled = vi.fn(() => false); + Object.defineProperty(recommendedTemplatesStore, 'isFeatureEnabled', { + get: vi.fn(() => false), + }); Object.defineProperty(templatesStore, 'hasCustomTemplatesHost', { get: vi.fn(() => false), }); diff --git a/packages/frontend/editor-ui/src/features/workflows/composables/useWorkflowsEmptyState.ts b/packages/frontend/editor-ui/src/features/workflows/composables/useWorkflowsEmptyState.ts index 472319fc10d..36b4ff121d9 100644 --- a/packages/frontend/editor-ui/src/features/workflows/composables/useWorkflowsEmptyState.ts +++ b/packages/frontend/editor-ui/src/features/workflows/composables/useWorkflowsEmptyState.ts @@ -37,7 +37,7 @@ export function useWorkflowsEmptyState() { const showRecommendedTemplatesInline = computed(() => { return ( - recommendedTemplatesStore.isFeatureEnabled() && + recommendedTemplatesStore.isFeatureEnabled && !readOnlyEnv.value && projectPermissions.value.workflow.create ); diff --git a/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.test.ts b/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.test.ts index 4425598d229..0b466ea92cd 100644 --- a/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.test.ts +++ b/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.test.ts @@ -2,19 +2,19 @@ import { createPinia, setActivePinia } from 'pinia'; import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client'; import { mock } from 'vitest-mock-extended'; import { useRecommendedTemplatesStore, NUMBER_OF_TEMPLATES } from './recommendedTemplates.store'; -import { VIEWS } from '@/app/constants'; +import { EMPTY_STATE_EXPERIMENT, VIEWS } from '@/app/constants'; -const { getDynamicRecommendedTemplates, mockTelemetry, mockPostHog, mockFetchTemplateById } = +const { getDynamicRecommendedTemplates, mockTelemetry, mockFetchTemplateById, mockPostHog } = vi.hoisted(() => { return { getDynamicRecommendedTemplates: vi.fn(), mockTelemetry: { track: vi.fn(), }, - mockPostHog: { - isVariantEnabled: vi.fn(), - }, mockFetchTemplateById: vi.fn(), + mockPostHog: { + getVariant: vi.fn(), + }, }; }); @@ -68,12 +68,21 @@ describe('useRecommendedTemplatesStore', () => { beforeEach(() => { vi.clearAllMocks(); setActivePinia(createPinia()); + // Default to templates variant enabled + mockPostHog.getVariant.mockReturnValue(EMPTY_STATE_EXPERIMENT.variantTemplates); store = useRecommendedTemplatesStore(); }); describe('isFeatureEnabled', () => { - it('should return true when templates are enabled and no custom host', () => { - expect(store.isFeatureEnabled()).toBe(true); + it('should return true when templates are enabled, no custom host, and variant is templates', () => { + expect(store.isFeatureEnabled).toBe(true); + }); + + it('should return false when variant is not templates', () => { + mockPostHog.getVariant.mockReturnValue(EMPTY_STATE_EXPERIMENT.control); + // Need to recreate the store after changing the mock + store = useRecommendedTemplatesStore(); + expect(store.isFeatureEnabled).toBe(false); }); }); @@ -142,102 +151,84 @@ describe('useRecommendedTemplatesStore', () => { }); describe('loadRecommendedTemplates', () => { - describe('when dynamic templates experiment is enabled', () => { - beforeEach(() => { - mockPostHog.isVariantEnabled.mockReturnValue(true); - }); + it('should fetch templates from dynamic API on success', async () => { + const mockTemplates = [ + { workflow: createMockTemplate(1) }, + { workflow: createMockTemplate(2) }, + { workflow: createMockTemplate(3) }, + ]; + getDynamicRecommendedTemplates.mockResolvedValue({ templates: mockTemplates }); - it('should fetch templates from dynamic API on success', async () => { - const mockTemplates = [ - { workflow: createMockTemplate(1) }, - { workflow: createMockTemplate(2) }, - { workflow: createMockTemplate(3) }, - ]; - getDynamicRecommendedTemplates.mockResolvedValue({ templates: mockTemplates }); + const result = await store.loadRecommendedTemplates(); - const result = await store.loadRecommendedTemplates(); - - expect(getDynamicRecommendedTemplates).toHaveBeenCalledWith({ baseUrl: '/rest' }); - expect(result).toHaveLength(3); - expect(result[0].id).toBe(1); - expect(result[1].id).toBe(2); - expect(result[2].id).toBe(3); - }); - - it('should limit templates to NUMBER_OF_TEMPLATES', async () => { - const mockTemplates = Array.from({ length: 10 }, (_, i) => ({ - workflow: createMockTemplate(i + 1), - })); - getDynamicRecommendedTemplates.mockResolvedValue({ templates: mockTemplates }); - - const result = await store.loadRecommendedTemplates(); - - expect(result).toHaveLength(NUMBER_OF_TEMPLATES); - }); - - it('should fallback to static IDs when API fails', async () => { - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - getDynamicRecommendedTemplates.mockRejectedValue(new Error('API Error')); - - const mockTemplate = createMockTemplate(7607); - mockFetchTemplateById.mockResolvedValue(mockTemplate); - - const result = await store.loadRecommendedTemplates(); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Dynamic templates failed, falling back to static IDs', - expect.any(Error), - ); - expect(mockFetchTemplateById).toHaveBeenCalled(); - expect(result.length).toBeGreaterThan(0); - - consoleSpy.mockRestore(); - }); - - it('should return empty array when API returns empty templates', async () => { - getDynamicRecommendedTemplates.mockResolvedValue({ templates: [] }); - - const result = await store.loadRecommendedTemplates(); - - expect(result).toEqual([]); - }); + expect(getDynamicRecommendedTemplates).toHaveBeenCalledWith({ baseUrl: '/rest' }); + expect(result).toHaveLength(3); + expect(result[0].id).toBe(1); + expect(result[1].id).toBe(2); + expect(result[2].id).toBe(3); }); - describe('when dynamic templates experiment is disabled', () => { - beforeEach(() => { - mockPostHog.isVariantEnabled.mockReturnValue(false); - }); + it('should limit templates to NUMBER_OF_TEMPLATES', async () => { + const mockTemplates = Array.from({ length: 10 }, (_, i) => ({ + workflow: createMockTemplate(i + 1), + })); + getDynamicRecommendedTemplates.mockResolvedValue({ templates: mockTemplates }); - it('should fetch templates using static IDs', async () => { - const mockTemplate = createMockTemplate(7607); - mockFetchTemplateById.mockResolvedValue(mockTemplate); + const result = await store.loadRecommendedTemplates(); - const result = await store.loadRecommendedTemplates(); + expect(result).toHaveLength(NUMBER_OF_TEMPLATES); + }); - expect(getDynamicRecommendedTemplates).not.toHaveBeenCalled(); - expect(mockFetchTemplateById).toHaveBeenCalled(); - expect(result.length).toBeGreaterThan(0); - }); + it('should fallback to static IDs when dynamic API fails', async () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + getDynamicRecommendedTemplates.mockRejectedValue(new Error('API Error')); - it('should filter out failed template fetches', async () => { - // Setup: 4 successful, 1 null, 1 rejected - mockFetchTemplateById - .mockResolvedValueOnce(createMockTemplate(1)) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(createMockTemplate(3)) - .mockRejectedValueOnce(new Error('Fetch error')) - .mockResolvedValueOnce(createMockTemplate(5)) - .mockResolvedValueOnce(createMockTemplate(6)); + const mockTemplate = createMockTemplate(7607); + mockFetchTemplateById.mockResolvedValue(mockTemplate); - const result = await store.loadRecommendedTemplates(); + const result = await store.loadRecommendedTemplates(); - // Verify no null values in result - expect(result.every((t) => t !== null)).toBe(true); + expect(consoleSpy).toHaveBeenCalledWith( + 'Dynamic templates failed, falling back to static IDs', + expect.any(Error), + ); + expect(mockFetchTemplateById).toHaveBeenCalled(); + expect(result.length).toBeGreaterThan(0); - // Verify only successfully fetched templates are included (4 out of 6) - expect(result).toHaveLength(4); - expect(result.map((t) => t.id)).toEqual([1, 3, 5, 6]); - }); + consoleSpy.mockRestore(); + }); + + it('should return empty array when API returns empty templates', async () => { + getDynamicRecommendedTemplates.mockResolvedValue({ templates: [] }); + + const result = await store.loadRecommendedTemplates(); + + expect(result).toEqual([]); + }); + + it('should filter out failed template fetches during fallback', async () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + getDynamicRecommendedTemplates.mockRejectedValue(new Error('API Error')); + + // Setup: 4 successful, 1 null, 1 rejected + mockFetchTemplateById + .mockResolvedValueOnce(createMockTemplate(1)) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(createMockTemplate(3)) + .mockRejectedValueOnce(new Error('Fetch error')) + .mockResolvedValueOnce(createMockTemplate(5)) + .mockResolvedValueOnce(createMockTemplate(6)); + + const result = await store.loadRecommendedTemplates(); + + // Verify no null values in result + expect(result.every((t) => t !== null)).toBe(true); + + // Verify only successfully fetched templates are included (4 out of 6) + expect(result).toHaveLength(4); + expect(result.map((t) => t.id)).toEqual([1, 3, 5, 6]); + + consoleSpy.mockRestore(); }); }); }); diff --git a/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.ts b/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.ts index 2e33c41c8dd..301d9c48d24 100644 --- a/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.ts +++ b/packages/frontend/editor-ui/src/features/workflows/templates/recommendations/recommendedTemplates.store.ts @@ -1,11 +1,12 @@ import { useTelemetry } from '@/app/composables/useTelemetry'; -import { DYNAMIC_TEMPLATES_EXPERIMENT, VIEWS } from '@/app/constants'; +import { computed } from 'vue'; +import { EMPTY_STATE_EXPERIMENT, VIEWS } from '@/app/constants'; import { useTemplatesStore } from '@/features/workflows/templates/templates.store'; import { defineStore } from 'pinia'; +import { usePostHog } from '@/app/stores/posthog.store'; import templateIds from './data/recommendedTemplateIds.json'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { usePostHog } from '@/app/stores/posthog.store'; import { useRootStore } from '@n8n/stores/useRootStore'; import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client'; import sampleSize from 'lodash/sampleSize'; @@ -21,16 +22,15 @@ export const useRecommendedTemplatesStore = defineStore('recommendedTemplates', const posthogStore = usePostHog(); const rootStore = useRootStore(); - const isFeatureEnabled = () => { - return settingsStore.isTemplatesEnabled && !templatesStore.hasCustomTemplatesHost; - }; - - const isDynamicTemplatesEnabled = () => { - return posthogStore.isVariantEnabled( - DYNAMIC_TEMPLATES_EXPERIMENT.name, - DYNAMIC_TEMPLATES_EXPERIMENT.variant, + const isFeatureEnabled = computed(() => { + const emptyStateVariant = posthogStore.getVariant(EMPTY_STATE_EXPERIMENT.name); + const isTemplatesVariant = emptyStateVariant === EMPTY_STATE_EXPERIMENT.variantTemplates; + return ( + settingsStore.isTemplatesEnabled && + !templatesStore.hasCustomTemplatesHost && + isTemplatesVariant ); - }; + }); async function getTemplateData(templateId: number): Promise { return await templatesStore.fetchTemplateById(templateId.toString()); @@ -61,19 +61,15 @@ export const useRecommendedTemplatesStore = defineStore('recommendedTemplates', async function loadRecommendedTemplates(): Promise { await nodeTypesStore.loadNodeTypesIfNotLoaded(); - if (isDynamicTemplatesEnabled()) { - try { - const response = await getDynamicRecommendedTemplates(rootStore.restApiContext); - return response.templates - .map((template) => template.workflow) - .slice(0, NUMBER_OF_TEMPLATES); - } catch (error) { - // Fallback to existing behavior on error - console.warn('Dynamic templates failed, falling back to static IDs', error); - } + // Always try dynamic templates first, fallback to static on error + try { + const response = await getDynamicRecommendedTemplates(rootStore.restApiContext); + return response.templates.map((template) => template.workflow).slice(0, NUMBER_OF_TEMPLATES); + } catch (error) { + console.warn('Dynamic templates failed, falling back to static IDs', error); } - // Existing behavior (fallback or flag disabled) + // Fallback to static template IDs const ids = getRandomTemplateIds(); const promises = ids.map(async (id) => await getTemplateData(id)); const results = await Promise.allSettled(promises); diff --git a/packages/testing/playwright/pages/WorkflowsPage.ts b/packages/testing/playwright/pages/WorkflowsPage.ts index b94f9559980..9ff0b5a76a0 100644 --- a/packages/testing/playwright/pages/WorkflowsPage.ts +++ b/packages/testing/playwright/pages/WorkflowsPage.ts @@ -20,7 +20,7 @@ export class WorkflowsPage extends BasePage { * This is the new workflow button on the workflows page, visible when there are no workflows. */ async clickNewWorkflowButtonFromOverview() { - await this.clickByTestId('start-from-scratch-button'); + await this.clickByTestId('new-workflow-card'); } async clickNewWorkflowButtonFromProject() {