Skip to content

Commit d1647ca

Browse files
authored
feat: Allow editing / deleting project variables if you have the project scope (n8n-io#24532)
1 parent 0c2e666 commit d1647ca

2 files changed

Lines changed: 162 additions & 8 deletions

File tree

packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectVariables.test.ts

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ describe('ProjectVariables', () => {
122122
projectsStore.personalProject = {
123123
id: 'personal-project-id',
124124
name: 'Current Project',
125-
scopes: ['projectVariable:create', 'projectVariable:read'],
125+
scopes: [
126+
'projectVariable:create',
127+
'projectVariable:read',
128+
'projectVariable:update',
129+
'projectVariable:delete',
130+
],
126131
} as Project;
127132
projectsStore.currentProject = projectsStore.personalProject;
128133

@@ -423,7 +428,7 @@ describe('ProjectVariables', () => {
423428
});
424429

425430
describe('permissions', () => {
426-
it('should disable edit button when user lacks update permission', async () => {
431+
it('should disable edit button when user lacks both global and project update permission', async () => {
427432
const usersStore = mockedStore(useUsersStore);
428433
usersStore.currentUser = { globalScopes: ['variable:read', 'variable:list'] } as IUser;
429434

@@ -439,6 +444,13 @@ describe('ProjectVariables', () => {
439444
},
440445
];
441446

447+
const projectsStore = mockedStore(useProjectsStore);
448+
projectsStore.currentProject = {
449+
id: 'project-id',
450+
name: 'Test Project',
451+
scopes: ['projectVariable:read', 'projectVariable:list'],
452+
} as Project;
453+
442454
const { getByTestId } = renderComponent();
443455
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
444456

@@ -447,7 +459,7 @@ describe('ProjectVariables', () => {
447459
expect(editButton).toBeDisabled();
448460
});
449461

450-
it('should disable delete button when user lacks delete permission', async () => {
462+
it('should disable delete button when user lacks both global and project delete permission', async () => {
451463
const usersStore = mockedStore(useUsersStore);
452464
usersStore.currentUser = { globalScopes: ['variable:read', 'variable:list'] } as IUser;
453465

@@ -463,6 +475,13 @@ describe('ProjectVariables', () => {
463475
},
464476
];
465477

478+
const projectsStore = mockedStore(useProjectsStore);
479+
projectsStore.currentProject = {
480+
id: 'project-id',
481+
name: 'Test Project',
482+
scopes: ['projectVariable:read', 'projectVariable:list'],
483+
} as Project;
484+
466485
const { getByTestId } = renderComponent();
467486

468487
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
@@ -471,6 +490,136 @@ describe('ProjectVariables', () => {
471490
const deleteButton = getByTestId('variable-row-delete-button');
472491
expect(deleteButton).toBeDisabled();
473492
});
493+
494+
it('should enable edit button when user has global update permission', async () => {
495+
const usersStore = mockedStore(useUsersStore);
496+
usersStore.currentUser = {
497+
globalScopes: ['variable:read', 'variable:list', 'variable:update'],
498+
} as IUser;
499+
500+
const settingsStore = mockedStore(useSettingsStore);
501+
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
502+
503+
const environmentsStore = mockedStore(useEnvironmentsStore);
504+
environmentsStore.variables = [
505+
{
506+
id: '1',
507+
key: 'TEST_VAR',
508+
value: 'value',
509+
},
510+
];
511+
512+
const projectsStore = mockedStore(useProjectsStore);
513+
projectsStore.currentProject = {
514+
id: 'project-id',
515+
name: 'Test Project',
516+
scopes: ['projectVariable:read', 'projectVariable:list'],
517+
} as Project;
518+
519+
const { getByTestId } = renderComponent();
520+
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
521+
522+
await userEvent.hover(getByTestId('variables-row'));
523+
const editButton = getByTestId('variable-row-edit-button');
524+
expect(editButton).not.toBeDisabled();
525+
});
526+
527+
it('should enable edit button when user has project update permission', async () => {
528+
const usersStore = mockedStore(useUsersStore);
529+
usersStore.currentUser = { globalScopes: ['variable:read', 'variable:list'] } as IUser;
530+
531+
const settingsStore = mockedStore(useSettingsStore);
532+
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
533+
534+
const environmentsStore = mockedStore(useEnvironmentsStore);
535+
environmentsStore.variables = [
536+
{
537+
id: '1',
538+
key: 'TEST_VAR',
539+
value: 'value',
540+
},
541+
];
542+
543+
const projectsStore = mockedStore(useProjectsStore);
544+
projectsStore.currentProject = {
545+
id: 'project-id',
546+
name: 'Test Project',
547+
scopes: ['projectVariable:read', 'projectVariable:list', 'projectVariable:update'],
548+
} as Project;
549+
550+
const { getByTestId } = renderComponent();
551+
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
552+
553+
await userEvent.hover(getByTestId('variables-row'));
554+
const editButton = getByTestId('variable-row-edit-button');
555+
expect(editButton).not.toBeDisabled();
556+
});
557+
558+
it('should enable delete button when user has global delete permission', async () => {
559+
const usersStore = mockedStore(useUsersStore);
560+
usersStore.currentUser = {
561+
globalScopes: ['variable:read', 'variable:list', 'variable:delete'],
562+
} as IUser;
563+
564+
const settingsStore = mockedStore(useSettingsStore);
565+
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
566+
567+
const environmentsStore = mockedStore(useEnvironmentsStore);
568+
environmentsStore.variables = [
569+
{
570+
id: '1',
571+
key: 'TEST_VAR',
572+
value: 'value',
573+
},
574+
];
575+
576+
const projectsStore = mockedStore(useProjectsStore);
577+
projectsStore.currentProject = {
578+
id: 'project-id',
579+
name: 'Test Project',
580+
scopes: ['projectVariable:read', 'projectVariable:list'],
581+
} as Project;
582+
583+
const { getByTestId } = renderComponent();
584+
585+
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
586+
587+
await userEvent.hover(getByTestId('variables-row'));
588+
const deleteButton = getByTestId('variable-row-delete-button');
589+
expect(deleteButton).not.toBeDisabled();
590+
});
591+
592+
it('should enable delete button when user has project delete permission', async () => {
593+
const usersStore = mockedStore(useUsersStore);
594+
usersStore.currentUser = { globalScopes: ['variable:read', 'variable:list'] } as IUser;
595+
596+
const settingsStore = mockedStore(useSettingsStore);
597+
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
598+
599+
const environmentsStore = mockedStore(useEnvironmentsStore);
600+
environmentsStore.variables = [
601+
{
602+
id: '1',
603+
key: 'TEST_VAR',
604+
value: 'value',
605+
},
606+
];
607+
608+
const projectsStore = mockedStore(useProjectsStore);
609+
projectsStore.currentProject = {
610+
id: 'project-id',
611+
name: 'Test Project',
612+
scopes: ['projectVariable:read', 'projectVariable:list', 'projectVariable:delete'],
613+
} as Project;
614+
615+
const { getByTestId } = renderComponent();
616+
617+
await waitFor(() => expect(getByTestId('variables-row')).toBeVisible());
618+
619+
await userEvent.hover(getByTestId('variables-row'));
620+
const deleteButton = getByTestId('variable-row-delete-button');
621+
expect(deleteButton).not.toBeDisabled();
622+
});
474623
});
475624

476625
describe('scope column visibility', () => {

packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectVariables.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ const globalPermissions = computed(
7171
const projectPermissions = computed(
7272
() => getResourcePermissions(projectsStore.currentProject?.scopes).projectVariable,
7373
);
74-
7574
const { isLoading, execute } = useAsyncState(environmentsStore.fetchAllVariables, [], {
7675
immediate: true,
7776
});
@@ -422,12 +421,15 @@ onMounted(() => {
422421
</td>
423422
<td v-if="isFeatureEnabled" align="right">
424423
<div class="action-buttons">
425-
<N8nTooltip :disabled="globalPermissions.update" placement="top">
424+
<N8nTooltip
425+
:disabled="globalPermissions.update ?? projectPermissions.update"
426+
placement="top"
427+
>
426428
<N8nButton
427429
data-test-id="variable-row-edit-button"
428430
type="tertiary"
429431
class="mr-xs"
430-
:disabled="!globalPermissions.update"
432+
:disabled="!(globalPermissions.update ?? projectPermissions.update)"
431433
@click="openEditVariableModal(data)"
432434
>
433435
{{ i18n.baseText('variables.row.button.edit') }}
@@ -436,11 +438,14 @@ onMounted(() => {
436438
{{ i18n.baseText('variables.row.button.edit.onlyRoleCanEdit') }}
437439
</template>
438440
</N8nTooltip>
439-
<N8nTooltip :disabled="globalPermissions.delete" placement="top">
441+
<N8nTooltip
442+
:disabled="globalPermissions.delete ?? projectPermissions.delete"
443+
placement="top"
444+
>
440445
<N8nButton
441446
data-test-id="variable-row-delete-button"
442447
type="tertiary"
443-
:disabled="!globalPermissions.delete"
448+
:disabled="!(globalPermissions.delete ?? projectPermissions.delete)"
444449
@click="handleDeleteVariable(data)"
445450
>
446451
{{ i18n.baseText('variables.row.button.delete') }}

0 commit comments

Comments
 (0)