fix(editor): Set up baseline to empty workflows list (no-changelog) (#25063)

Co-authored-by: Svetoslav Dekov <svetoslav.dekov@n8n.io>
This commit is contained in:
Ricardo Espinoza
2026-02-02 06:36:08 -05:00
committed by GitHub
parent c7c9fa5693
commit e53cff5d83
11 changed files with 212 additions and 160 deletions
@@ -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": "<a href=\"#\">Back to Personal</a>",
"workflows.empty.readyToRun": "Run live demo",
@@ -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();
@@ -76,6 +76,9 @@ describe('EmptyStateLayout', () => {
} as unknown as ReturnType<typeof useSourceControlStore>['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<typeof useSourceControlStore>['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',
@@ -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) => {
</div>
</template>
<!-- State 3: Fallback -->
<!-- State 3: Fallback (Baseline) -->
<template v-else>
<N8nHeading tag="h1" size="2xlarge" bold :class="$style.welcomeTitle">
{{ emptyStateHeading }}
@@ -124,25 +143,53 @@ const handleBuilderPromptSubmit = async (prompt: string) => {
<N8nText tag="p" size="large" color="text-base">
{{ emptyStateDescription }}
</N8nText>
<N8nCard
<!-- Two cards or single card depending on ready-to-run availability -->
<div
v-if="canCreateWorkflow"
:class="$style.actionCard"
hoverable
data-test-id="new-workflow-card"
@click="addWorkflow"
:class="[$style.actionCardsContainer, { [$style.singleCard]: !showReadyToRunCard }]"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="file"
color="foreground-dark"
:stroke-width="1.5"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.startFromScratch') }}
</N8nText>
</div>
</N8nCard>
<!-- Card 1: Try AI workflow (conditional) -->
<N8nCard
v-if="showReadyToRunCard"
:class="$style.actionCard"
hoverable
data-test-id="ready-to-run-card"
@click="handleReadyToRunClick"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="zap"
color="foreground-dark"
:stroke-width="1.5"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.tryAiWorkflow') }}
</N8nText>
</div>
</N8nCard>
<!-- Card 2: Start from scratch (always shown) -->
<N8nCard
:class="$style.actionCard"
hoverable
data-test-id="new-workflow-card"
@click="addWorkflow"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="file"
color="foreground-dark"
:stroke-width="1.5"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.startFromScratch') }}
</N8nText>
</div>
</N8nCard>
</div>
</div>
</template>
</div>
@@ -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;
@@ -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,
];
@@ -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<string | null>(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);
@@ -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),
});
@@ -37,7 +37,7 @@ export function useWorkflowsEmptyState() {
const showRecommendedTemplatesInline = computed(() => {
return (
recommendedTemplatesStore.isFeatureEnabled() &&
recommendedTemplatesStore.isFeatureEnabled &&
!readOnlyEnv.value &&
projectPermissions.value.workflow.create
);
@@ -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();
});
});
});
@@ -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<ITemplatesWorkflowFull | null> {
return await templatesStore.fetchTemplateById(templateId.toString());
@@ -61,19 +61,15 @@ export const useRecommendedTemplatesStore = defineStore('recommendedTemplates',
async function loadRecommendedTemplates(): Promise<ITemplatesWorkflowFull[]> {
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);
@@ -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() {