From 9bebbab3db77da2f047b386e345ce12da14d6e35 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Wed, 17 Dec 2025 17:16:39 +0200 Subject: [PATCH] feat(editor): Update Chat sidebar to match v2 styles (#23267) --- .../src/components/N8nIcon/icons.ts | 4 + .../N8nMenuItem/MenuItem.stories.ts | 10 + .../src/components/N8nMenuItem/MenuItem.vue | 24 +- .../@n8n/design-system/src/types/menu.ts | 1 + .../frontend/@n8n/i18n/src/locales/en.json | 3 +- .../src/app/components/BottomMenu.vue | 231 ++++++++++ .../src/app/components/MainSidebar.test.ts | 20 - .../src/app/components/MainSidebar.vue | 435 ++---------------- .../app/components/MainSidebarHeader.test.ts | 143 ++++++ .../src/app/components/MainSidebarHeader.vue | 256 +++++++++++ .../editor-ui/src/app/constants/modals.ts | 1 - .../ai/chatHub/ChatPersonalAgentsView.vue | 13 - .../src/features/ai/chatHub/ChatView.vue | 1 - .../ai/chatHub/ChatWorkflowAgentsView.vue | 2 - .../src/features/ai/chatHub/chat.api.ts | 2 +- .../src/features/ai/chatHub/chat.store.ts | 4 +- .../chatHub/components/AgentEditorModal.vue | 6 +- .../ai/chatHub/components/ChatAgentAvatar.vue | 52 ++- .../components/ChatConversationHeader.vue | 29 +- .../components/ChatSessionMenuItem.vue | 9 +- .../ai/chatHub/components/ChatSidebar.vue | 146 +++--- .../chatHub/components/ChatSidebarContent.vue | 190 ++++---- .../ai/chatHub/components/ChatSidebarLink.vue | 44 +- .../chatHub/components/ChatSidebarOpener.vue | 20 - .../composables/useChatHubSidebarState.ts | 50 -- .../src/features/ai/chatHub/constants.ts | 1 + .../projects/components/ProjectNavigation.vue | 1 + .../composables/useChatHubCommands.ts | 6 +- 28 files changed, 975 insertions(+), 729 deletions(-) create mode 100644 packages/frontend/editor-ui/src/app/components/BottomMenu.vue create mode 100644 packages/frontend/editor-ui/src/app/components/MainSidebarHeader.test.ts create mode 100644 packages/frontend/editor-ui/src/app/components/MainSidebarHeader.vue delete mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarOpener.vue delete mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatHubSidebarState.ts diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts index d3feaab958c..8f3b527dd81 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts @@ -72,6 +72,7 @@ import IconLucideCircle from '~icons/lucide/circle'; import IconLucideCircleAlert from '~icons/lucide/circle-alert'; import IconLucideCircleCheck from '~icons/lucide/circle-check'; import IconLucideCircleDot from '~icons/lucide/circle-dot'; +import IconLucideCircleEllipsis from '~icons/lucide/circle-ellipsis'; import IconLucideCircleHelp from '~icons/lucide/circle-help'; import IconLucideCircleMinus from '~icons/lucide/circle-minus'; import IconLucideCirclePause from '~icons/lucide/circle-pause'; @@ -151,6 +152,7 @@ import IconLucideMaximize2 from '~icons/lucide/maximize-2'; import IconLucideMenu from '~icons/lucide/menu'; import IconLucideMessageCircle from '~icons/lucide/message-circle'; import IconLucideMessageSquare from '~icons/lucide/message-square'; +import IconLucideMessageSquarePlus from '~icons/lucide/message-square-plus'; import IconLucideMessagesSquare from '~icons/lucide/messages-square'; import IconLucideMic from '~icons/lucide/mic'; import IconLucideMilestone from '~icons/lucide/milestone'; @@ -518,6 +520,7 @@ export const updatedIconSet = { 'circle-alert': IconLucideCircleAlert, 'circle-check': IconLucideCircleCheck, 'circle-dot': IconLucideCircleDot, + 'circle-ellipsis': IconLucideCircleEllipsis, 'circle-help': IconLucideCircleHelp, 'circle-minus': IconLucideCircleMinus, 'circle-pause': IconLucideCirclePause, @@ -597,6 +600,7 @@ export const updatedIconSet = { menu: IconLucideMenu, 'message-circle': IconLucideMessageCircle, 'message-square': IconLucideMessageSquare, + 'message-square-plus': IconLucideMessageSquarePlus, 'messages-square': IconLucideMessagesSquare, mic: IconLucideMic, milestone: IconLucideMilestone, diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts index 6bf53173801..d8007fae82c 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts @@ -60,6 +60,16 @@ withSecondaryIconTooltip.args = { }, }; +export const withBetaTag = template.bind({}); +withBetaTag.args = { + item: { + id: 'workflows', + icon: 'home', + label: 'Workflows', + beta: true, + }, +}; + export const compact = template.bind({}); compact.args = { item: { 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 cde4b1f019f..f1a2658ca1f 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -3,6 +3,7 @@ import { computed } from 'vue'; import type { IMenuItem } from '@n8n/design-system/types'; +import BetaTag from '../BetaTag/BetaTag.vue'; import N8nIcon from '../N8nIcon'; import type { IconName } from '../N8nIcon/icons'; import N8nRoute from '../N8nRoute'; @@ -66,7 +67,7 @@ const iconColor = computed(() => { :id="item.id" :to="to" role="menuitem" - :class="[$style.menuItem, { [$style.active]: active }]" + :class="[$style.menuItem, { [$style.active]: active, [$style.compact]: compact }]" :aria-label="props.ariaLabel" data-test-id="menu-item" @click="emit('click')" @@ -83,7 +84,12 @@ const iconColor = computed(() => { > - {{ item.label }} +
+ + {{ item.label }} + + +
@@ -125,6 +131,10 @@ const iconColor = computed(() => { background-color: var(--color--background--light-1); color: var(--color--text--shade-1); } + + &.compact { + gap: 0; + } } .menuItem:focus-visible { @@ -136,7 +146,6 @@ const iconColor = computed(() => { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - flex: 1; line-height: var(--font-size--lg); min-width: 0; } @@ -176,4 +185,13 @@ const iconColor = computed(() => { color: var(--color--text--shade-1); } } + +.menuItemLabel { + display: flex; + align-items: center; + flex-direction: row; + gap: var(--spacing--3xs); + flex: 1; + min-width: 0; +} diff --git a/packages/frontend/@n8n/design-system/src/types/menu.ts b/packages/frontend/@n8n/design-system/src/types/menu.ts index d1bc0681616..8975951b670 100644 --- a/packages/frontend/@n8n/design-system/src/types/menu.ts +++ b/packages/frontend/@n8n/design-system/src/types/menu.ts @@ -36,6 +36,7 @@ export type IMenuItem = { disabled?: boolean; notification?: boolean; size?: 'medium' | 'small'; + beta?: boolean; }; export interface ICustomMenuItem { diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 7c0aff192a2..7432d67bab8 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -456,6 +456,7 @@ "chatHub.sidebar.link.newChat": "New chat", "chatHub.sidebar.link.workflowAgents": "Workflow agents", "chatHub.sidebar.link.personalAgents": "Personal agents", + "chatHub.sidebar.loadMoreSessions": "Load more", "chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.", "chatEmbed.infoTip.link": "More info", "chatEmbed.title": "Embed Chat in your website", @@ -3737,7 +3738,7 @@ "projects.menu.title": "Projects", "projects.menu.personal": "Personal", "projects.menu.addFirstProject": "Add project", - "projects.menu.chat": "Chat (Beta)", + "projects.menu.chat": "Chat", "projects.settings": "Project settings", "projects.settings.info": "Project info", "projects.settings.newProjectName": "My project", diff --git a/packages/frontend/editor-ui/src/app/components/BottomMenu.vue b/packages/frontend/editor-ui/src/app/components/BottomMenu.vue new file mode 100644 index 00000000000..1f492ba384e --- /dev/null +++ b/packages/frontend/editor-ui/src/app/components/BottomMenu.vue @@ -0,0 +1,231 @@ + + + + + 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 32a034e0383..0d877aafe2f 100644 --- a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts +++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts @@ -6,7 +6,6 @@ import { defaultSettings } from '@/__tests__/defaults'; import MainSidebar from '@/app/components/MainSidebar.vue'; import { useSettingsStore } from '@/app/stores/settings.store'; 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'; @@ -27,7 +26,6 @@ vi.mock('vue-router', () => ({ let renderComponent: ReturnType; let settingsStore: MockedStore; let uiStore: MockedStore; -let sourceControlStore: MockedStore; let versionsStore: MockedStore; let usersStore: MockedStore; let templatesStore: MockedStore; @@ -54,7 +52,6 @@ describe('MainSidebar', () => { }); settingsStore = mockedStore(useSettingsStore); uiStore = mockedStore(useUIStore); - sourceControlStore = mockedStore(useSourceControlStore); versionsStore = mockedStore(useVersionsStore); usersStore = mockedStore(useUsersStore); templatesStore = mockedStore(useTemplatesStore); @@ -83,23 +80,6 @@ describe('MainSidebar', () => { expect(() => renderComponent()).not.toThrow(); }); - test.each([ - [false, true, true], - [true, false, false], - [true, true, false], - [false, false, false], - ])( - 'should render readonly tooltip when is opened %s and the environment is readonly %s', - (sidebarMenuCollapsed, branchReadOnly, shouldRender) => { - uiStore.sidebarMenuCollapsed = sidebarMenuCollapsed; - sourceControlStore.preferences.branchReadOnly = branchReadOnly; - - const { queryByTestId } = renderComponent(); - - expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender); - }, - ); - describe('Version Update CTA', () => { it('should not render version update CTA when hasVersionUpdates is false', () => { versionsStore.hasVersionUpdates = false; diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue index 5e0d11bc79b..f933378dfe3 100644 --- a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue +++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue @@ -1,67 +1,43 @@ - @@ -152,17 +150,6 @@ watch( position: relative; } -.menuButton { - position: absolute; - top: 0; - left: 0; - margin: var(--spacing--sm); - - .isMobileDevice & { - margin: var(--spacing--2xs); - } -} - .header { display: flex; justify-content: space-between; diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue index 0da39b9ff2f..18fca5d8137 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -604,7 +604,6 @@ function onFilesDropped(files: File[]) { :selected-model="selectedModel" :credentials="credentialsByProvider" :ready-to-show-model-selector="isNewSession || !!currentConversation" - :is-new-session="isNewSession" @select-model="handleSelectModel" @edit-custom-agent="handleEditAgent" @create-custom-agent="openNewAgentCreator" diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue index 429a54bd7dd..30887571acd 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue @@ -14,7 +14,6 @@ import { useMediaQuery } from '@vueuse/core'; import { MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants'; import { useRouter } from 'vue-router'; import ChatLayout from '@/features/ai/chatHub/components/ChatLayout.vue'; -import ChatSidebarOpener from '@/features/ai/chatHub/components/ChatSidebarOpener.vue'; import SkeletonAgentCard from '@/features/ai/chatHub/components/SkeletonAgentCard.vue'; import { useI18n } from '@n8n/i18n'; @@ -96,7 +95,6 @@ watch( /> - diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts index b61a737237d..05075c0fab3 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts @@ -16,8 +16,8 @@ import type { ChatHubUpdateConversationRequest, EnrichedStructuredChunk, ChatHubLLMProvider, + ChatProviderSettingsDto, } from '@n8n/api-types'; -import type { ChatProviderSettingsDto } from '@n8n/api-types'; // Workflows stream data as newline separated JSON objects (jsonl) const STREAM_SEPARATOR = '\n'; diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts index 44fe5700dc3..fccf66fc08a 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { CHAT_STORE } from './constants'; +import { CHAT_STORE, CHAT_SESSIONS_PAGE_SIZE } from './constants'; import { computed, ref } from 'vue'; import { v4 as uuidv4 } from 'uuid'; import { useI18n } from '@n8n/i18n'; @@ -322,7 +322,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { try { const cursor = reset ? undefined : (sessions.value?.nextCursor ?? undefined); const [response] = await Promise.all([ - fetchSessionsApi(rootStore.restApiContext, 40, cursor), + fetchSessionsApi(rootStore.restApiContext, CHAT_SESSIONS_PAGE_SIZE, cursor), new Promise((resolve) => setTimeout(resolve, options.minLoadingTime ?? 0)), ]); diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue index d325940b803..711585a63a2 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue @@ -147,11 +147,11 @@ watch( watch( [isOpened, isLoadingAgent, nameInputRef], - async ([opened, isLoading, name]) => { + ([opened, isLoading, nameInput]) => { if (opened && !isLoading) { // autofocus attribute doesn't work in modal // https://github.com/element-plus/element-plus/issues/15250 - name?.focus(); + nameInput?.focus(); } }, { immediate: true, flush: 'post' }, @@ -308,8 +308,8 @@ function onSelectTools() { :button-tooltip="i18n.baseText('chatHub.agent.editor.iconPicker.button.tooltip')" /> {{ agent?.name || i18n.baseText('chatHub.agent.unavailableAgent') }} - - {{ agent.icon.value }} - - - +
+ + {{ agent.icon.value }} + + + +
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue index a1efeae331c..69e62af3084 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue @@ -1,40 +1,32 @@ @@ -227,11 +241,6 @@ onMounted(() => { display: flex; flex-direction: column; align-items: stretch; - padding-block: var(--spacing--4xs); - - &.isMobileDevice { - padding-block: 0; - } } .header { @@ -253,36 +262,35 @@ onMounted(() => { .links { display: flex; flex-direction: column; - padding: 0 var(--spacing--xs) var(--spacing--sm) var(--spacing--xs); + padding: 0 var(--spacing--3xs) var(--spacing--2xs); gap: 1px; + + &.collapsed { + border-bottom: var(--border); + } } .historySections { display: flex; flex-direction: column; - padding: 0 var(--spacing--xs) var(--spacing--sm) var(--spacing--xs); - gap: var(--spacing--xs); + padding: var(--spacing--2xs) var(--spacing--3xs) var(--spacing--2xs); + gap: var(--spacing--sm); - .isMobileDevice & { - gap: var(--spacing--sm); + &.collapsed { + gap: 0; } } .group { display: flex; flex-direction: column; - gap: 1px; + gap: 2px; } .groupHeader { padding: 0 var(--spacing--4xs) var(--spacing--3xs) var(--spacing--4xs); } -.loadMoreTrigger { - height: 1px; - width: 100%; -} - .loading, .empty { padding: var(--spacing--xs); diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue index 9310587baaa..b1266ad191c 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue @@ -8,14 +8,18 @@ const { active = false, to, label, + title, menuItems = [], icon, + compact, } = defineProps<{ active?: boolean; to: RouteLocationRaw; label: string; + title: string; menuItems?: Array>; icon?: IconName; + compact?: boolean; }>(); const emit = defineEmits<{ @@ -35,17 +39,20 @@ defineSlots<{