Skip to content

Commit 4916b12

Browse files
committed
fix: filter roles displays only the user scope related
1 parent 24e28e3 commit 4916b12

4 files changed

Lines changed: 119 additions & 10 deletions

File tree

src/authz-module/components/TableControlBar/RolesFilter.test.tsx

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
import { screen } from '@testing-library/react';
1+
import { screen, within } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
23
import { renderWrapper } from '@src/setupTest';
4+
import { useValidateUserPermissionsNonSuspense } from '@src/data/hooks';
5+
import { CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS } from '@src/authz-module/roles-permissions';
36
import RolesFilter from './RolesFilter';
47

8+
jest.mock('@src/data/hooks', () => ({
9+
useValidateUserPermissionsNonSuspense: jest.fn(),
10+
}));
11+
12+
const mockUsePermissions = useValidateUserPermissionsNonSuspense as jest.Mock;
13+
14+
const permissionsData = ({ library, course }: { library?: boolean; course?: boolean }) => [
15+
{ action: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, allowed: !!library },
16+
{ action: CONTENT_COURSE_PERMISSIONS.VIEW_COURSE_TEAM, allowed: !!course },
17+
];
18+
519
describe('RolesFilter', () => {
620
const defaultProps = {
721
filterButtonText: 'Roles',
@@ -10,11 +24,17 @@ describe('RolesFilter', () => {
1024
disabled: false,
1125
};
1226

27+
const openDropdown = async (user: ReturnType<typeof userEvent.setup>) => {
28+
await user.click(screen.getByRole('button', { name: /Roles/i }));
29+
return within(await screen.findByRole('group', { name: 'Roles' }));
30+
};
31+
1332
beforeEach(() => {
1433
jest.clearAllMocks();
34+
mockUsePermissions.mockReturnValue({ data: permissionsData({ library: true, course: true }) });
1535
});
1636

17-
it('renders without crashing', () => {
37+
it('renders the filter toggle', () => {
1838
renderWrapper(<RolesFilter {...defaultProps} />);
1939
expect(screen.getByText('Roles')).toBeInTheDocument();
2040
});
@@ -29,9 +49,52 @@ describe('RolesFilter', () => {
2949
expect(screen.getByText('Select Roles')).toBeInTheDocument();
3050
});
3151

32-
it('calls setFilter when filter changes', () => {
33-
const mockSetFilter = jest.fn();
34-
renderWrapper(<RolesFilter {...defaultProps} setFilter={mockSetFilter} />);
35-
expect(screen.getByText('Roles')).toBeInTheDocument();
52+
it('calls setFilter with the selected role when a role is checked', async () => {
53+
const user = userEvent.setup();
54+
const setFilter = jest.fn();
55+
renderWrapper(<RolesFilter {...defaultProps} setFilter={setFilter} />);
56+
const menu = await openDropdown(user);
57+
await user.click(menu.getByLabelText('Course Admin'));
58+
expect(setFilter).toHaveBeenCalledWith(
59+
['course_admin'],
60+
expect.objectContaining({ value: 'course_admin', displayName: 'Course Admin' }),
61+
);
62+
});
63+
64+
it('shows only library roles when the user can view the library scope only', async () => {
65+
const user = userEvent.setup();
66+
mockUsePermissions.mockReturnValue({ data: permissionsData({ library: true }) });
67+
renderWrapper(<RolesFilter {...defaultProps} />);
68+
const menu = await openDropdown(user);
69+
expect(menu.getByText('Libraries')).toBeInTheDocument();
70+
expect(menu.getByLabelText('Library Admin')).toBeInTheDocument();
71+
expect(menu.queryByText('Courses')).not.toBeInTheDocument();
72+
expect(menu.queryByLabelText('Course Admin')).not.toBeInTheDocument();
73+
});
74+
75+
it('shows both course and library roles when the user can view both scopes', async () => {
76+
const user = userEvent.setup();
77+
renderWrapper(<RolesFilter {...defaultProps} />);
78+
const menu = await openDropdown(user);
79+
expect(menu.getByText('Courses')).toBeInTheDocument();
80+
expect(menu.getByText('Libraries')).toBeInTheDocument();
81+
});
82+
83+
it('shows no role options when the user cannot view any scope', async () => {
84+
const user = userEvent.setup();
85+
mockUsePermissions.mockReturnValue({ data: permissionsData({}) });
86+
renderWrapper(<RolesFilter {...defaultProps} />);
87+
const menu = await openDropdown(user);
88+
expect(menu.queryByText('Courses')).not.toBeInTheDocument();
89+
expect(menu.queryByText('Libraries')).not.toBeInTheDocument();
90+
});
91+
92+
it('shows no role options while permissions are still loading', async () => {
93+
const user = userEvent.setup();
94+
mockUsePermissions.mockReturnValue({ data: undefined });
95+
renderWrapper(<RolesFilter {...defaultProps} />);
96+
const menu = await openDropdown(user);
97+
expect(menu.queryByText('Courses')).not.toBeInTheDocument();
98+
expect(menu.queryByText('Libraries')).not.toBeInTheDocument();
3699
});
37100
});

src/authz-module/components/TableControlBar/RolesFilter.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useMemo } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
33
import { Person } from '@openedx/paragon/icons';
4+
import { useValidateUserPermissionsNonSuspense } from '@src/data/hooks';
5+
import { CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS } from '@src/authz-module/roles-permissions';
46
import MultipleChoiceFilter from './MultipleChoiceFilter';
57
import { MultipleChoiceFilterProps } from './types';
68
import { getRolesFiltersOptions } from '../constants';
@@ -11,7 +13,27 @@ const RolesFilter = ({
1113
filterButtonText, filterValue, setFilter, disabled,
1214
}: RolesFilterProps) => {
1315
const intl = useIntl();
14-
const rolesOptions = useMemo(() => getRolesFiltersOptions(intl), [intl]);
16+
const { data: permissions } = useValidateUserPermissionsNonSuspense([
17+
{ action: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM },
18+
{ action: CONTENT_COURSE_PERMISSIONS.VIEW_COURSE_TEAM },
19+
]);
20+
21+
// Only show role groups for the domains the user can view. Global roles stay
22+
// hidden until a platform-wide permission is available to gate them on.
23+
const allowedContexts = useMemo(() => {
24+
const contexts = new Set<string>();
25+
permissions?.forEach((p) => {
26+
if (!p.allowed) { return; }
27+
if (p.action === CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM) { contexts.add('library'); }
28+
if (p.action === CONTENT_COURSE_PERMISSIONS.VIEW_COURSE_TEAM) { contexts.add('course'); }
29+
});
30+
return contexts;
31+
}, [permissions]);
32+
33+
const rolesOptions = useMemo(
34+
() => getRolesFiltersOptions(intl).filter((option) => allowedContexts.has(option.contextType)),
35+
[intl, allowedContexts],
36+
);
1537
return (
1638
<MultipleChoiceFilter
1739
filterButtonText={filterButtonText}

src/authz-module/components/TableControlBar/TableControlBar.test.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ jest.mock('@src/authz-module/data/hooks', () => ({
4343
useScopes: () => ({ data: { results: [] } }),
4444
}));
4545

46+
// RolesFilter validates view-team permissions to decide which role groups to show.
47+
// Grant both so the full course/library role set renders for these wiring tests.
48+
jest.mock('@src/data/hooks', () => {
49+
const { CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS } = jest.requireActual('@src/authz-module/roles-permissions');
50+
return {
51+
useValidateUserPermissionsNonSuspense: () => ({
52+
data: [
53+
{ action: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, allowed: true },
54+
{ action: CONTENT_COURSE_PERMISSIONS.VIEW_COURSE_TEAM, allowed: true },
55+
],
56+
}),
57+
};
58+
});
59+
4660
describe('TableControlBar', () => {
4761
const mockDataTableContext = {
4862
columns: mockColumns,
@@ -90,9 +104,9 @@ describe('TableControlBar', () => {
90104
const rolesButton = screen.getByText('Select Roles');
91105
expect(rolesButton).toBeInTheDocument();
92106
await user.click(rolesButton);
93-
const superAdminOption = screen.getByRole('checkbox', { name: /Super Admin/i });
94-
expect(superAdminOption).toBeInTheDocument();
95-
await user.click(superAdminOption);
107+
const courseAdminOption = screen.getByRole('checkbox', { name: /Course Admin/i });
108+
expect(courseAdminOption).toBeInTheDocument();
109+
await user.click(courseAdminOption);
96110
expect(contextWithRolesFilter.columns[0].setFilter).toHaveBeenCalled();
97111
});
98112

src/authz-module/components/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,62 +8,72 @@ export const getRolesFiltersOptions = (intl: IntlShape) => [
88
groupIcon: Language,
99
displayName: 'Super Admin',
1010
value: 'super_admin',
11+
contextType: 'global',
1112
},
1213
{
1314
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
1415
groupIcon: Language,
1516
displayName: 'Global Staff',
1617
value: 'global_staff',
18+
contextType: 'global',
1719
},
1820

1921
{
2022
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
2123
groupIcon: School,
2224
displayName: 'Course Admin',
2325
value: 'course_admin',
26+
contextType: 'course',
2427
},
2528
{
2629
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
2730
groupIcon: School,
2831
displayName: 'Course Staff',
2932
value: 'course_staff',
33+
contextType: 'course',
3034
},
3135
{
3236
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
3337
groupIcon: School,
3438
displayName: 'Course Editor',
3539
value: 'course_editor',
40+
contextType: 'course',
3641
},
3742
{
3843
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
3944
groupIcon: School,
4045
displayName: 'Course Auditor',
4146
value: 'course_auditor',
47+
contextType: 'course',
4248
},
4349

4450
{
4551
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
4652
groupIcon: LibraryBooks,
4753
displayName: 'Library Admin',
4854
value: 'library_admin',
55+
contextType: 'library',
4956
},
5057
{
5158
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
5259
groupIcon: LibraryBooks,
5360
displayName: 'Library Author',
5461
value: 'library_author',
62+
contextType: 'library',
5563
},
5664
{
5765
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
5866
groupIcon: LibraryBooks,
5967
displayName: 'Library Contributor',
6068
value: 'library_contributor',
69+
contextType: 'library',
6170
},
6271
{
6372
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
6473
groupIcon: LibraryBooks,
6574
displayName: 'Library User',
6675
value: 'library_user',
76+
contextType: 'library',
6777
},
6878
];
6979

0 commit comments

Comments
 (0)