From 956b11a560528336a74be40f722fa05bf3cca94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Fri, 6 Dec 2024 08:52:07 +0100 Subject: [PATCH] fix(editor): Universal button snags (#11974) Co-authored-by: Csaba Tuncsik --- .../NavigationDropdown.test.ts | 69 +++++++++++++++---- .../NavigationDropdown.vue | 44 +++++++++--- .../design-system/src/css/_tokens.dark.scss | 4 ++ packages/design-system/src/css/_tokens.scss | 5 ++ .../editor-ui/src/components/MainSidebar.vue | 55 ++++++++++----- .../components/Projects/ProjectHeader.test.ts | 25 ++++++- .../src/components/Projects/ProjectHeader.vue | 46 +++++++++---- .../useGlobalEntityCreation.test.ts | 25 +++++++ .../composables/useGlobalEntityCreation.ts | 23 ++++++- .../src/plugins/i18n/locales/en.json | 1 + .../src/views/CredentialsView.test.ts | 4 +- 11 files changed, 245 insertions(+), 56 deletions(-) diff --git a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts index a7d9936e08..fe57291225 100644 --- a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts +++ b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts @@ -33,7 +33,7 @@ describe('N8nNavigationDropdown', () => { it('default slot should trigger first level', async () => { const { getByTestId, queryByTestId } = render(NavigationDropdown, { slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, - props: { menu: [{ id: 'aaa', title: 'aaa', route: { name: 'projects' } }] }, + props: { menu: [{ id: 'first', title: 'first', route: { name: 'projects' } }] }, global: { plugins: [router], }, @@ -51,9 +51,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' } }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' } }], }, ], }, @@ -80,9 +80,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' }, icon: 'user' }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' }, icon: 'user' }], }, ], }, @@ -100,9 +100,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' }, icon: 'user' }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' }, icon: 'user' }], }, ], }, @@ -114,8 +114,53 @@ describe('N8nNavigationDropdown', () => { await userEvent.click(getByTestId('navigation-submenu-item')); expect(emitted('itemClick')).toStrictEqual([ - [{ active: true, index: 'bbb', indexPath: ['-1', 'aaa', 'bbb'] }], + [{ active: true, index: 'nested', indexPath: ['-1', 'first', 'nested'] }], ]); - expect(emitted('select')).toStrictEqual([['bbb']]); + expect(emitted('select')).toStrictEqual([['nested']]); + }); + + it('should open first level on click', async () => { + const { getByTestId, getByText } = render(NavigationDropdown, { + slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, + props: { + menu: [ + { + id: 'first', + title: 'first', + }, + ], + }, + }); + expect(getByText('first')).not.toBeVisible(); + await userEvent.click(getByTestId('test-trigger')); + expect(getByText('first')).toBeVisible(); + }); + + it('should toggle nested level on mouseenter / mouseleave', async () => { + const { getByTestId, getByText } = render(NavigationDropdown, { + slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, + props: { + menu: [ + { + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested' }], + }, + ], + }, + }); + expect(getByText('first')).not.toBeVisible(); + await userEvent.click(getByTestId('test-trigger')); + expect(getByText('first')).toBeVisible(); + + expect(getByText('nested')).not.toBeVisible(); + await userEvent.hover(getByTestId('navigation-submenu')); + await waitFor(() => expect(getByText('nested')).toBeVisible()); + + await userEvent.pointer([ + { target: getByTestId('navigation-submenu') }, + { target: getByTestId('test-trigger') }, + ]); + await waitFor(() => expect(getByText('nested')).not.toBeVisible()); }); }); diff --git a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue index 1329d9a9ed..ce728a44ba 100644 --- a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue +++ b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue @@ -29,7 +29,7 @@ defineProps<{ }>(); const menuRef = ref(null); -const menuIndex = ref('-1'); +const ROOT_MENU_INDEX = '-1'; const emit = defineEmits<{ itemClick: [item: MenuItemRegistered]; @@ -37,7 +37,18 @@ const emit = defineEmits<{ }>(); const close = () => { - menuRef.value?.close(menuIndex.value); + menuRef.value?.close(ROOT_MENU_INDEX); +}; + +const menuTrigger = ref<'click' | 'hover'>('click'); +const onOpen = (index: string) => { + if (index !== ROOT_MENU_INDEX) return; + menuTrigger.value = 'hover'; +}; + +const onClose = (index: string) => { + if (index !== ROOT_MENU_INDEX) return; + menuTrigger.value = 'click'; }; defineExpose({ @@ -50,14 +61,16 @@ defineExpose({ ref="menuRef" mode="horizontal" unique-opened - menu-trigger="click" + :menu-trigger="menuTrigger" :ellipsis="false" :class="$style.dropdown" @select="emit('select', $event)" @keyup.escape="close" + @open="onOpen" + @close="onClose" > - + {{ item.title }} + @@ -125,17 +145,25 @@ defineExpose({ } } +.nestedSubmenu { + :global(.el-menu) { + max-height: 450px; + overflow: auto; + } +} + .submenu { padding: 5px 0 !important; :global(.el-menu--horizontal .el-menu .el-menu-item), :global(.el-menu--horizontal .el-menu .el-sub-menu__title) { color: var(--color-text-dark); + background-color: var(--color-menu-background); } :global(.el-menu--horizontal .el-menu .el-menu-item:not(.is-disabled):hover), :global(.el-menu--horizontal .el-menu .el-sub-menu__title:not(.is-disabled):hover) { - background-color: var(--color-foreground-base); + background-color: var(--color-menu-hover-background); } :global(.el-popper) { diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 72963efcf5..a3fc653550 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -462,6 +462,10 @@ --color-configurable-node-name: var(--color-text-dark); --color-secondary-link: var(--prim-color-secondary-tint-200); --color-secondary-link-hover: var(--prim-color-secondary-tint-100); + + --color-menu-background: var(--prim-gray-740); + --color-menu-hover-background: var(--prim-gray-670); + --color-menu-active-background: var(--prim-gray-670); } body[data-theme='dark'] { diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 56d5142c87..87951534ee 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -533,6 +533,11 @@ --color-secondary-link: var(--color-secondary); --color-secondary-link-hover: var(--color-secondary-shade-1); + // Menu + --color-menu-background: var(--prim-gray-0); + --color-menu-hover-background: var(--prim-gray-120); + --color-menu-active-background: var(--prim-gray-120); + // Generated Color Shades from 50 to 950 // Not yet used in design system @each $color in ('neutral', 'success', 'warning', 'danger') { diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 0ed9eaab58..9b294a85f1 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -291,7 +291,12 @@ const checkWidthAndAdjustSidebar = async (width: number) => { } }; -const { menu, handleSelect: handleMenuSelect } = useGlobalEntityCreation(); +const { + menu, + handleSelect: handleMenuSelect, + createProjectAppendSlotName, + projectsLimitReachedMessage, +} = useGlobalEntityCreation(); onClickOutside(createBtn as Ref, () => { createBtn.value?.close(); }); @@ -311,8 +316,8 @@ onClickOutside(createBtn as Ref, () => { :class="['clickable', $style.sideMenuCollapseButton]" @click="toggleCollapse" > - - + +
n8n @@ -323,9 +328,21 @@ onClickOutside(createBtn as Ref, () => { @select="handleMenuSelect" > +
- + - + diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts index 4bc8e6d43a..21a4c8c52f 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts @@ -3,11 +3,13 @@ import { within } from '@testing-library/dom'; import { createComponentRenderer } from '@/__tests__/render'; import { mockedStore } from '@/__tests__/utils'; import { createTestProject } from '@/__tests__/data/projects'; -import { useRoute } from 'vue-router'; +import * as router from 'vue-router'; +import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue'; import { useProjectsStore } from '@/stores/projects.store'; import type { Project } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types'; +import { VIEWS } from '@/constants'; vi.mock('vue-router', async () => { const actual = await vi.importActual('vue-router'); @@ -35,13 +37,13 @@ const renderComponent = createComponentRenderer(ProjectHeader, { }, }); -let route: ReturnType; +let route: ReturnType; let projectsStore: ReturnType>; describe('ProjectHeader', () => { beforeEach(() => { createTestingPinia(); - route = useRoute(); + route = router.useRoute(); projectsStore = mockedStore(useProjectsStore); projectsStore.teamProjectsLimit = -1; @@ -159,4 +161,21 @@ describe('ProjectHeader', () => { expect(within(getByTestId('resource-add')).getByRole('button', { name: label })).toBeVisible(); }); + + it('should not render creation button in setting page', async () => { + projectsStore.currentProject = createTestProject({ type: ProjectTypes.Personal }); + vi.spyOn(router, 'useRoute').mockReturnValueOnce({ + name: VIEWS.PROJECT_SETTINGS, + } as RouteLocationNormalizedLoadedGeneric); + const { queryByTestId } = renderComponent({ + global: { + stubs: { + N8nNavigationDropdown: { + template: '
', + }, + }, + }, + }); + expect(queryByTestId('resource-add')).not.toBeInTheDocument(); + }); }); diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue index ba04291b92..977abe7393 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue @@ -1,7 +1,7 @@