diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts
index 39669089314..db0d62fe11f 100644
--- a/packages/cli/src/workflows/workflow.service.ts
+++ b/packages/cli/src/workflows/workflow.service.ts
@@ -30,6 +30,10 @@ import { FileLocation, BinaryDataService } from 'n8n-core';
import { NodeApiError, PROJECT_ROOT, assert } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
+import { WorkflowFinderService } from './workflow-finder.service';
+import { WorkflowHistoryService } from './workflow-history/workflow-history.service';
+import { WorkflowSharingService } from './workflow-sharing.service';
+
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@@ -41,16 +45,11 @@ import { validateEntity } from '@/generic-helpers';
import type { ListQuery } from '@/requests';
import { hasSharing } from '@/requests';
import { OwnershipService } from '@/services/ownership.service';
-// eslint-disable-next-line import-x/no-cycle
import { ProjectService } from '@/services/project.service.ee';
import { RoleService } from '@/services/role.service';
import { TagService } from '@/services/tag.service';
import * as WorkflowHelpers from '@/workflow-helpers';
-import { WorkflowFinderService } from './workflow-finder.service';
-import { WorkflowHistoryService } from './workflow-history/workflow-history.service';
-import { WorkflowSharingService } from './workflow-sharing.service';
-
@Service()
export class WorkflowService {
constructor(
diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts
index 97b24da0762..1705a64df35 100644
--- a/packages/cli/test/integration/public-api/workflows.test.ts
+++ b/packages/cli/test/integration/public-api/workflows.test.ts
@@ -16,18 +16,18 @@ import { InstanceSettings } from 'n8n-core';
import type { INode } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
-import { ActiveWorkflowManager } from '@/active-workflow-manager';
-import { STARTING_NODES } from '@/constants';
-import { ExecutionService } from '@/executions/execution.service';
-import { ProjectService } from '@/services/project.service.ee';
-import { Telemetry } from '@/telemetry';
-
import { createTag } from '../shared/db/tags';
import { createMemberWithApiKey, createOwnerWithApiKey } from '../shared/db/users';
import { createWorkflowHistoryItem } from '../shared/db/workflow-history';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
+import { ActiveWorkflowManager } from '@/active-workflow-manager';
+import { STARTING_NODES } from '@/constants';
+import { ExecutionService } from '@/executions/execution.service';
+import { ProjectService } from '@/services/project.service.ee';
+import { Telemetry } from '@/telemetry';
+
mockInstance(Telemetry);
let ownerPersonalProject: Project;
diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts
index 9f21c635c61..bf31974b2e0 100644
--- a/packages/cli/test/integration/workflows/workflows.controller.test.ts
+++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts
@@ -29,17 +29,11 @@ import {
} from '@n8n/db';
import { Container } from '@n8n/di';
import type { Scope } from '@n8n/permissions';
+import { createFolder } from '@test-integration/db/folders';
import { DateTime } from 'luxon';
import { PROJECT_ROOT, type INode, type IPinData, type IWorkflowBase } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
-import { ActiveWorkflowManager } from '@/active-workflow-manager';
-import { EventService } from '@/events/event.service';
-import { License } from '@/license';
-import { ProjectService } from '@/services/project.service.ee';
-import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
-import { createFolder } from '@test-integration/db/folders';
-
import { saveCredential } from '../shared/db/credentials';
import { createCustomRoleWithScopeSlugs, cleanupRolesAndScopes } from '../shared/db/roles';
import { assignTagToWorkflow, createTag } from '../shared/db/tags';
@@ -49,6 +43,12 @@ import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
import { makeWorkflow, MOCK_PINDATA } from '../shared/utils/';
+import { ActiveWorkflowManager } from '@/active-workflow-manager';
+import { EventService } from '@/events/event.service';
+import { License } from '@/license';
+import { ProjectService } from '@/services/project.service.ee';
+import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
+
let owner: User;
let member: User;
let anotherMember: User;
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
index 41e96790ec5..cde4b1f019f 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
@@ -81,7 +81,7 @@ const iconColor = computed(() => {
:color="iconColor"
>{{ item.icon.value }}
-
+
{{ item.label }}
@@ -98,17 +98,12 @@ const iconColor = computed(() => {
margin-bottom: var(--spacing--5xs);
}
-.router-link-active,
-.active {
- background-color: var(--color--foreground);
-}
-
.menuItem {
display: flex;
align-items: center;
justify-content: center;
- padding: var(--spacing--3xs);
- gap: var(--spacing--3xs);
+ padding: var(--spacing--4xs);
+ gap: var(--spacing--4xs);
cursor: pointer;
color: var(--color--text);
border-radius: var(--spacing--4xs);
@@ -120,11 +115,16 @@ const iconColor = computed(() => {
&:hover .menuItemIcon {
color: var(--color--text--shade-1);
}
-}
-.menuItem:hover:not(.active) {
- background-color: var(--color--background--light-1);
- color: var(--color--text--shade-1);
+ &:global(.router-link-active),
+ &.active {
+ background-color: var(--color--background--light-1);
+ }
+
+ &:hover:not(.active):not(:global(.router-link-active)) {
+ background-color: var(--color--background--light-1);
+ color: var(--color--text--shade-1);
+ }
}
.menuItem:focus-visible {
@@ -147,15 +147,18 @@ const iconColor = computed(() => {
.menuItemIcon {
position: relative;
- width: var(--spacing--sm);
- height: var(--spacing--sm);
- min-width: var(--spacing--sm);
+ width: var(--spacing--lg);
+ height: var(--spacing--lg);
+ min-width: var(--spacing--lg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
&.notification::after {
content: '';
position: absolute;
- top: calc(var(--spacing--5xs) * -1);
- right: calc(var(--spacing--5xs) * -1);
+ top: 0;
+ right: 0;
width: var(--spacing--4xs);
height: var(--spacing--4xs);
background-color: var(--color--danger);
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index 4f126150dbd..9749d35d1af 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -1258,6 +1258,8 @@
"logs.details.body.multipleInputs": "Multiple inputs. View them by {button}",
"logs.details.body.multipleInputs.openingTheNode": "opening the node",
"mainSidebar.aboutN8n": "About n8n",
+ "mainSidebar.state.collapse": "Collapse sidebar",
+ "mainSidebar.state.expand": "Expand sidebar",
"mainSidebar.confirmMessage.workflowArchive.cancelButtonText": "",
"mainSidebar.confirmMessage.workflowArchive.confirmButtonText": "Yes, archive",
"mainSidebar.confirmMessage.workflowArchive.headline": "Archive Workflow?",
@@ -1283,6 +1285,7 @@
"mainSidebar.prompt.invalidUrl": "Invalid URL",
"mainSidebar.prompt.workflowUrl": "Workflow URL",
"mainSidebar.save": "@:_reusableBaseText.save",
+ "mainSidebar.settings": "Settings",
"mainSidebar.showError.stopExecution.title": "Problem stopping execution",
"mainSidebar.showMessage.handleFileImport.message": "The file does not contain valid JSON data",
"mainSidebar.showMessage.handleFileImport.title": "Could not import file",
@@ -3866,7 +3869,7 @@
"templateSetup.credential.description": "The credential you select will be used in the {0} node of the workflow template. | The credential you select will be used in the {0} nodes of the workflow template.",
"templateSetup.continue.button.fillRemaining": "Fill remaining credentials to continue",
"setupCredentialsModal.title": "Set up template",
- "becomeCreator.text": "Share your workflows with 40k+ users, unlock perks, and shine as a featured template creator!",
+ "becomeCreator.text": "Share your workflows with other users, unlock perks, and become a featured template creator!",
"becomeCreator.buttonText": "Become a creator",
"becomeCreator.closeButtonTitle": "Close",
"feedback.title": "Was this helpful?",
@@ -4065,7 +4068,7 @@
"insights.upgradeModal.perks.1": "Zoom into last 24 hours with hourly granularity",
"insights.upgradeModal.perks.2": "Gain deeper visibility into workflow trends over time",
"insights.upgradeModal.title": "Upgrade to Enterprise",
- "whatsNew.versionsBehind": "{count} version behind | {count} versions behind",
+ "whatsNew.versionsBehind": "Update ({count} version behind) | Update ({count} versions behind)",
"whatsNew.update": "Update",
"whatsNew.updateNudgeTooltip": "Only owners can perform updates",
"whatsNew.updateAvailable": "You're currently on version {currentVersion}. Update to {latestVersion} to get {count} versions worth of new features, improvements, and fixes. See what changed",
diff --git a/packages/frontend/editor-ui/src/app/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue b/packages/frontend/editor-ui/src/app/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue
index 01e496135c4..1ae72b26c8c 100644
--- a/packages/frontend/editor-ui/src/app/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue
+++ b/packages/frontend/editor-ui/src/app/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue
@@ -3,7 +3,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
import { useBecomeTemplateCreatorStore } from './becomeTemplateCreatorStore';
import { useI18n } from '@n8n/i18n';
-import { N8nButton, N8nIcon } from '@n8n/design-system';
+import { N8nButton, N8nIcon, N8nText } from '@n8n/design-system';
const i18n = useI18n();
const store = useBecomeTemplateCreatorStore();
const telemetry = useTelemetry();
@@ -20,23 +20,22 @@ const onClick = () => {
data-test-id="become-template-creator-cta"
>
-
+
{{ i18n.baseText('becomeCreator.text') }}
-
+
{
flex-direction: column;
background-color: var(--color--background--light-2);
border: var(--border);
- border-right: 0;
+ margin: var(--spacing--2xs);
+ border-radius: var(--radius);
}
.textAndCloseButton {
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 f537b71d706..32a034e0383 100644
--- a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts
@@ -9,10 +9,17 @@ import { useUIStore } from '@/app/stores/ui.store';
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
import { useVersionsStore } from '@/app/stores/versions.store';
import { useUsersStore } from '@/features/settings/users/users.store';
+import { useTemplatesStore } from '@/features/workflows/templates/templates.store';
+import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store';
+import { usePersonalizedTemplatesV3Store } from '@/experiments/personalizedTemplatesV3/stores/personalizedTemplatesV3.store';
+import { useTemplatesDataQualityStore } from '@/experiments/templatesDataQuality/stores/templatesDataQuality.store';
import type { Version } from '@n8n/rest-api-client/api/versions';
+import { ABOUT_MODAL_KEY, WHATS_NEW_MODAL_KEY } from '@/app/constants';
vi.mock('vue-router', () => ({
- useRouter: () => ({}),
+ useRouter: () => ({
+ resolve: vi.fn(() => ({ meta: {} })),
+ }),
useRoute: () => reactive({}),
RouterLink: vi.fn(),
}));
@@ -23,6 +30,10 @@ let uiStore: MockedStore;
let sourceControlStore: MockedStore;
let versionsStore: MockedStore;
let usersStore: MockedStore;
+let templatesStore: MockedStore;
+let personalizedTemplatesV2Store: MockedStore;
+let personalizedTemplatesV3Store: MockedStore;
+let templatesDataQualityStore: MockedStore;
const mockVersion: Version = {
name: '1.2.0',
@@ -46,6 +57,10 @@ describe('MainSidebar', () => {
sourceControlStore = mockedStore(useSourceControlStore);
versionsStore = mockedStore(useVersionsStore);
usersStore = mockedStore(useUsersStore);
+ templatesStore = mockedStore(useTemplatesStore);
+ personalizedTemplatesV2Store = mockedStore(usePersonalizedTemplatesV2Store);
+ personalizedTemplatesV3Store = mockedStore(usePersonalizedTemplatesV3Store);
+ templatesDataQualityStore = mockedStore(useTemplatesDataQualityStore);
settingsStore.settings = defaultSettings;
@@ -54,6 +69,14 @@ describe('MainSidebar', () => {
versionsStore.nextVersions = [];
usersStore.canUserUpdateVersion = true;
uiStore.sidebarMenuCollapsed = false;
+ settingsStore.isTemplatesEnabled = true;
+ templatesStore.hasCustomTemplatesHost = false;
+ templatesStore.websiteTemplateRepositoryURL = 'https://n8n.io/workflows';
+
+ // Default experiment store values
+ personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false);
+ personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => false);
+ templatesDataQualityStore.isFeatureEnabled = vi.fn(() => false);
});
it('renders the sidebar without error', () => {
@@ -87,18 +110,16 @@ describe('MainSidebar', () => {
expect(queryByTestId('version-update-cta-button')).not.toBeInTheDocument();
});
- it('should render version update CTA disabled when canUserUpdateVersion is false', async () => {
+ it('should not render version update CTA when canUserUpdateVersion is false', async () => {
versionsStore.hasVersionUpdates = true;
versionsStore.nextVersions = [mockVersion];
usersStore.canUserUpdateVersion = false;
- const { findByTestId, getByText } = renderComponent();
+ const { queryByTestId, getByText } = renderComponent();
- getByText('What’s New').click();
+ getByText('Help').click();
- const updateButton = await findByTestId('version-update-cta-button');
- expect(updateButton).toBeInTheDocument();
- expect(updateButton).toBeDisabled();
+ expect(queryByTestId('version-update-cta-button')).not.toBeInTheDocument();
});
it('should render version update CTA enabled when canUserUpdateVersion is true and hasVersionUpdates is true', async () => {
@@ -108,11 +129,107 @@ describe('MainSidebar', () => {
const { getByText, findByTestId } = renderComponent();
- getByText('What’s New').click();
+ getByText('Help').click();
const updateButton = await findByTestId('version-update-cta-button');
expect(updateButton).toBeInTheDocument();
expect(updateButton).toBeEnabled();
});
});
+
+ describe('mainMenuItems', () => {
+ it('should show templates menu when templates are enabled and no experiment is active', () => {
+ settingsStore.isTemplatesEnabled = true;
+ templatesStore.hasCustomTemplatesHost = false;
+ personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false);
+ personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => false);
+ templatesDataQualityStore.isFeatureEnabled = vi.fn(() => false);
+
+ const { getAllByTestId } = renderComponent();
+
+ // Should have at least one templates item visible
+ const templatesItems = getAllByTestId('main-sidebar-templates');
+ expect(templatesItems.length).toBeGreaterThan(0);
+ });
+
+ it('should show templates menu when experiment is enabled', () => {
+ settingsStore.isTemplatesEnabled = true;
+ personalizedTemplatesV3Store.isFeatureEnabled = vi.fn(() => true);
+ personalizedTemplatesV2Store.isFeatureEnabled = vi.fn(() => false);
+ templatesDataQualityStore.isFeatureEnabled = vi.fn(() => false);
+
+ const { getAllByTestId } = renderComponent();
+
+ // Should have templates item visible when experiment is enabled
+ const templatesItems = getAllByTestId('main-sidebar-templates');
+ expect(templatesItems.length).toBeGreaterThan(0);
+ });
+
+ it('should not show templates menu when templates are disabled', () => {
+ settingsStore.isTemplatesEnabled = false;
+
+ const { queryAllByTestId } = renderComponent();
+
+ // Should have no templates items when templates are disabled
+ const templatesItems = queryAllByTestId('main-sidebar-templates');
+ expect(templatesItems).toHaveLength(0);
+ });
+
+ it('should show help menu item', () => {
+ const { getByTestId } = renderComponent();
+
+ expect(getByTestId('main-sidebar-help')).toBeInTheDocument();
+ });
+
+ it('should show settings menu item', () => {
+ const { getByTestId } = renderComponent();
+
+ expect(getByTestId('main-sidebar-settings')).toBeInTheDocument();
+ });
+ });
+
+ describe('handleSelect', () => {
+ beforeEach(() => {
+ uiStore.openModal = vi.fn();
+ uiStore.openModalWithData = vi.fn();
+ personalizedTemplatesV3Store.markTemplateRecommendationInteraction = vi.fn();
+ });
+
+ it('should open about modal when about is selected', async () => {
+ const { getByText, findByText } = renderComponent();
+
+ getByText('Help').click();
+ const aboutItem = await findByText('About n8n');
+ aboutItem.click();
+
+ expect(uiStore.openModal).toHaveBeenCalledWith(ABOUT_MODAL_KEY);
+ });
+
+ it('should open whats new modal when whats new article is selected', async () => {
+ versionsStore.hasVersionUpdates = true;
+ versionsStore.whatsNewArticles = [
+ {
+ id: 123,
+ title: 'Test Article',
+ content: 'Test content',
+ createdAt: '2025-01-01T00:00:00Z',
+ updatedAt: null,
+ publishedAt: '2025-01-01T00:00:00Z',
+ },
+ ];
+
+ const { getByText, findByText } = renderComponent();
+
+ getByText('Help').click();
+ const articleItem = await findByText('Test Article');
+ articleItem.click();
+
+ expect(uiStore.openModalWithData).toHaveBeenCalledWith({
+ name: WHATS_NEW_MODAL_KEY,
+ data: {
+ articleId: 123,
+ },
+ });
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
index acfa1a7ab19..2af3abe5b5f 100644
--- a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
@@ -1,5 +1,5 @@
-