Skip to content

Commit 2103a7a

Browse files
fix: app actions ignoring role filters and i18n translation (RocketChat#39868)
1 parent 5183daa commit 2103a7a

File tree

4 files changed

+220
-2
lines changed

4 files changed

+220
-2
lines changed

.changeset/neat-bananas-behave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Fixes app actions ignoring role filters and i18n translation
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui';
2+
import { UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui';
3+
import { mockAppRoot } from '@rocket.chat/mock-providers';
4+
import { renderHook } from '@testing-library/react';
5+
6+
import { useApplyButtonAuthFilter } from './useApplyButtonFilters';
7+
8+
describe('useApplyButtonAuthFilter', () => {
9+
describe('Role-based filtering', () => {
10+
it('should filter button when user does not have required role (hasAllRoles)', () => {
11+
const button: IUIActionButton = {
12+
appId: 'test-app',
13+
actionId: 'test-action',
14+
labelI18n: 'test_label',
15+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
16+
when: {
17+
hasAllRoles: ['admin'],
18+
},
19+
};
20+
21+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
22+
wrapper: mockAppRoot()
23+
.withJohnDoe({ roles: ['user'] })
24+
.build(),
25+
});
26+
27+
expect(result.current(button)).toBe(false);
28+
});
29+
30+
it('should show button when user has required role (hasAllRoles)', () => {
31+
const button: IUIActionButton = {
32+
appId: 'test-app',
33+
actionId: 'test-action',
34+
labelI18n: 'test_label',
35+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
36+
when: {
37+
hasAllRoles: ['admin'],
38+
},
39+
};
40+
41+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
42+
wrapper: mockAppRoot().withJohnDoe().withRole('admin').build(),
43+
});
44+
45+
expect(result.current(button)).toBe(true);
46+
});
47+
48+
it('should filter button when user does not have any required role (hasOneRole)', () => {
49+
const button: IUIActionButton = {
50+
appId: 'test-app',
51+
actionId: 'test-action',
52+
labelI18n: 'test_label',
53+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
54+
when: {
55+
hasOneRole: ['admin', 'moderator'],
56+
},
57+
};
58+
59+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
60+
wrapper: mockAppRoot()
61+
.withJohnDoe({ roles: ['user'] })
62+
.build(),
63+
});
64+
65+
expect(result.current(button)).toBe(false);
66+
});
67+
68+
it('should show button when user has at least one of the required roles (hasOneRole)', () => {
69+
const button: IUIActionButton = {
70+
appId: 'test-app',
71+
actionId: 'test-action',
72+
labelI18n: 'test_label',
73+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
74+
when: {
75+
hasOneRole: ['admin', 'moderator'],
76+
},
77+
};
78+
79+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
80+
wrapper: mockAppRoot().withJohnDoe().withRole('moderator').build(),
81+
});
82+
83+
expect(result.current(button)).toBe(true);
84+
});
85+
86+
it('should show button when no role filter is specified', () => {
87+
const button: IUIActionButton = {
88+
appId: 'test-app',
89+
actionId: 'test-action',
90+
labelI18n: 'test_label',
91+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
92+
};
93+
94+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
95+
wrapper: mockAppRoot()
96+
.withJohnDoe({ roles: ['user'] })
97+
.build(),
98+
});
99+
100+
expect(result.current(button)).toBe(true);
101+
});
102+
103+
it('should filter button when user is not logged in and role is required (hasAllRoles)', () => {
104+
const button: IUIActionButton = {
105+
appId: 'test-app',
106+
actionId: 'test-action',
107+
labelI18n: 'test_label',
108+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
109+
when: {
110+
hasAllRoles: ['admin'],
111+
},
112+
};
113+
114+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
115+
wrapper: mockAppRoot().withAnonymous().build(),
116+
});
117+
118+
expect(result.current(button)).toBe(false);
119+
});
120+
});
121+
122+
describe('Permission-based filtering', () => {
123+
it('should filter button when user does not have required permission (hasAllPermissions)', () => {
124+
const button: IUIActionButton = {
125+
appId: 'test-app',
126+
actionId: 'test-action',
127+
labelI18n: 'test_label',
128+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
129+
when: {
130+
hasAllPermissions: ['manage-apps'],
131+
},
132+
};
133+
134+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
135+
wrapper: mockAppRoot().withJohnDoe().build(),
136+
});
137+
138+
expect(result.current(button)).toBe(false);
139+
});
140+
141+
it('should show button when user has required permission (hasAllPermissions)', () => {
142+
const button: IUIActionButton = {
143+
appId: 'test-app',
144+
actionId: 'test-action',
145+
labelI18n: 'test_label',
146+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
147+
when: {
148+
hasAllPermissions: ['manage-apps'],
149+
},
150+
};
151+
152+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
153+
wrapper: mockAppRoot().withJohnDoe().withPermission('manage-apps').build(),
154+
});
155+
156+
expect(result.current(button)).toBe(true);
157+
});
158+
159+
it('should show button when user has at least one of the required permissions (hasOnePermission)', () => {
160+
const button: IUIActionButton = {
161+
appId: 'test-app',
162+
actionId: 'test-action',
163+
labelI18n: 'test_label',
164+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
165+
when: {
166+
hasOnePermission: ['manage-apps', 'manage-users'],
167+
},
168+
};
169+
170+
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
171+
wrapper: mockAppRoot().withJohnDoe().withPermission('manage-apps').build(),
172+
});
173+
174+
expect(result.current(button)).toBe(true);
175+
});
176+
});
177+
178+
describe('Combined filters', () => {
179+
it('should apply both role and permission filters (AND logic)', () => {
180+
const button: IUIActionButton = {
181+
appId: 'test-app',
182+
actionId: 'test-action',
183+
labelI18n: 'test_label',
184+
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
185+
when: {
186+
hasAllRoles: ['admin'],
187+
hasAllPermissions: ['manage-apps'],
188+
},
189+
};
190+
191+
const { result: result1 } = renderHook(() => useApplyButtonAuthFilter(), {
192+
wrapper: mockAppRoot()
193+
.withJohnDoe({ roles: ['user'] })
194+
.build(),
195+
});
196+
expect(result1.current(button)).toBe(false);
197+
198+
const { result: result2 } = renderHook(() => useApplyButtonAuthFilter(), {
199+
wrapper: mockAppRoot()
200+
.withJohnDoe({ roles: ['user'] })
201+
.withPermission('manage-apps')
202+
.build(),
203+
});
204+
expect(result2.current(button)).toBe(false);
205+
206+
const { result: result3 } = renderHook(() => useApplyButtonAuthFilter(), {
207+
wrapper: mockAppRoot().withJohnDoe().withRole('admin').withPermission('manage-apps').build(),
208+
});
209+
expect(result3.current(button)).toBe(true);
210+
});
211+
});
212+
});

