mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Universal button snags (#11974)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Csaba Tuncsik <csaba.tuncsik@gmail.com>
This commit is contained in:
parent
b1f8663265
commit
956b11a560
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ defineProps<{
|
|||
}>();
|
||||
|
||||
const menuRef = ref<typeof ElMenu | null>(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"
|
||||
>
|
||||
<ElSubMenu
|
||||
:index="menuIndex"
|
||||
:index="ROOT_MENU_INDEX"
|
||||
:class="$style.trigger"
|
||||
:popper-offset="-10"
|
||||
:popper-class="$style.submenu"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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'] {
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -400,7 +417,7 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-menu>
|
||||
</N8nMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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<typeof useRoute>;
|
||||
let route: ReturnType<typeof router.useRoute>;
|
||||
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
|
||||
|
||||
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: '<div><slot></slot></div>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(queryByTestId('resource-add')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, type Ref, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { N8nNavigationDropdown } from 'n8n-design-system';
|
||||
import { N8nNavigationDropdown, N8nButton, N8nIconButton, N8nTooltip } from 'n8n-design-system';
|
||||
import { onClickOutside, type VueInstance } from '@vueuse/core';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
|
@ -9,6 +9,7 @@ import { useProjectsStore } from '@/stores/projects.store';
|
|||
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
|
@ -47,9 +48,8 @@ const showSettings = computed(
|
|||
projectsStore.currentProject?.type === ProjectTypes.Team,
|
||||
);
|
||||
|
||||
const { menu, handleSelect } = useGlobalEntityCreation(
|
||||
computed(() => !Boolean(projectsStore.currentProject)),
|
||||
);
|
||||
const { menu, handleSelect, createProjectAppendSlotName, projectsLimitReachedMessage } =
|
||||
useGlobalEntityCreation(computed(() => !Boolean(projectsStore.currentProject)));
|
||||
|
||||
const createLabel = computed(() => {
|
||||
if (!projectsStore.currentProject) {
|
||||
|
@ -82,17 +82,35 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
|||
</slot>
|
||||
</N8nText>
|
||||
</div>
|
||||
<div v-if="route.name !== VIEWS.PROJECT_SETTINGS" :class="[$style.headerActions]">
|
||||
<N8nNavigationDropdown
|
||||
ref="createBtn"
|
||||
data-test-id="resource-add"
|
||||
:menu="menu"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<N8nIconButton :label="createLabel" icon="plus" style="width: auto" />
|
||||
<template #[createProjectAppendSlotName]="{ item }">
|
||||
<N8nTooltip
|
||||
v-if="item.disabled"
|
||||
placement="right"
|
||||
:content="projectsLimitReachedMessage"
|
||||
>
|
||||
<N8nButton
|
||||
:size="'mini'"
|
||||
style="margin-left: auto"
|
||||
type="tertiary"
|
||||
@click="handleSelect(item.id)"
|
||||
>
|
||||
{{ i18n.baseText('generic.upgrade') }}
|
||||
</N8nButton>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
</N8nNavigationDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<ProjectTabs :show-settings="showSettings" />
|
||||
<N8nNavigationDropdown
|
||||
ref="createBtn"
|
||||
data-test-id="resource-add"
|
||||
:menu="menu"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<N8nIconButton :label="createLabel" icon="plus" style="width: auto" />
|
||||
</N8nNavigationDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -106,6 +124,10 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
|||
min-height: 64px;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
border: 1px solid var(--color-foreground-light);
|
||||
padding: 6px;
|
||||
|
|
|
@ -7,6 +7,9 @@ import type router from 'vue-router';
|
|||
import { flushPromises } from '@vue/test-utils';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||
import type { CloudPlanState } from '@/Interface';
|
||||
|
||||
import { VIEWS } from '@/constants';
|
||||
import type { Project, ProjectListItem } from '@/types/projects.types';
|
||||
|
@ -153,6 +156,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
describe('global', () => {
|
||||
it('should show personal + all team projects', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
projectsStore.teamProjectsLimit = -1;
|
||||
|
||||
const personalProjectId = 'personal-project';
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
|
@ -222,4 +226,25 @@ describe('useGlobalEntityCreation', () => {
|
|||
expect(redirect.goToUpgrade).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show plan and limit according to deployment type', () => {
|
||||
const settingsStore = mockedStore(useSettingsStore);
|
||||
|
||||
const cloudPlanStore = mockedStore(useCloudPlanStore);
|
||||
cloudPlanStore.currentPlanData = { displayName: 'Pro' } as CloudPlanState['data'];
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
projectsStore.teamProjectsLimit = 10;
|
||||
|
||||
settingsStore.isCloudDeployment = true;
|
||||
const { projectsLimitReachedMessage } = useGlobalEntityCreation(true);
|
||||
expect(projectsLimitReachedMessage.value).toContain(
|
||||
'You have reached the Pro plan limit of 10.',
|
||||
);
|
||||
|
||||
settingsStore.isCloudDeployment = false;
|
||||
expect(projectsLimitReachedMessage.value).toContain(
|
||||
'Upgrade to unlock projects for more granular control over sharing, access and organisation of workflows',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@ import { useI18n } from '@/composables/useI18n';
|
|||
import { sortByProperty } from '@/utils/sortUtils';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
|
@ -28,6 +30,8 @@ export const useGlobalEntityCreation = (
|
|||
) => {
|
||||
const CREATE_PROJECT_ID = 'create-project';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const cloudPlanStore = useCloudPlanStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const router = useRouter();
|
||||
|
@ -172,6 +176,7 @@ export const useGlobalEntityCreation = (
|
|||
{
|
||||
id: CREATE_PROJECT_ID,
|
||||
title: 'Project',
|
||||
disabled: !projectsStore.canCreateProjects,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@ -204,5 +209,21 @@ export const useGlobalEntityCreation = (
|
|||
void usePageRedirectionHelper().goToUpgrade('rbac', 'upgrade-rbac');
|
||||
};
|
||||
|
||||
return { menu, handleSelect };
|
||||
const projectsLimitReachedMessage = computed(() => {
|
||||
if (settingsStore.isCloudDeployment) {
|
||||
return i18n.baseText('projects.create.limitReached', {
|
||||
adjustToNumber: projectsStore.teamProjectsLimit,
|
||||
interpolate: {
|
||||
planName: cloudPlanStore.currentPlanData?.displayName ?? '',
|
||||
limit: projectsStore.teamProjectsLimit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return i18n.baseText('projects.create.limitReached.self');
|
||||
});
|
||||
|
||||
const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`);
|
||||
|
||||
return { menu, handleSelect, createProjectAppendSlotName, projectsLimitReachedMessage };
|
||||
};
|
||||
|
|
|
@ -2563,6 +2563,7 @@
|
|||
"projects.error.title": "Project error",
|
||||
"projects.create.limit": "{num} project | {num} projects",
|
||||
"projects.create.limitReached": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}",
|
||||
"projects.create.limitReached.self": "Upgrade to unlock projects for more granular control over sharing, access and organisation of workflows",
|
||||
"projects.create.limitReached.link": "View plans",
|
||||
"projects.move.resource.modal.title": "Choose a project or user to move this {resourceTypeLabel} to",
|
||||
"projects.move.resource.modal.message": "\"{resourceName}\" is currently {inPersonalProject}{inTeamProject}",
|
||||
|
|
|
@ -35,7 +35,9 @@ const initialState = {
|
|||
},
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(CredentialsView);
|
||||
const renderComponent = createComponentRenderer(CredentialsView, {
|
||||
global: { stubs: { ProjectHeader: true } },
|
||||
});
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
|
||||
describe('CredentialsView', () => {
|
||||
|
|
Loading…
Reference in a new issue