Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Universal button snags #11974

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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],
},
Expand All @@ -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' } }],
},
],
},
Expand All @@ -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' }],
},
],
},
Expand All @@ -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' }],
},
],
},
Expand All @@ -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());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,26 @@ defineProps<{
}>();

const menuRef = ref<typeof ElMenu | null>(null);
const menuIndex = ref('-1');
const ROOT_MENU_INDEX = '-1';

const emit = defineEmits<{
itemClick: [item: MenuItemRegistered];
select: [id: Item['id']];
}>();

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({
Expand All @@ -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"
>
<ElSubMenu
:index="menuIndex"
:index="ROOT_MENU_INDEX"
:class="$style.trigger"
:popper-offset="-10"
:popper-class="$style.submenu"
Expand All @@ -70,10 +83,15 @@ defineExpose({

<template v-for="item in menu" :key="item.id">
<template v-if="item.submenu">
<ElSubMenu :index="item.id" :popper-offset="-10" data-test-id="navigation-submenu">
<ElSubMenu
:popper-class="$style.nestedSubmenu"
:index="item.id"
:popper-offset="-10"
data-test-id="navigation-submenu"
>
<template #title>{{ item.title }}</template>
<template v-for="subitem in item.submenu" :key="subitem.id">
<ConditionalRouterLink :to="!subitem.disabled && subitem.route">
<ConditionalRouterLink :to="(!subitem.disabled && subitem.route) || undefined">
<ElMenuItem
data-test-id="navigation-submenu-item"
:index="subitem.id"
Expand All @@ -82,18 +100,20 @@ defineExpose({
>
<N8nIcon v-if="subitem.icon" :icon="subitem.icon" :class="$style.submenu__icon" />
{{ subitem.title }}
<slot :name="`item.append.${item.id}`" v-bind="{ item }" />
</ElMenuItem>
</ConditionalRouterLink>
</template>
</ElSubMenu>
</template>
<ConditionalRouterLink v-else :to="!item.disabled && item.route">
<ConditionalRouterLink v-else :to="(!item.disabled && item.route) || undefined">
<ElMenuItem
:index="item.id"
:disabled="item.disabled"
data-test-id="navigation-menu-item"
>
{{ item.title }}
<slot :name="`item.append.${item.id}`" v-bind="{ item }" />
</ElMenuItem>
</ConditionalRouterLink>
</template>
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions packages/design-system/src/css/_tokens.dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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'] {
Expand Down
5 changes: 5 additions & 0 deletions packages/design-system/src/css/_tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
55 changes: 36 additions & 19 deletions packages/editor-ui/src/components/MainSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<VueInstance>, () => {
createBtn.value?.close();
});
Expand All @@ -311,8 +316,8 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
:class="['clickable', $style.sideMenuCollapseButton]"
@click="toggleCollapse"
>
<n8n-icon v-if="isCollapsed" icon="chevron-right" size="xsmall" class="ml-5xs" />
<n8n-icon v-else icon="chevron-left" size="xsmall" class="mr-5xs" />
<N8nIcon v-if="isCollapsed" icon="chevron-right" size="xsmall" class="ml-5xs" />
<N8nIcon v-else icon="chevron-left" size="xsmall" class="mr-5xs" />
</div>
<div :class="$style.logo">
<img :src="logoPath" data-test-id="n8n-logo" :class="$style.icon" alt="n8n" />
Expand All @@ -323,9 +328,21 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
@select="handleMenuSelect"
>
<N8nIconButton icon="plus" type="secondary" outline />
<template #[createProjectAppendSlotName]="{ item }">
<N8nTooltip v-if="item.disabled" placement="right" :content="projectsLimitReachedMessage">
<N8nButton
:size="'mini'"
style="margin-left: auto"
type="tertiary"
@click="handleMenuSelect(item.id)"
>
{{ i18n.baseText('generic.upgrade') }}
</N8nButton>
</N8nTooltip>
</template>
</N8nNavigationDropdown>
</div>
<n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
<N8nMenu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
<template #header>
<ProjectNavigation
:collapsed="isCollapsed"
Expand All @@ -347,14 +364,14 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
<div :class="$style.giftContainer">
<GiftNotificationIcon />
</div>
<n8n-text
<N8nText
:class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }"
color="text-base"
>
{{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{
nextVersions.length > 1 ? 's' : ''
}}
</n8n-text>
</N8nText>
</div>
<MainSidebarSourceControl :is-collapsed="isCollapsed" />
</div>
Expand All @@ -363,35 +380,35 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
<div ref="user" :class="$style.userArea">
<div class="ml-3xs" data-test-id="main-sidebar-user-menu">
<!-- This dropdown is only enabled when sidebar is collapsed -->
<el-dropdown placement="right-end" trigger="click" @command="onUserActionToggle">
<ElDropdown placement="right-end" trigger="click" @command="onUserActionToggle">
<div :class="{ [$style.avatar]: true, ['clickable']: isCollapsed }">
<n8n-avatar
<N8nAvatar
:first-name="usersStore.currentUser?.firstName"
:last-name="usersStore.currentUser?.lastName"
size="small"
/>
</div>
<template v-if="isCollapsed" #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="settings">
<ElDropdownMenu>
<ElDropdownItem command="settings">
{{ i18n.baseText('settings') }}
</el-dropdown-item>
<el-dropdown-item command="logout">
</ElDropdownItem>
<ElDropdownItem command="logout">
{{ i18n.baseText('auth.signout') }}
</el-dropdown-item>
</el-dropdown-menu>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</el-dropdown>
</ElDropdown>
</div>
<div
:class="{ ['ml-2xs']: true, [$style.userName]: true, [$style.expanded]: fullyExpanded }"
>
<n8n-text size="small" :bold="true" color="text-dark">{{
<N8nText size="small" :bold="true" color="text-dark">{{
usersStore.currentUser?.fullName
}}</n8n-text>
}}</N8nText>
</div>
<div :class="{ [$style.userActions]: true, [$style.expanded]: fullyExpanded }">
<n8n-action-dropdown
<N8nActionDropdown
:items="userMenuItems"
placement="top-start"
data-test-id="user-menu"
Expand All @@ -400,7 +417,7 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
</div>
</div>
</template>
</n8n-menu>
</N8nMenu>
</div>
</template>

Expand Down
Loading
Loading