Skip to content

Commit eb19e00

Browse files
authored
feat(editor): Expose workflow:execute scope checkbox in custom role UI (#26405)
1 parent b935dc1 commit eb19e00

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

packages/frontend/@n8n/i18n/src/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2354,8 +2354,9 @@
23542354
"projectRoles.workflow:read": "View",
23552355
"projectRoles.workflow:read.tooltip": "View workflows within the project",
23562356
"projectRoles.workflow:execute": "Execute",
2357+
"projectRoles.workflow:execute.tooltip": "Execute workflows within the project",
23572358
"projectRoles.workflow:update": "Edit",
2358-
"projectRoles.workflow:update.tooltip": "Edit workflow content and execute workflows",
2359+
"projectRoles.workflow:update.tooltip": "Edit workflow content",
23592360
"projectRoles.workflow:create": "Create",
23602361
"projectRoles.workflow:create.tooltip": "Create new workflows",
23612362
"projectRoles.workflow:share": "Share",

packages/frontend/editor-ui/src/features/collaboration/projects/components/RoleHoverPopover.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ describe('RoleHoverPopover', () => {
9292
it('should display permission count', () => {
9393
const { getByText } = renderComponent();
9494

95-
expect(getByText('3/40 permissions')).toBeInTheDocument();
95+
expect(getByText('3/41 permissions')).toBeInTheDocument();
9696
});
9797

9898
it('should display role description when available', () => {

packages/frontend/editor-ui/src/features/project-roles/ProjectRoleView.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,116 @@ describe('ProjectRoleView', () => {
492492
});
493493
});
494494

495+
describe('workflow:execute scope dependency', () => {
496+
it('should render workflow:execute checkbox in the UI', async () => {
497+
const { getByTestId } = renderComponent();
498+
499+
await waitFor(() =>
500+
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
501+
);
502+
});
503+
504+
it('should auto-check workflow:read when workflow:execute is checked and workflow:read is unchecked', async () => {
505+
const { getByTestId } = renderComponent();
506+
507+
await waitFor(() =>
508+
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
509+
);
510+
511+
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
512+
const readCheckbox = getByTestId('scope-checkbox-workflow:read');
513+
514+
// workflow:read starts checked (it's in defaultScopes), uncheck it first
515+
await userEvent.click(readCheckbox);
516+
expect(readCheckbox).not.toBeChecked();
517+
expect(executeCheckbox).not.toBeChecked();
518+
519+
// Now check workflow:execute — should auto-check workflow:read
520+
await userEvent.click(executeCheckbox);
521+
expect(executeCheckbox).toBeChecked();
522+
expect(readCheckbox).toBeChecked();
523+
});
524+
525+
it('should not double-add workflow:read when workflow:execute is checked and workflow:read is already checked', async () => {
526+
const { getByTestId } = renderComponent();
527+
528+
await waitFor(() =>
529+
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
530+
);
531+
532+
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
533+
const readCheckbox = getByTestId('scope-checkbox-workflow:read');
534+
535+
// workflow:read is already checked (it's in defaultScopes)
536+
expect(readCheckbox).toBeChecked();
537+
538+
await userEvent.click(executeCheckbox);
539+
expect(executeCheckbox).toBeChecked();
540+
// workflow:read should still be checked, not toggled off
541+
expect(readCheckbox).toBeChecked();
542+
});
543+
544+
it('should auto-uncheck workflow:execute when workflow:read is unchecked', async () => {
545+
const { getByTestId } = renderComponent();
546+
547+
await waitFor(() =>
548+
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
549+
);
550+
551+
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
552+
const readCheckbox = getByTestId('scope-checkbox-workflow:read');
553+
554+
// First enable workflow:execute
555+
await userEvent.click(executeCheckbox);
556+
expect(executeCheckbox).toBeChecked();
557+
expect(readCheckbox).toBeChecked();
558+
559+
// Uncheck workflow:read — should auto-uncheck workflow:execute
560+
await userEvent.click(readCheckbox);
561+
expect(readCheckbox).not.toBeChecked();
562+
expect(executeCheckbox).not.toBeChecked();
563+
});
564+
565+
it('should not affect workflow:execute when workflow:read is unchecked and workflow:execute was not checked', async () => {
566+
const { getByTestId } = renderComponent();
567+
568+
await waitFor(() =>
569+
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
570+
);
571+
572+
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
573+
const readCheckbox = getByTestId('scope-checkbox-workflow:read');
574+
575+
// workflow:execute is not checked; uncheck workflow:read
576+
expect(executeCheckbox).not.toBeChecked();
577+
await userEvent.click(readCheckbox);
578+
expect(readCheckbox).not.toBeChecked();
579+
expect(executeCheckbox).not.toBeChecked();
580+
});
581+
582+
it('should NOT auto-toggle workflow:execute when workflow:update is toggled', async () => {
583+
const { getByTestId } = renderComponent();
584+
585+
await waitFor(() =>
586+
expect(getByTestId('scope-checkbox-workflow:update')).toBeInTheDocument(),
587+
);
588+
589+
const updateCheckbox = getByTestId('scope-checkbox-workflow:update');
590+
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
591+
592+
expect(executeCheckbox).not.toBeChecked();
593+
594+
await userEvent.click(updateCheckbox);
595+
expect(updateCheckbox).toBeChecked();
596+
// workflow:execute must remain unchanged
597+
expect(executeCheckbox).not.toBeChecked();
598+
599+
await userEvent.click(updateCheckbox);
600+
expect(updateCheckbox).not.toBeChecked();
601+
expect(executeCheckbox).not.toBeChecked();
602+
});
603+
});
604+
495605
describe('External Secrets Scopes', () => {
496606
it('should not render externalSecretsProvider scope type when roleBasedAccess is off', () => {
497607
const { queryByText } = renderComponent();

packages/frontend/editor-ui/src/features/project-roles/ProjectRoleView.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ const scopes = SCOPES;
140140
141141
function toggleScope(scope: string) {
142142
const index = form.value.scopes.indexOf(scope);
143+
const isBeingAdded = index === -1;
144+
143145
if (index !== -1) {
144146
form.value.scopes.splice(index, 1);
145147
} else {
@@ -155,8 +157,17 @@ function toggleScope(scope: string) {
155157
toggleScope(scope.replace(':read', ':list'));
156158
}
157159
158-
if (scope === 'workflow:update') {
159-
toggleScope('workflow:execute');
160+
// Dependency: workflow:execute requires workflow:read
161+
if (scope === 'workflow:execute' && isBeingAdded) {
162+
if (!form.value.scopes.includes('workflow:read')) {
163+
toggleScope('workflow:read');
164+
}
165+
}
166+
167+
if (scope === 'workflow:read' && !isBeingAdded) {
168+
if (form.value.scopes.includes('workflow:execute')) {
169+
toggleScope('workflow:execute');
170+
}
160171
}
161172
}
162173

packages/frontend/editor-ui/src/features/project-roles/projectRoleScopes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const UI_OPERATIONS = {
1919
folder: ['read', 'update', 'create', 'move', 'delete'],
2020
workflow: [
2121
'read',
22+
'execute',
2223
'update',
2324
'create',
2425
'publish',

0 commit comments

Comments
 (0)