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

View File

@@ -195,4 +195,22 @@ describe('ProjectsNavigation', () => {
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();
});
});

View File

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

View File

@@ -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();

View File

@@ -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,

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.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.",