From e33d0d7466969c372b2ea82eb1b1ebf52ed5fe92 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Wed, 12 Mar 2025 16:14:02 +0100 Subject: [PATCH] fix(editor): Add disabled state with tooltip on project creation buttons if user lacks permission (#13867) --- .../editor-ui/src/components/MainSidebar.vue | 8 +++ .../Projects/ProjectNavigation.test.ts | 18 ++++++ .../components/Projects/ProjectNavigation.vue | 60 +++++++++++-------- .../useGlobalEntityCreation.test.ts | 14 +++++ .../composables/useGlobalEntityCreation.ts | 10 +++- .../src/plugins/i18n/locales/en.json | 1 + 6 files changed, 85 insertions(+), 26 deletions(-) diff --git a/packages/frontend/editor-ui/src/components/MainSidebar.vue b/packages/frontend/editor-ui/src/components/MainSidebar.vue index a5019bd686..7b2fbe7c24 100644 --- a/packages/frontend/editor-ui/src/components/MainSidebar.vue +++ b/packages/frontend/editor-ui/src/components/MainSidebar.vue @@ -294,6 +294,7 @@ const { createCredentialsAppendSlotName, projectsLimitReachedMessage, upgradeLabel, + hasPermissionToCreateProjects, } = useGlobalEntityCreation(); onClickOutside(createBtn as Ref, () => { createBtn.value?.close(); @@ -385,7 +386,14 @@ onClickOutside(createBtn as Ref, () => { placement="right" :content="projectsLimitReachedMessage" > + { expect(getByTestId('project-plus-button')).toBeVisible(); expect(getByTestId('add-first-project-button')).toBeVisible(); }); + + it('should show project plus button and add first project button in disabled state if user does not have permission', async () => { + projectsStore.teamProjectsLimit = -1; + projectsStore.hasPermissionToCreateProjects = false; + + const { getByTestId } = renderComponent({ + props: { + collapsed: false, + }, + }); + const plusButton = getByTestId('project-plus-button'); + const addFirstProjectButton = getByTestId('add-first-project-button'); + + expect(plusButton).toBeVisible(); + expect(plusButton).toBeDisabled(); + expect(addFirstProjectButton).toBeVisible(); + expect(addFirstProjectButton).toBeDisabled(); + }); }); diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.vue b/packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.vue index 62073f20df..095e1d1da9 100644 --- a/packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.vue +++ b/packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.vue @@ -83,15 +83,21 @@ const showAddFirstProject = computed( bold > {{ locale.baseText('projects.menu.title') }} - + + + - - {{ locale.baseText('projects.menu.addFirstProject') }} - + + {{ locale.baseText('projects.menu.addFirstProject') }} + +
diff --git a/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.test.ts b/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.test.ts index f8445325d6..09d04b8ffd 100644 --- a/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.test.ts +++ b/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.test.ts @@ -99,6 +99,17 @@ describe('useGlobalEntityCreation', () => { expect(menu.value[0].submenu?.length).toBe(4); expect(menu.value[1].submenu?.length).toBe(4); }); + + it('disables project creation item if user has no rbac permission', () => { + const projectsStore = mockedStore(useProjectsStore); + projectsStore.canCreateProjects = true; + projectsStore.isTeamProjectFeatureEnabled = true; + projectsStore.hasPermissionToCreateProjects = false; + + const { menu, projectsLimitReachedMessage } = useGlobalEntityCreation(); + expect(menu.value[2].disabled).toBeTruthy(); + expect(projectsLimitReachedMessage.value).toContain('Your current role does not allow you'); + }); }); describe('handleSelect()', () => { @@ -115,6 +126,7 @@ describe('useGlobalEntityCreation', () => { const projectsStore = mockedStore(useProjectsStore); projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.canCreateProjects = true; + projectsStore.hasPermissionToCreateProjects = true; projectsStore.createProject.mockResolvedValueOnce({ name: 'test', id: '1' } as Project); const { handleSelect } = useGlobalEntityCreation(); @@ -132,6 +144,7 @@ describe('useGlobalEntityCreation', () => { const projectsStore = mockedStore(useProjectsStore); projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.canCreateProjects = true; + projectsStore.hasPermissionToCreateProjects = true; projectsStore.createProject.mockRejectedValueOnce(new Error('error')); const { handleSelect } = useGlobalEntityCreation(); @@ -162,6 +175,7 @@ describe('useGlobalEntityCreation', () => { const projectsStore = mockedStore(useProjectsStore); projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.teamProjectsLimit = 10; + projectsStore.hasPermissionToCreateProjects = true; settingsStore.isCloudDeployment = true; const { projectsLimitReachedMessage, upgradeLabel } = useGlobalEntityCreation(); diff --git a/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.ts b/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.ts index 31d967199c..15f74dfc9f 100644 --- a/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.ts +++ b/packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.ts @@ -162,7 +162,7 @@ export const useGlobalEntityCreation = () => { { id: CREATE_PROJECT_ID, title: 'Project', - disabled: !projectsStore.canCreateProjects, + disabled: !projectsStore.canCreateProjects || !projectsStore.hasPermissionToCreateProjects, }, ]; }); @@ -192,7 +192,7 @@ export const useGlobalEntityCreation = () => { const handleSelect = (id: string) => { if (id !== CREATE_PROJECT_ID) return; - if (projectsStore.canCreateProjects) { + if (projectsStore.canCreateProjects && projectsStore.hasPermissionToCreateProjects) { void createProject(); return; } @@ -215,6 +215,10 @@ export const useGlobalEntityCreation = () => { return i18n.baseText('projects.create.limitReached.self'); } + if (!projectsStore.hasPermissionToCreateProjects) { + return i18n.baseText('projects.create.permissionDenied'); + } + return i18n.baseText('projects.create.limitReached', { adjustToNumber: projectsStore.teamProjectsLimit, interpolate: { @@ -226,6 +230,7 @@ export const useGlobalEntityCreation = () => { const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`); const createWorkflowsAppendSlotName = computed(() => `item.append.${WORKFLOWS_MENU_ID}`); const createCredentialsAppendSlotName = computed(() => `item.append.${CREDENTIALS_MENU_ID}`); + const hasPermissionToCreateProjects = projectsStore.hasPermissionToCreateProjects; const upgradeLabel = computed(() => { if (settingsStore.isCloudDeployment) { @@ -246,6 +251,7 @@ export const useGlobalEntityCreation = () => { createWorkflowsAppendSlotName, createCredentialsAppendSlotName, projectsLimitReachedMessage, + hasPermissionToCreateProjects, upgradeLabel, createProject, isCreatingProject, diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json index d4685e92dc..8fea560cc3 100644 --- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json @@ -2689,6 +2689,7 @@ "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.create.permissionDenied": "Your current role does not allow you to create projects", "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}", "projects.move.resource.modal.message.team": "in the \"{resourceHomeProjectName}\" project.",