Skip to content

Commit e33d0d7

Browse files
authoredMar 12, 2025··
fix(editor): Add disabled state with tooltip on project creation buttons if user lacks permission (#13867)
1 parent c7bcdc5 commit e33d0d7

File tree

6 files changed

+85
-26
lines changed

6 files changed

+85
-26
lines changed
 

‎packages/frontend/editor-ui/src/components/MainSidebar.vue

+8
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ const {
294294
createCredentialsAppendSlotName,
295295
projectsLimitReachedMessage,
296296
upgradeLabel,
297+
hasPermissionToCreateProjects,
297298
} = useGlobalEntityCreation();
298299
onClickOutside(createBtn as Ref<VueInstance>, () => {
299300
createBtn.value?.close();
@@ -385,7 +386,14 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
385386
placement="right"
386387
:content="projectsLimitReachedMessage"
387388
>
389+
<N8nIcon
390+
v-if="!hasPermissionToCreateProjects"
391+
style="margin-left: auto; margin-right: 5px"
392+
icon="lock"
393+
size="xsmall"
394+
/>
388395
<N8nButton
396+
v-else
389397
:size="'mini'"
390398
style="margin-left: auto"
391399
type="tertiary"

‎packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,22 @@ describe('ProjectsNavigation', () => {
195195
expect(getByTestId('project-plus-button')).toBeVisible();
196196
expect(getByTestId('add-first-project-button')).toBeVisible();
197197
});
198+
199+
it('should show project plus button and add first project button in disabled state if user does not have permission', async () => {
200+
projectsStore.teamProjectsLimit = -1;
201+
projectsStore.hasPermissionToCreateProjects = false;
202+
203+
const { getByTestId } = renderComponent({
204+
props: {
205+
collapsed: false,
206+
},
207+
});
208+
const plusButton = getByTestId('project-plus-button');
209+
const addFirstProjectButton = getByTestId('add-first-project-button');
210+
211+
expect(plusButton).toBeVisible();
212+
expect(plusButton).toBeDisabled();
213+
expect(addFirstProjectButton).toBeVisible();
214+
expect(addFirstProjectButton).toBeDisabled();
215+
});
198216
});

‎packages/frontend/editor-ui/src/components/Projects/ProjectNavigation.vue

+36-24
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,21 @@ const showAddFirstProject = computed(
8383
bold
8484
>
8585
<span>{{ locale.baseText('projects.menu.title') }}</span>
86-
<N8nButton
87-
v-if="projectsStore.canCreateProjects"
88-
icon="plus"
89-
text
90-
data-test-id="project-plus-button"
91-
:disabled="isCreatingProject"
92-
:class="$style.plusBtn"
93-
@click="globalEntityCreation.createProject"
94-
/>
86+
<N8nTooltip
87+
placement="right"
88+
:disabled="projectsStore.hasPermissionToCreateProjects"
89+
:content="locale.baseText('projects.create.permissionDenied')"
90+
>
91+
<N8nButton
92+
v-if="projectsStore.canCreateProjects"
93+
icon="plus"
94+
text
95+
data-test-id="project-plus-button"
96+
:disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
97+
:class="$style.plusBtn"
98+
@click="globalEntityCreation.createProject"
99+
/>
100+
</N8nTooltip>
95101
</N8nText>
96102
<ElMenu
97103
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
@@ -118,22 +124,28 @@ const showAddFirstProject = computed(
118124
data-test-id="project-menu-item"
119125
/>
120126
</ElMenu>
121-
<N8nButton
122-
v-if="showAddFirstProject"
123-
:class="[
124-
$style.addFirstProjectBtn,
125-
{
126-
[$style.collapsed]: props.collapsed,
127-
},
128-
]"
129-
:disabled="isCreatingProject"
130-
type="secondary"
131-
icon="plus"
132-
data-test-id="add-first-project-button"
133-
@click="globalEntityCreation.createProject"
127+
<N8nTooltip
128+
placement="right"
129+
:disabled="projectsStore.hasPermissionToCreateProjects"
130+
:content="locale.baseText('projects.create.permissionDenied')"
134131
>
135-
{{ locale.baseText('projects.menu.addFirstProject') }}
136-
</N8nButton>
132+
<N8nButton
133+
v-if="showAddFirstProject"
134+
:class="[
135+
$style.addFirstProjectBtn,
136+
{
137+
[$style.collapsed]: props.collapsed,
138+
},
139+
]"
140+
:disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
141+
type="secondary"
142+
icon="plus"
143+
data-test-id="add-first-project-button"
144+
@click="globalEntityCreation.createProject"
145+
>
146+
{{ locale.baseText('projects.menu.addFirstProject') }}
147+
</N8nButton>
148+
</N8nTooltip>
137149
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mb-m" />
138150
</div>
139151
</template>

‎packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ describe('useGlobalEntityCreation', () => {
9999
expect(menu.value[0].submenu?.length).toBe(4);
100100
expect(menu.value[1].submenu?.length).toBe(4);
101101
});
102+
103+
it('disables project creation item if user has no rbac permission', () => {
104+
const projectsStore = mockedStore(useProjectsStore);
105+
projectsStore.canCreateProjects = true;
106+
projectsStore.isTeamProjectFeatureEnabled = true;
107+
projectsStore.hasPermissionToCreateProjects = false;
108+
109+
const { menu, projectsLimitReachedMessage } = useGlobalEntityCreation();
110+
expect(menu.value[2].disabled).toBeTruthy();
111+
expect(projectsLimitReachedMessage.value).toContain('Your current role does not allow you');
112+
});
102113
});
103114

