mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-13 13:57:29 -08:00
fix(editor): Update the universal create button interaction (#12105)
This commit is contained in:
parent
c572c0648c
commit
5300e0ac45
|
@ -6,48 +6,10 @@ export class CredentialsPage extends BasePage {
|
|||
getters = {
|
||||
emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'),
|
||||
createCredentialButton: () => {
|
||||
cy.getByTestId('resource-add').should('be.visible').click();
|
||||
cy.getByTestId('resource-add')
|
||||
.find('.el-sub-menu__title')
|
||||
.as('menuitem')
|
||||
.should('have.attr', 'aria-describedby');
|
||||
|
||||
cy.get('@menuitem')
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'aria-describedby')
|
||||
.then((el) => cy.get(`[id="${el}"]`))
|
||||
.as('submenu');
|
||||
|
||||
cy.get('@submenu')
|
||||
.should('be.visible')
|
||||
.within((submenu) => {
|
||||
// If submenu has another submenu
|
||||
if (submenu.find('[data-test-id="navigation-submenu"]').length) {
|
||||
cy.wrap(submenu)
|
||||
.find('[data-test-id="navigation-submenu"]')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Credential")')
|
||||
.as('child')
|
||||
.click();
|
||||
|
||||
cy.get('@child')
|
||||
.should('be.visible')
|
||||
.find('[data-test-id="navigation-submenu-item"]')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Personal")')
|
||||
.as('button');
|
||||
} else {
|
||||
cy.wrap(submenu)
|
||||
.find('[data-test-id="navigation-menu-item"]')
|
||||
.filter(':contains("Credential")')
|
||||
.as('button');
|
||||
}
|
||||
});
|
||||
|
||||
return cy.get('@button').should('be.visible');
|
||||
cy.getByTestId('add-resource').should('be.visible').click();
|
||||
cy.getByTestId('add-resource').getByTestId('action-credential').should('be.visible');
|
||||
return cy.getByTestId('add-resource').getByTestId('action-credential');
|
||||
},
|
||||
|
||||
// cy.getByTestId('resources-list-add'),
|
||||
searchInput: () => cy.getByTestId('resources-list-search'),
|
||||
emptyList: () => cy.getByTestId('resources-list-empty'),
|
||||
credentialCards: () => cy.getByTestId('resources-list-item'),
|
||||
|
|
|
@ -8,45 +8,8 @@ export class WorkflowsPage extends BasePage {
|
|||
newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'),
|
||||
searchBar: () => cy.getByTestId('resources-list-search'),
|
||||
createWorkflowButton: () => {
|
||||
cy.getByTestId('resource-add').should('be.visible').click();
|
||||
cy.getByTestId('resource-add')
|
||||
.find('.el-sub-menu__title')
|
||||
.as('menuitem')
|
||||
.should('have.attr', 'aria-describedby');
|
||||
|
||||
cy.get('@menuitem')
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'aria-describedby')
|
||||
.then((el) => cy.get(`[id="${el}"]`))
|
||||
.as('submenu');
|
||||
|
||||
cy.get('@submenu')
|
||||
.should('be.visible')
|
||||
.within((submenu) => {
|
||||
// If submenu has another submenu
|
||||
if (submenu.find('[data-test-id="navigation-submenu"]').length) {
|
||||
cy.wrap(submenu)
|
||||
.find('[data-test-id="navigation-submenu"]')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Workflow")')
|
||||
.as('child')
|
||||
.click();
|
||||
|
||||
cy.get('@child')
|
||||
.should('be.visible')
|
||||
.find('[data-test-id="navigation-submenu-item"]')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Personal")')
|
||||
.as('button');
|
||||
} else {
|
||||
cy.wrap(submenu)
|
||||
.find('[data-test-id="navigation-menu-item"]')
|
||||
.filter(':contains("Workflow")')
|
||||
.as('button');
|
||||
}
|
||||
});
|
||||
|
||||
return cy.get('@button').should('be.visible');
|
||||
cy.getByTestId('add-resource-workflow').should('be.visible');
|
||||
return cy.getByTestId('add-resource-workflow');
|
||||
},
|
||||
workflowCards: () => cy.getByTestId('resources-list-item'),
|
||||
workflowCard: (workflowName: string) =>
|
||||
|
|
|
@ -759,7 +759,10 @@ const createToastMessagingForNewCredentials = (
|
|||
toastText = i18n.baseText('credentials.create.personal.toast.text');
|
||||
}
|
||||
|
||||
if (projectsStore.currentProject) {
|
||||
if (
|
||||
projectsStore.currentProject &&
|
||||
projectsStore.currentProject.id !== projectsStore.personalProject?.id
|
||||
) {
|
||||
toastTitle = i18n.baseText('credentials.create.project.toast.title', {
|
||||
interpolate: { projectName: project?.name ?? '' },
|
||||
});
|
||||
|
|
|
@ -605,7 +605,11 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
|||
if (!id || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(id)) {
|
||||
let toastTitle = locale.baseText('workflows.create.personal.toast.title');
|
||||
let toastText = locale.baseText('workflows.create.personal.toast.text');
|
||||
if (projectsStore.currentProject) {
|
||||
|
||||
if (
|
||||
projectsStore.currentProject &&
|
||||
projectsStore.currentProject.id !== projectsStore.personalProject?.id
|
||||
) {
|
||||
toastTitle = locale.baseText('workflows.create.project.toast.title', {
|
||||
interpolate: { projectName: projectsStore.currentProject.name ?? '' },
|
||||
});
|
||||
|
|
|
@ -296,6 +296,7 @@ const {
|
|||
handleSelect: handleMenuSelect,
|
||||
createProjectAppendSlotName,
|
||||
projectsLimitReachedMessage,
|
||||
upgradeLabel,
|
||||
} = useGlobalEntityCreation();
|
||||
onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||
createBtn.value?.close();
|
||||
|
@ -336,7 +337,7 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
|||
type="tertiary"
|
||||
@click="handleMenuSelect(item.id)"
|
||||
>
|
||||
{{ i18n.baseText('generic.upgrade') }}
|
||||
{{ upgradeLabel }}
|
||||
</N8nButton>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts" setup>
|
||||
import { N8nIconButton, N8nActionToggle } from 'n8n-design-system';
|
||||
|
||||
type Action = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
defineProps<{
|
||||
actions: Action[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [id: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[$style.buttonGroup]">
|
||||
<slot></slot>
|
||||
<N8nActionToggle
|
||||
data-test-id="add-resource"
|
||||
:actions="actions"
|
||||
placement="bottom-end"
|
||||
:teleported="false"
|
||||
@action="emit('action', $event)"
|
||||
>
|
||||
<N8nIconButton :class="[$style.buttonGroupDropdown]" icon="angle-down" />
|
||||
</N8nActionToggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttonGroup {
|
||||
display: inline-flex;
|
||||
|
||||
:global(> .button) {
|
||||
border-right: 1px solid var(--button-font-color, var(--color-button-primary-font));
|
||||
|
||||
&:not(:first-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroupDropdown {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,4 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { within } from '@testing-library/dom';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createTestProject } from '@/__tests__/data/projects';
|
||||
|
@ -10,13 +9,19 @@ import { useProjectsStore } from '@/stores/projects.store';
|
|||
import type { Project } from '@/types/projects.types';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import { VIEWS } from '@/constants';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor, within } from '@testing-library/vue';
|
||||
|
||||
const mockPush = vi.fn();
|
||||
vi.mock('vue-router', async () => {
|
||||
const actual = await vi.importActual('vue-router');
|
||||
const params = {};
|
||||
const location = {};
|
||||
return {
|
||||
...actual,
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useRoute: () => ({
|
||||
params,
|
||||
location,
|
||||
|
@ -32,7 +37,6 @@ const renderComponent = createComponentRenderer(ProjectHeader, {
|
|||
global: {
|
||||
stubs: {
|
||||
ProjectTabs: projectTabsSpy,
|
||||
N8nNavigationDropdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -143,23 +147,45 @@ describe('ProjectHeader', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[null, 'Create'],
|
||||
[createTestProject({ type: ProjectTypes.Personal }), 'Create in personal'],
|
||||
[createTestProject({ type: ProjectTypes.Team }), 'Create in project'],
|
||||
])('in project %s should render correct create button label %s', (project, label) => {
|
||||
projectsStore.currentProject = project;
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
stubs: {
|
||||
N8nNavigationDropdown: {
|
||||
template: '<div><slot></slot></div>',
|
||||
},
|
||||
},
|
||||
},
|
||||
it('should create a workflow', async () => {
|
||||
const project = createTestProject({
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
projectsStore.currentProject = project;
|
||||
|
||||
expect(within(getByTestId('resource-add')).getByRole('button', { name: label })).toBeVisible();
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
await userEvent.click(getByTestId('add-resource-workflow'));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith({
|
||||
name: VIEWS.NEW_WORKFLOW,
|
||||
query: { projectId: project.id },
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropdown', () => {
|
||||
it('should create a credential', async () => {
|
||||
const project = createTestProject({
|
||||
scopes: ['credential:create'],
|
||||
});
|
||||
projectsStore.currentProject = project;
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
await userEvent.click(within(getByTestId('add-resource')).getByRole('button'));
|
||||
|
||||
await waitFor(() => expect(getByTestId('action-credential')).toBeVisible());
|
||||
|
||||
await userEvent.click(getByTestId('action-credential'));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith({
|
||||
name: VIEWS.PROJECTS_CREDENTIALS,
|
||||
params: {
|
||||
projectId: project.id,
|
||||
credentialId: 'create',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render creation button in setting page', async () => {
|
||||
|
@ -167,15 +193,7 @@ describe('ProjectHeader', () => {
|
|||
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();
|
||||
const { queryByTestId } = renderComponent();
|
||||
expect(queryByTestId('add-resource-buttons')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, type Ref, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { N8nNavigationDropdown, N8nButton, N8nIconButton, N8nTooltip } from 'n8n-design-system';
|
||||
import { onClickOutside, type VueInstance } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { N8nButton } from 'n8n-design-system';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
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';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import ProjectCreateResource from '@/components/Projects/ProjectCreateResource.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const i18n = useI18n();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const createBtn = ref<InstanceType<typeof N8nNavigationDropdown>>();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
|
||||
const headerIcon = computed(() => {
|
||||
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
||||
|
@ -48,22 +48,58 @@ const showSettings = computed(
|
|||
projectsStore.currentProject?.type === ProjectTypes.Team,
|
||||
);
|
||||
|
||||
const { menu, handleSelect, createProjectAppendSlotName, projectsLimitReachedMessage } =
|
||||
useGlobalEntityCreation(computed(() => !Boolean(projectsStore.currentProject)));
|
||||
const homeProject = computed(() => projectsStore.currentProject ?? projectsStore.personalProject);
|
||||
|
||||
const createLabel = computed(() => {
|
||||
if (!projectsStore.currentProject) {
|
||||
return i18n.baseText('projects.create');
|
||||
} else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
|
||||
return i18n.baseText('projects.create.personal');
|
||||
} else {
|
||||
return i18n.baseText('projects.create.team');
|
||||
const ACTION_TYPES = {
|
||||
WORKFLOW: 'workflow',
|
||||
CREDENTIAL: 'credential',
|
||||
} as const;
|
||||
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
|
||||
|
||||
const createWorkflowButton = computed(() => ({
|
||||
value: ACTION_TYPES.WORKFLOW,
|
||||
label: 'Create Workflow',
|
||||
disabled:
|
||||
sourceControlStore.preferences.branchReadOnly ||
|
||||
!getResourcePermissions(homeProject.value?.scopes).workflow.create,
|
||||
}));
|
||||
const menu = computed(() => [
|
||||
{
|
||||
value: ACTION_TYPES.CREDENTIAL,
|
||||
label: 'Create credential',
|
||||
disabled:
|
||||
sourceControlStore.preferences.branchReadOnly ||
|
||||
!getResourcePermissions(homeProject.value?.scopes).credential.create,
|
||||
},
|
||||
]);
|
||||
|
||||
const actions: Record<ActionTypes, (projectId: string) => void> = {
|
||||
[ACTION_TYPES.WORKFLOW]: (projectId: string) => {
|
||||
void router.push({
|
||||
name: VIEWS.NEW_WORKFLOW,
|
||||
query: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[ACTION_TYPES.CREDENTIAL]: (projectId: string) => {
|
||||
void router.push({
|
||||
name: VIEWS.PROJECTS_CREDENTIALS,
|
||||
params: {
|
||||
projectId,
|
||||
credentialId: 'create',
|
||||
},
|
||||
});
|
||||
},
|
||||
} as const;
|
||||
|
||||
const onSelect = (action: string) => {
|
||||
const executableAction = actions[action as ActionTypes];
|
||||
if (!homeProject.value) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||
createBtn.value?.close();
|
||||
});
|
||||
executableAction(homeProject.value.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -83,30 +119,17 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
|||
</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"
|
||||
<ProjectCreateResource
|
||||
data-test-id="add-resource-buttons"
|
||||
:actions="menu"
|
||||
@action="onSelect"
|
||||
>
|
||||
<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>
|
||||
<N8nButton
|
||||
data-test-id="add-resource-workflow"
|
||||
v-bind="createWorkflowButton"
|
||||
@click="onSelect(ACTION_TYPES.WORKFLOW)"
|
||||
/>
|
||||
</ProjectCreateResource>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import type router from 'vue-router';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
|
@ -80,79 +79,6 @@ describe('useGlobalEntityCreation', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('single project', () => {
|
||||
const currentProjectId = 'current-project';
|
||||
|
||||
it('should use currentProject', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
projectsStore.currentProject = { id: currentProjectId } as Project;
|
||||
|
||||
const { menu } = useGlobalEntityCreation(false);
|
||||
|
||||
expect(menu.value[0]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
route: { name: VIEWS.NEW_WORKFLOW, query: { projectId: currentProjectId } },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(menu.value[1]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
route: {
|
||||
name: VIEWS.PROJECTS_CREDENTIALS,
|
||||
params: { projectId: currentProjectId, credentialId: 'create' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be disabled in readOnly', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
projectsStore.currentProject = { id: currentProjectId } as Project;
|
||||
|
||||
const sourceControl = mockedStore(useSourceControlStore);
|
||||
sourceControl.preferences.branchReadOnly = true;
|
||||
|
||||
const { menu } = useGlobalEntityCreation(false);
|
||||
|
||||
expect(menu.value[0]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
disabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(menu.value[1]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
disabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be disabled based in scopes', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
projectsStore.currentProject = { id: currentProjectId, scopes: [] } as unknown as Project;
|
||||
|
||||
const { menu } = useGlobalEntityCreation(false);
|
||||
|
||||
expect(menu.value[0]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
disabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(menu.value[1]).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
disabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('global', () => {
|
||||
it('should show personal + all team projects', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
|
@ -167,7 +93,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
{ id: '3', name: '3', type: 'team' },
|
||||
] as ProjectListItem[];
|
||||
|
||||
const { menu } = useGlobalEntityCreation(true);
|
||||
const { menu } = useGlobalEntityCreation();
|
||||
|
||||
expect(menu.value[0].submenu?.length).toBe(4);
|
||||
expect(menu.value[1].submenu?.length).toBe(4);
|
||||
|
@ -178,7 +104,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
it('should only handle create-project', () => {
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
const { handleSelect } = useGlobalEntityCreation(true);
|
||||
const { handleSelect } = useGlobalEntityCreation();
|
||||
handleSelect('dummy');
|
||||
expect(projectsStore.createProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -190,7 +116,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
projectsStore.canCreateProjects = true;
|
||||
projectsStore.createProject.mockResolvedValueOnce({ name: 'test', id: '1' } as Project);
|
||||
|
||||
const { handleSelect } = useGlobalEntityCreation(true);
|
||||
const { handleSelect } = useGlobalEntityCreation();
|
||||
|
||||
handleSelect('create-project');
|
||||
await flushPromises();
|
||||
|
@ -207,7 +133,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
projectsStore.canCreateProjects = true;
|
||||
projectsStore.createProject.mockRejectedValueOnce(new Error('error'));
|
||||
|
||||
const { handleSelect } = useGlobalEntityCreation(true);
|
||||
const { handleSelect } = useGlobalEntityCreation();
|
||||
|
||||
handleSelect('create-project');
|
||||
await flushPromises();
|
||||
|
@ -220,7 +146,7 @@ describe('useGlobalEntityCreation', () => {
|
|||
projectsStore.isTeamProjectFeatureEnabled = true;
|
||||
const redirect = usePageRedirectionHelper();
|
||||
|
||||
const { handleSelect } = useGlobalEntityCreation(true);
|
||||
const { handleSelect } = useGlobalEntityCreation();
|
||||
|
||||
handleSelect('create-project');
|
||||
expect(redirect.goToUpgrade).toHaveBeenCalled();
|
||||
|
@ -237,14 +163,20 @@ describe('useGlobalEntityCreation', () => {
|
|||
projectsStore.teamProjectsLimit = 10;
|
||||
|
||||
settingsStore.isCloudDeployment = true;
|
||||
const { projectsLimitReachedMessage } = useGlobalEntityCreation(true);
|
||||
const { projectsLimitReachedMessage, upgradeLabel } = useGlobalEntityCreation();
|
||||
expect(projectsLimitReachedMessage.value).toContain(
|
||||
'You have reached the Pro plan limit of 10.',
|
||||
);
|
||||
expect(upgradeLabel.value).toBe('Upgrade');
|
||||
|
||||
settingsStore.isCloudDeployment = false;
|
||||
expect(projectsLimitReachedMessage.value).toContain('You have reached the plan limit of');
|
||||
expect(upgradeLabel.value).toBe('Upgrade');
|
||||
|
||||
projectsStore.isTeamProjectFeatureEnabled = false;
|
||||
expect(projectsLimitReachedMessage.value).toContain(
|
||||
'Upgrade to unlock projects for more granular control over sharing, access and organisation of workflows',
|
||||
);
|
||||
expect(upgradeLabel.value).toBe('Enterprise');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { computed, toValue, type ComputedRef, type Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
@ -25,9 +25,7 @@ type Item = BaseItem & {
|
|||
submenu?: BaseItem[];
|
||||
};
|
||||
|
||||
export const useGlobalEntityCreation = (
|
||||
multipleProjects: Ref<boolean> | ComputedRef<boolean> | boolean = true,
|
||||
) => {
|
||||
export const useGlobalEntityCreation = () => {
|
||||
const CREATE_PROJECT_ID = 'create-project';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
@ -77,34 +75,10 @@ export const useGlobalEntityCreation = (
|
|||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// single project
|
||||
if (!toValue(multipleProjects)) {
|
||||
return [
|
||||
{
|
||||
id: 'workflow',
|
||||
title: 'Workflow',
|
||||
disabled: disabledWorkflow(projectsStore.currentProject?.scopes),
|
||||
route: {
|
||||
name: VIEWS.NEW_WORKFLOW,
|
||||
query: {
|
||||
projectId: projectsStore.currentProject?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Credential',
|
||||
disabled: disabledCredential(projectsStore.currentProject?.scopes),
|
||||
route: {
|
||||
name: VIEWS.PROJECTS_CREDENTIALS,
|
||||
params: {
|
||||
projectId: projectsStore.currentProject?.id,
|
||||
credentialId: 'create',
|
||||
},
|
||||
},
|
||||
id: CREATE_PROJECT_ID,
|
||||
title: 'Project',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -211,7 +185,7 @@ export const useGlobalEntityCreation = (
|
|||
|
||||
const projectsLimitReachedMessage = computed(() => {
|
||||
if (settingsStore.isCloudDeployment) {
|
||||
return i18n.baseText('projects.create.limitReached', {
|
||||
return i18n.baseText('projects.create.limitReached.cloud', {
|
||||
adjustToNumber: projectsStore.teamProjectsLimit,
|
||||
interpolate: {
|
||||
planName: cloudPlanStore.currentPlanData?.displayName ?? '',
|
||||
|
@ -220,10 +194,37 @@ export const useGlobalEntityCreation = (
|
|||
});
|
||||
}
|
||||
|
||||
return i18n.baseText('projects.create.limitReached.self');
|
||||
if (!projectsStore.isTeamProjectFeatureEnabled) {
|
||||
return i18n.baseText('projects.create.limitReached.self');
|
||||
}
|
||||
|
||||
return i18n.baseText('projects.create.limitReached', {
|
||||
adjustToNumber: projectsStore.teamProjectsLimit,
|
||||
interpolate: {
|
||||
limit: projectsStore.teamProjectsLimit,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`);
|
||||
|
||||
return { menu, handleSelect, createProjectAppendSlotName, projectsLimitReachedMessage };
|
||||
const upgradeLabel = computed(() => {
|
||||
if (settingsStore.isCloudDeployment) {
|
||||
return i18n.baseText('generic.upgrade');
|
||||
}
|
||||
|
||||
if (!projectsStore.isTeamProjectFeatureEnabled) {
|
||||
return i18n.baseText('generic.enterprise');
|
||||
}
|
||||
|
||||
return i18n.baseText('generic.upgrade');
|
||||
});
|
||||
|
||||
return {
|
||||
menu,
|
||||
handleSelect,
|
||||
createProjectAppendSlotName,
|
||||
projectsLimitReachedMessage,
|
||||
upgradeLabel,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"generic.copy": "Copy",
|
||||
"generic.delete": "Delete",
|
||||
"generic.dontShowAgain": "Don't show again",
|
||||
"generic.enterprise": "Enterprise",
|
||||
"generic.executions": "Executions",
|
||||
"generic.tests": "Tests",
|
||||
"generic.or": "or",
|
||||
|
@ -77,6 +78,7 @@
|
|||
"generic.ownedByMe": "(You)",
|
||||
"generic.moreInfo": "More info",
|
||||
"generic.next": "Next",
|
||||
"generic.pro": "Pro",
|
||||
"generic.viewDocs": "View docs",
|
||||
"about.aboutN8n": "About n8n",
|
||||
"about.close": "Close",
|
||||
|
@ -630,7 +632,7 @@
|
|||
"credentials.noResults.withSearch.switchToShared.preamble": "some credentials may be",
|
||||
"credentials.noResults.withSearch.switchToShared.link": "hidden",
|
||||
"credentials.create.personal.toast.title": "Credential successfully created",
|
||||
"credentials.create.personal.toast.text": "This credential is currently private to you.",
|
||||
"credentials.create.personal.toast.text": "This credential has been created inside your personal space.",
|
||||
"credentials.create.project.toast.title": "Credential successfully created in {projectName}",
|
||||
"credentials.create.project.toast.text": "All members from {projectName} will have access to this credential.",
|
||||
"dataDisplay.needHelp": "Need help?",
|
||||
|
@ -2329,7 +2331,7 @@
|
|||
"workflows.concurrentChanges.confirmMessage.cancelButtonText": "Cancel",
|
||||
"workflows.concurrentChanges.confirmMessage.confirmButtonText": "Overwrite and Save",
|
||||
"workflows.create.personal.toast.title": "Workflow successfully created",
|
||||
"workflows.create.personal.toast.text": "This workflow is currently private to you.",
|
||||
"workflows.create.personal.toast.text": "This workflow has been created inside your personal space.",
|
||||
"workflows.create.project.toast.title": "Workflow successfully created in {projectName}",
|
||||
"workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.",
|
||||
"importCurlModal.title": "Import cURL command",
|
||||
|
@ -2571,6 +2573,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.cloud": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects.",
|
||||
"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",
|
||||
|
|
Loading…
Reference in a new issue