fix(editor): Add disabled state with tooltip on project creation buttons if user lacks permission (#13867)

This commit is contained in:
Guillaume Jacquart
2025-03-12 16:14:02 +01:00
committed by GitHub
parent c7bcdc544d
commit e33d0d7466
6 changed files with 85 additions and 26 deletions

View File

@@ -294,6 +294,7 @@ const {
createCredentialsAppendSlotName, createCredentialsAppendSlotName,
projectsLimitReachedMessage, projectsLimitReachedMessage,
upgradeLabel, upgradeLabel,
hasPermissionToCreateProjects,
} = useGlobalEntityCreation(); } = useGlobalEntityCreation();
onClickOutside(createBtn as Ref<VueInstance>, () => { onClickOutside(createBtn as Ref<VueInstance>, () => {
createBtn.value?.close(); createBtn.value?.close();
@@ -385,7 +386,14 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
placement="right" placement="right"
:content="projectsLimitReachedMessage" :content="projectsLimitReachedMessage"
> >
<N8nIcon
v-if="!hasPermissionToCreateProjects"
style="margin-left: auto; margin-right: 5px"
icon="lock"
size="xsmall"
/>
<N8nButton <N8nButton
v-else
:size="'mini'" :size="'mini'"
style="margin-left: auto" style="margin-left: auto"
type="tertiary" type="tertiary"

View File

@@ -195,4 +195,22 @@ describe('ProjectsNavigation', () => {
expect(getByTestId('project-plus-button')).toBeVisible(); expect(getByTestId('project-plus-button')).toBeVisible();
expect(getByTestId('add-first-project-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();
});
}); });

View File

@@ -83,15 +83,21 @@ const showAddFirstProject = computed(
bold bold
> >
<span>{{ locale.baseText('projects.menu.title') }}</span> <span>{{ locale.baseText('projects.menu.title') }}</span>
<N8nTooltip
placement="right"
:disabled="projectsStore.hasPermissionToCreateProjects"
:content="locale.baseText('projects.create.permissionDenied')"
>
<N8nButton <N8nButton
v-if="projectsStore.canCreateProjects" v-if="projectsStore.canCreateProjects"
icon="plus" icon="plus"
text text
data-test-id="project-plus-button" data-test-id="project-plus-button"
:disabled="isCreatingProject" :disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
:class="$style.plusBtn" :class="$style.plusBtn"
@click="globalEntityCreation.createProject" @click="globalEntityCreation.createProject"
/> />
</N8nTooltip>
</N8nText> </N8nText>
<ElMenu <ElMenu
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled" v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
@@ -118,6 +124,11 @@ const showAddFirstProject = computed(
data-test-id="project-menu-item" data-test-id="project-menu-item"
/> />
</ElMenu> </ElMenu>
<N8nTooltip
placement="right"
:disabled="projectsStore.hasPermissionToCreateProjects"
:content="locale.baseText('projects.create.permissionDenied')"
>
<N8nButton <N8nButton
v-if="showAddFirstProject" v-if="showAddFirstProject"
:class="[ :class="[
@@ -126,7 +137,7 @@ const showAddFirstProject = computed(
[$style.collapsed]: props.collapsed, [$style.collapsed]: props.collapsed,
}, },
]" ]"
:disabled="isCreatingProject" :disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
type="secondary" type="secondary"
icon="plus" icon="plus"
data-test-id="add-first-project-button" data-test-id="add-first-project-button"
@@ -134,6 +145,7 @@ const showAddFirstProject = computed(
> >
{{ locale.baseText('projects.menu.addFirstProject') }} {{ locale.baseText('projects.menu.addFirstProject') }}
</N8nButton> </N8nButton>
</N8nTooltip>
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mb-m" /> <hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mb-m" />
</div> </div>
</template> </template>

View File

@@ -99,6 +99,17 @@ describe('useGlobalEntityCreation', () => {
expect(menu.value[0].submenu?.length).toBe(4); expect(menu.value[0].submenu?.length).toBe(4);
expect(menu.value[1].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()', () => { describe('handleSelect()', () => {
@@ -115,6 +126,7 @@ describe('useGlobalEntityCreation', () => {
const projectsStore = mockedStore(useProjectsStore); const projectsStore = mockedStore(useProjectsStore);
projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.isTeamProjectFeatureEnabled = true;
projectsStore.canCreateProjects = true; projectsStore.canCreateProjects = true;
projectsStore.hasPermissionToCreateProjects = true;
projectsStore.createProject.mockResolvedValueOnce({ name: 'test', id: '1' } as Project); projectsStore.createProject.mockResolvedValueOnce({ name: 'test', id: '1' } as Project);
const { handleSelect } = useGlobalEntityCreation(); const { handleSelect } = useGlobalEntityCreation();
@@ -132,6 +144,7 @@ describe('useGlobalEntityCreation', () => {
const projectsStore = mockedStore(useProjectsStore); const projectsStore = mockedStore(useProjectsStore);
projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.isTeamProjectFeatureEnabled = true;
projectsStore.canCreateProjects = true; projectsStore.canCreateProjects = true;
projectsStore.hasPermissionToCreateProjects = true;
projectsStore.createProject.mockRejectedValueOnce(new Error('error')); projectsStore.createProject.mockRejectedValueOnce(new Error('error'));
const { handleSelect } = useGlobalEntityCreation(); const { handleSelect } = useGlobalEntityCreation();
@@ -162,6 +175,7 @@ describe('useGlobalEntityCreation', () => {
const projectsStore = mockedStore(useProjectsStore); const projectsStore = mockedStore(useProjectsStore);
projectsStore.isTeamProjectFeatureEnabled = true; projectsStore.isTeamProjectFeatureEnabled = true;
projectsStore.teamProjectsLimit = 10; projectsStore.teamProjectsLimit = 10;
projectsStore.hasPermissionToCreateProjects = true;
settingsStore.isCloudDeployment = true; settingsStore.isCloudDeployment = true;
const { projectsLimitReachedMessage, upgradeLabel } = useGlobalEntityCreation(); const { projectsLimitReachedMessage, upgradeLabel } = useGlobalEntityCreation();

View File

@@ -162,7 +162,7 @@ export const useGlobalEntityCreation = () => {
{ {
id: CREATE_PROJECT_ID, id: CREATE_PROJECT_ID,
title: 'Project', title: 'Project',
disabled: !projectsStore.canCreateProjects, disabled: !projectsStore.canCreateProjects || !projectsStore.hasPermissionToCreateProjects,
}, },
]; ];
}); });
@@ -192,7 +192,7 @@ export const useGlobalEntityCreation = () => {
const handleSelect = (id: string) => { const handleSelect = (id: string) => {
if (id !== CREATE_PROJECT_ID) return; if (id !== CREATE_PROJECT_ID) return;
if (projectsStore.canCreateProjects) { if (projectsStore.canCreateProjects && projectsStore.hasPermissionToCreateProjects) {
void createProject(); void createProject();
return; return;
} }
@@ -215,6 +215,10 @@ export const useGlobalEntityCreation = () => {
return i18n.baseText('projects.create.limitReached.self'); return i18n.baseText('projects.create.limitReached.self');
} }
if (!projectsStore.hasPermissionToCreateProjects) {
return i18n.baseText('projects.create.permissionDenied');
}
return i18n.baseText('projects.create.limitReached', { return i18n.baseText('projects.create.limitReached', {
adjustToNumber: projectsStore.teamProjectsLimit, adjustToNumber: projectsStore.teamProjectsLimit,
interpolate: { interpolate: {
@@ -226,6 +230,7 @@ export const useGlobalEntityCreation = () => {
const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`); const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`);
const createWorkflowsAppendSlotName = computed(() => `item.append.${WORKFLOWS_MENU_ID}`); const createWorkflowsAppendSlotName = computed(() => `item.append.${WORKFLOWS_MENU_ID}`);
const createCredentialsAppendSlotName = computed(() => `item.append.${CREDENTIALS_MENU_ID}`); const createCredentialsAppendSlotName = computed(() => `item.append.${CREDENTIALS_MENU_ID}`);
const hasPermissionToCreateProjects = projectsStore.hasPermissionToCreateProjects;
const upgradeLabel = computed(() => { const upgradeLabel = computed(() => {
if (settingsStore.isCloudDeployment) { if (settingsStore.isCloudDeployment) {
@@ -246,6 +251,7 @@ export const useGlobalEntityCreation = () => {
createWorkflowsAppendSlotName, createWorkflowsAppendSlotName,
createCredentialsAppendSlotName, createCredentialsAppendSlotName,
projectsLimitReachedMessage, projectsLimitReachedMessage,
hasPermissionToCreateProjects,
upgradeLabel, upgradeLabel,
createProject, createProject,
isCreatingProject, isCreatingProject,

View File

@@ -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.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.self": "Upgrade to unlock projects for more granular control over sharing, access and organisation of workflows",
"projects.create.limitReached.link": "View plans", "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.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": "\"{resourceName}\" is currently {inPersonalProject}{inTeamProject}",
"projects.move.resource.modal.message.team": "in the \"{resourceHomeProjectName}\" project.", "projects.move.resource.modal.message.team": "in the \"{resourceHomeProjectName}\" project.",