104115
describe('handleSelect()', () => {
@@ -115,6 +126,7 @@ describe('useGlobalEntityCreation', () => {
115126
const projectsStore = mockedStore(useProjectsStore);
116127
projectsStore.isTeamProjectFeatureEnabled = true;
117128
projectsStore.canCreateProjects = true;
129+
projectsStore.hasPermissionToCreateProjects = true;
118130
projectsStore.createProject.mockResolvedValueOnce({ name: 'test', id: '1' } as Project);
119131

120132
const { handleSelect } = useGlobalEntityCreation();
@@ -132,6 +144,7 @@ describe('useGlobalEntityCreation', () => {
132144
const projectsStore = mockedStore(useProjectsStore);
133145
projectsStore.isTeamProjectFeatureEnabled = true;
134146
projectsStore.canCreateProjects = true;
147+
projectsStore.hasPermissionToCreateProjects = true;
135148
projectsStore.createProject.mockRejectedValueOnce(new Error('error'));
136149

137150
const { handleSelect } = useGlobalEntityCreation();
@@ -162,6 +175,7 @@ describe('useGlobalEntityCreation', () => {
162175
const projectsStore = mockedStore(useProjectsStore);
163176
projectsStore.isTeamProjectFeatureEnabled = true;
164177
projectsStore.teamProjectsLimit = 10;
178+
projectsStore.hasPermissionToCreateProjects = true;
165179

166180
settingsStore.isCloudDeployment = true;
167181
const { projectsLimitReachedMessage, upgradeLabel } = useGlobalEntityCreation();

‎packages/frontend/editor-ui/src/composables/useGlobalEntityCreation.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const useGlobalEntityCreation = () => {
162162
{
163163
id: CREATE_PROJECT_ID,
164164
title: 'Project',
165-
disabled: !projectsStore.canCreateProjects,
165+
disabled: !projectsStore.canCreateProjects || !projectsStore.hasPermissionToCreateProjects,
166166
},
167167
];
168168
});
@@ -192,7 +192,7 @@ export const useGlobalEntityCreation = () => {
192192
const handleSelect = (id: string) => {
193193
if (id !== CREATE_PROJECT_ID) return;
194194

195-
if (projectsStore.canCreateProjects) {
195+
if (projectsStore.canCreateProjects && projectsStore.hasPermissionToCreateProjects) {
196196
void createProject();
197197
return;
198198
}
@@ -215,6 +215,10 @@ export const useGlobalEntityCreation = () => {
215215
return i18n.baseText('projects.create.limitReached.self');
216216
}
217217

218+
if (!projectsStore.hasPermissionToCreateProjects) {
219+
return i18n.baseText('projects.create.permissionDenied');
220+
}
221+
218222
return i18n.baseText('projects.create.limitReached', {
219223
adjustToNumber: projectsStore.teamProjectsLimit,
220224
interpolate: {
@@ -226,6 +230,7 @@ export const useGlobalEntityCreation = () => {
226230
const createProjectAppendSlotName = computed(() => `item.append.${CREATE_PROJECT_ID}`);
227231
const createWorkflowsAppendSlotName = computed(() => `item.append.${WORKFLOWS_MENU_ID}`);
228232
const createCredentialsAppendSlotName = computed(() => `item.append.${CREDENTIALS_MENU_ID}`);
233+
const hasPermissionToCreateProjects = projectsStore.hasPermissionToCreateProjects;
229234

230235
const upgradeLabel = computed(() => {
231236
if (settingsStore.isCloudDeployment) {
@@ -246,6 +251,7 @@ export const useGlobalEntityCreation = () => {
246251
createWorkflowsAppendSlotName,
247252
createCredentialsAppendSlotName,
248253
projectsLimitReachedMessage,
254+
hasPermissionToCreateProjects,
249255
upgradeLabel,
250256
createProject,
251257
isCreatingProject,

‎packages/frontend/editor-ui/src/plugins/i18n/locales/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -2689,6 +2689,7 @@
26892689
"projects.create.limitReached.cloud": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects.",
26902690
"projects.create.limitReached.self": "Upgrade to unlock projects for more granular control over sharing, access and organisation of workflows",
26912691
"projects.create.limitReached.link": "View plans",
2692+
"projects.create.permissionDenied": "Your current role does not allow you to create projects",
26922693
"projects.move.resource.modal.title": "Choose a project or user to move this {resourceTypeLabel} to",
26932694
"projects.move.resource.modal.message": "\"{resourceName}\" is currently {inPersonalProject}{inTeamProject}",
26942695
"projects.move.resource.modal.message.team": "in the \"{resourceHomeProjectName}\" project.",

0 commit comments

Comments
 (0)
Please sign in to comment.