apps/meteor/client/hooks/useApplyButtonFilters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const useApplyButtonAuthFilter = (): ((button: IUIActionButton) => boolea
6666

6767
const hasAllPermissionsResult = hasAllPermissions ? queryAllPermissions(hasAllPermissions)[1]() : true;
6868
const hasOnePermissionResult = hasOnePermission ? queryAtLeastOnePermission(hasOnePermission)[1]() : true;
69-
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(role, room?._id)) : true;
69+
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(role, room?._id)[1]()) : true;
7070
const hasOneRoleResult = hasOneRole ? !!uid && hasOneRole.some((role) => queryRole(role, room?._id)[1]()) : true;
7171

7272
return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult;

apps/meteor/client/hooks/useUserDropdownAppsActionButtons.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
77
import { useAppActionButtons } from './useAppActionButtons';
88
import { useApplyButtonAuthFilter } from './useApplyButtonFilters';
99
import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError';
10+
import { Utilities } from '../../ee/lib/misc/Utilities';
1011
import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager';
1112

1213
export const useUserDropdownAppsActionButtons = () => {
@@ -25,7 +26,7 @@ export const useUserDropdownAppsActionButtons = () => {
2526
return {
2627
id: `${action.appId}_${action.actionId}`,
2728
// icon: action.icon as GenericMenuItemProps['icon'],
28-
content: action.labelI18n,
29+
content: t(Utilities.getI18nKeyForApp(action.labelI18n, action.appId)),
2930
onClick: () => {
3031
void actionManager
3132
.emitInteraction(action.appId, {

0 commit comments

Comments
 (0)