Skip to content

Commit 849a6cb

Browse files
committed
refactor: centralize role/route constants and derive role maps from metadata
1 parent a48f0d0 commit 849a6cb

21 files changed

Lines changed: 110 additions & 163 deletions

src/authz-module/audit-user/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@openedx/paragon';
1010
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
1111
import {
12-
AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE,
12+
ROUTES, TABLE_DEFAULT_PAGE_SIZE,
1313
} from '@src/authz-module/constants';
1414
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
1515
import { useNavigate, useParams } from 'react-router-dom';
@@ -81,7 +81,7 @@ const AuditUserPage = () => {
8181
if (!user && !isLoadingUser) {
8282
// @ts-ignore
8383
if (!isErrorUser || errorUser?.customAttributes?.httpErrorStatus === 404) {
84-
navigate(AUTHZ_HOME_PATH);
84+
navigate(ROUTES.HOME_PATH);
8585
}
8686
}
8787
}, [user, isLoadingUser, navigate, isErrorUser, errorUser]);
@@ -96,7 +96,7 @@ const AuditUserPage = () => {
9696
const navLinks = useMemo(() => [
9797
{
9898
label: formatMessage(baseMessages['authz.management.home.nav.link']),
99-
to: AUTHZ_HOME_PATH,
99+
to: ROUTES.HOME_PATH,
100100
},
101101
], [formatMessage]);
102102

@@ -202,7 +202,7 @@ const AuditUserPage = () => {
202202
});
203203
handleCloseConfirmDeletionModal();
204204
if (remainingRolesCount === 0) {
205-
navigate(AUTHZ_HOME_PATH);
205+
navigate(ROUTES.HOME_PATH);
206206
}
207207
},
208208
onError: (error, retryVariables) => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const mockRoles: Role[] = [
1010
userCount: 0,
1111
permissions: [],
1212
role: '',
13-
contextType: '',
13+
contextType: 'course',
1414
scope: '',
1515
},
1616
{
@@ -19,7 +19,7 @@ const mockRoles: Role[] = [
1919
userCount: 0,
2020
permissions: [],
2121
role: '',
22-
contextType: '',
22+
contextType: 'course',
2323
scope: '',
2424
},
2525
{
@@ -28,7 +28,7 @@ const mockRoles: Role[] = [
2828
userCount: 0,
2929
permissions: [],
3030
role: '',
31-
contextType: '',
31+
contextType: 'course',
3232
scope: '',
3333
},
3434
];

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ describe('RenderAdminRole', () => {
2727
expect(container.querySelector('.mb-0')).toBeInTheDocument();
2828
});
2929

30-
it('displays admin message for roles containing admin', () => {
31-
renderWrapper(<RenderAdminRole role={adminRole} />);
30+
it('displays admin message for superuser role', () => {
31+
renderWrapper(<RenderAdminRole role={superuserRole} />);
3232
expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument();
3333
});
3434

35-
it('displays staff message for superuser role', () => {
36-
renderWrapper(<RenderAdminRole role={superuserRole} />);
35+
it('displays staff message for non-django admin roles', () => {
36+
renderWrapper(<RenderAdminRole role={adminRole} />);
3737
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
3838
});
3939

@@ -57,9 +57,9 @@ describe('RenderAdminRole', () => {
5757
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
5858
});
5959

60-
it('displays admin message for mixed case admin role', () => {
60+
it('displays staff message for mixed case admin role', () => {
6161
renderWrapper(<RenderAdminRole role={mixedCaseAdminRole} />);
62-
expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument();
62+
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
6363
});
6464

6565
it('displays staff message for regular role without admin', () => {

src/authz-module/components/RenderAdminRole.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { SUPERUSER_ROLE } from '@src/authz-module/constants';
23
import messages from '@src/authz-module/audit-user/messages';
34

45
interface RenderAdminRoleProps {
@@ -7,8 +8,7 @@ interface RenderAdminRoleProps {
78

89
const RenderAdminRole = ({ role }: RenderAdminRoleProps) => {
910
const intl = useIntl();
10-
// Determine which message to show based on role
11-
const messageKey = role?.toLowerCase().includes('admin')
11+
const messageKey = role === SUPERUSER_ROLE
1212
? 'authz.user.table.permissions.role.admin'
1313
: 'authz.user.table.permissions.role.staff';
1414

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('TableCells Components', () => {
260260
const viewButton = screen.getByRole('button', { name: /view/i });
261261
await user.click(viewButton);
262262

263-
expect(mockNavigate).toHaveBeenCalledWith('/authz/user/user+with@special.chars');
263+
expect(mockNavigate).toHaveBeenCalledWith(`/authz/user/${encodeURIComponent('user+with@special.chars')}`);
264264
});
265265
});
266266

src/authz-module/components/TableCells.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { UserRoleWithPermissions, RoleToDelete } from '@src/types';
99
import { useNavigate } from 'react-router-dom';
1010
import { useContext, useMemo } from 'react';
1111
import {
12-
ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL,
12+
ADMIN_ROLES, buildUserPath, DJANGO_MANAGED_ROLES, getScopeContextType,
13+
MAP_ROLE_KEY_TO_LABEL, SUPERUSER_ROLE,
1314
} from '@src/authz-module/constants';
1415
import {
1516
Icon, IconButton, OverlayTrigger, Tooltip, DataTableContext,
@@ -62,7 +63,7 @@ const NameCell = ({ row }: CellProps) => {
6263
const ViewActionCell = ({ row }: CellProps) => {
6364
const { formatMessage } = useIntl();
6465
const navigate = useNavigate();
65-
const viewPath = `/authz/user/${row.original.username}`;
66+
const viewPath = buildUserPath(row.original.username ?? '');
6667
return (
6768
<IconButton
6869
src={RemoveRedEye}
@@ -92,7 +93,7 @@ const ScopeCell = ({ row }: CellProps) => {
9293
iconSrc: RESOURCE_ICONS.GLOBAL,
9394
};
9495
}
95-
const scopeIcon = row.original.role?.startsWith('lib') ? RESOURCE_ICONS.LIBRARY : RESOURCE_ICONS.COURSE;
96+
const scopeIcon = getScopeContextType(row.original.scope) === 'library' ? RESOURCE_ICONS.LIBRARY : RESOURCE_ICONS.COURSE;
9697
return {
9798
scopeText: row.original.scope,
9899
iconSrc: scopeIcon,
@@ -125,7 +126,7 @@ const PermissionsCell = ({ row }: CellProps) => {
125126
{ isDjangoRole
126127
? formatMessage(
127128
messages['authz.user.table.permissions.access.label'],
128-
{ accessType: role === 'django.superuser' ? 'total' : 'partial' },
129+
{ accessType: role === SUPERUSER_ROLE ? 'total' : 'partial' },
129130
)
130131
: formatMessage(messages['authz.user.table.permissions.available.count'], { count })}
131132
</span>
Lines changed: 35 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,44 @@
11
import type { IntlShape } from '@edx/frontend-platform/i18n';
22
import { Language, LibraryBooks, School } from '@openedx/paragon/icons';
3+
import { allRolesMetadata } from '@src/authz-module/roles-permissions';
4+
import { GLOBAL_STAFF_ROLE, MAP_ROLE_KEY_TO_LABEL, SUPERUSER_ROLE } from '@src/authz-module/constants';
35
import messages from './messages';
46

5-
export const getRolesFiltersOptions = (intl: IntlShape) => [
6-
{
7-
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
8-
groupIcon: Language,
9-
displayName: 'Super Admin',
10-
value: 'super_admin',
11-
},
12-
{
13-
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
14-
groupIcon: Language,
15-
displayName: 'Global Staff',
16-
value: 'global_staff',
17-
},
18-
19-
{
20-
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
21-
groupIcon: School,
22-
displayName: 'Course Admin',
23-
value: 'course_admin',
24-
},
25-
{
26-
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
27-
groupIcon: School,
28-
displayName: 'Course Staff',
29-
value: 'course_staff',
30-
},
31-
{
32-
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
33-
groupIcon: School,
34-
displayName: 'Course Editor',
35-
value: 'course_editor',
36-
},
37-
{
38-
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
39-
groupIcon: School,
40-
displayName: 'Course Auditor',
41-
value: 'course_auditor',
42-
},
43-
44-
{
45-
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
46-
groupIcon: LibraryBooks,
47-
displayName: 'Library Admin',
48-
value: 'library_admin',
49-
},
50-
{
51-
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
52-
groupIcon: LibraryBooks,
53-
displayName: 'Library Author',
54-
value: 'library_author',
55-
},
56-
{
57-
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
58-
groupIcon: LibraryBooks,
59-
displayName: 'Library Contributor',
60-
value: 'library_contributor',
61-
},
62-
{
63-
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
64-
groupIcon: LibraryBooks,
65-
displayName: 'Library User',
66-
value: 'library_user',
67-
},
68-
];
69-
707
export const RESOURCE_ICONS = {
718
COURSE: School,
729
LIBRARY: LibraryBooks,
7310
GLOBAL: Language,
7411
};
12+
13+
// The API expects the underscore format when roles are sent as filter values,
14+
// while role data received from the API uses the dotted format (e.g. django.superuser).
15+
const GLOBAL_ROLE_FILTER_OPTIONS = [
16+
{ value: 'super_admin', displayName: MAP_ROLE_KEY_TO_LABEL[SUPERUSER_ROLE] },
17+
{ value: 'global_staff', displayName: MAP_ROLE_KEY_TO_LABEL[GLOBAL_STAFF_ROLE] },
18+
];
19+
20+
export const getRolesFiltersOptions = (intl: IntlShape) => {
21+
const globalGroup = {
22+
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
23+
groupIcon: RESOURCE_ICONS.GLOBAL,
24+
};
25+
const contextGroups = {
26+
course: {
27+
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
28+
groupIcon: RESOURCE_ICONS.COURSE,
29+
},
30+
library: {
31+
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
32+
groupIcon: RESOURCE_ICONS.LIBRARY,
33+
},
34+
};
35+
36+
return [
37+
...GLOBAL_ROLE_FILTER_OPTIONS.map((role) => ({ ...globalGroup, ...role })),
38+
...allRolesMetadata.map((meta) => ({
39+
...contextGroups[meta.contextType],
40+
displayName: meta.name,
41+
value: meta.role,
42+
})),
43+
];
44+
};

src/authz-module/constants.ts

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
const ORG_AGGREGATE_SCOPE_BUILDERS = {
1+
import type { ContextType } from '@src/types';
2+
import { allRolesMetadata } from './roles-permissions';
3+
4+
const ORG_AGGREGATE_SCOPE_BUILDERS: Record<ContextType, (orgSlug: string) => string> = {
25
course: (orgSlug: string) => `course-v1:${orgSlug}+*`,
36
library: (orgSlug: string) => `lib:${orgSlug}:*`,
47
};
@@ -9,21 +12,19 @@ export const getOrgAggregateScopeKey = (contextType: string, orgSlug: string): s
912
return builder(orgSlug);
1013
};
1114

15+
export const getScopeContextType = (scope: string): ContextType => (scope.startsWith('lib') ? 'library' : 'course');
16+
1217
export const DEFAULT_TOAST_DELAY = 5000;
1318
export const RETRY_TOAST_DELAY = 120_000; // 2 minutes
14-
export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
15-
username: 'skeleton',
16-
name: '',
17-
email: '',
18-
roles: [],
19-
}));
2019

2120
export const ROUTES = {
2221
HOME_PATH: '/authz',
2322
AUDIT_USER_PATH: '/user/:username',
2423
ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
2524
};
2625

26+
export const buildUserPath = (username: string) => `${ROUTES.HOME_PATH}${ROUTES.AUDIT_USER_PATH.replace(':username', encodeURIComponent(username))}`;
27+
2728
export const buildWizardPath = (options?: { users?: string; from?: string }) => {
2829
const base = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
2930
if (!options) { return base; }
@@ -34,42 +35,20 @@ export const buildWizardPath = (options?: { users?: string; from?: string }) =>
3435
return query ? `${base}?${query}` : base;
3536
};
3637

37-
export enum RoleOperationErrorStatus {
38-
USER_NOT_FOUND = 'user_not_found',
39-
USER_ALREADY_HAS_ROLE = 'user_already_has_role',
40-
USER_DOES_NOT_HAVE_ROLE = 'user_does_not_have_role',
41-
ROLE_ASSIGNMENT_ERROR = 'role_assignment_error',
42-
ROLE_REMOVAL_ERROR = 'role_removal_error',
43-
}
44-
4538
export const MAX_TABLE_FILTERS_APPLIED = 10;
4639

47-
export const AUTHZ_HOME_PATH = '/authz';
40+
// Role data received from the API uses the dotted format for Django-managed roles.
41+
export const SUPERUSER_ROLE = 'django.superuser';
42+
export const GLOBAL_STAFF_ROLE = 'django.globalstaff';
43+
export const DJANGO_MANAGED_ROLES = [SUPERUSER_ROLE, GLOBAL_STAFF_ROLE];
4844

4945
export const MAP_ROLE_KEY_TO_LABEL: Record<string, string> = {
50-
library_admin: 'Library Admin',
51-
library_author: 'Library Author',
52-
library_contributor: 'Library Contributor',
53-
library_user: 'Library User',
54-
course_admin: 'Course Admin',
55-
course_staff: 'Course Staff',
56-
course_editor: 'Course Editor',
57-
course_auditor: 'Course Auditor',
58-
'django.superuser': 'Super Admin',
59-
'django.globalstaff': 'Global Staff',
46+
...Object.fromEntries(allRolesMetadata.map((meta) => [meta.role, meta.name])),
47+
[SUPERUSER_ROLE]: 'Super Admin',
48+
[GLOBAL_STAFF_ROLE]: 'Global Staff',
6049
};
6150

62-
export const DJANGO_MANAGED_ROLES = ['django.superuser', 'django.globalstaff'];
63-
6451
export const TABLE_DEFAULT_PAGE_SIZE = 10;
6552

6653
export const DEFAULT_FILTER_PAGE_SIZE = 5;
6754
export const ADMIN_ROLES = ['course_admin', 'library_admin'];
68-
69-
// Resource Type Definitions
70-
export const RESOURCE_TYPES = {
71-
LIBRARY: 'library',
72-
COURSE: 'course',
73-
} as const;
74-
75-
export type ResourceType = typeof RESOURCE_TYPES[keyof typeof RESOURCE_TYPES];

src/authz-module/hooks/useQuerySettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useState } from 'react';
22
import { QuerySettings } from '@src/authz-module/data/api';
3+
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
34

45
interface DataTableFilters {
56
pageSize: number;
@@ -32,7 +33,7 @@ export const useQuerySettings = (
3233
scopes: null,
3334
organizations: null,
3435
search: null,
35-
pageSize: 10,
36+
pageSize: TABLE_DEFAULT_PAGE_SIZE,
3637
pageIndex: 0,
3738
order: null,
3839
sortBy: null,
@@ -49,7 +50,7 @@ export const useQuerySettings = (
4950
const scopesFilter = tableFilters.filters?.find((filter) => filter.id === 'scope')?.value?.join(',') ?? '';
5051

5152
// Extract pagination
52-
const { pageSize = 10, pageIndex = 0 } = tableFilters;
53+
const { pageSize = TABLE_DEFAULT_PAGE_SIZE, pageIndex = 0 } = tableFilters;
5354

5455
// Extract and convert sorting
5556
let sortByOption = '';

src/authz-module/role-assignation-wizard/AssignRoleWizard.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,11 @@ import { RoleMetadata } from '@src/types';
1010
import { useToastManager } from '@src/components/ToastManager/ToastManagerContext';
1111
import SelectUsersAndRoleStep from './components/SelectUsersAndRoleStep';
1212
import DefineApplicationScopeStep from './components/DefineApplicationScopeStep';
13-
import { libraryRolesMetadata } from '../roles-permissions/library/constants';
14-
import { courseRolesMetadata } from '../roles-permissions/course/constants';
13+
import { allRolesMetadata } from '../roles-permissions';
1514
import { useValidateUsers, useAssignTeamMembersRole } from '../data/hooks';
1615
import messages from './messages';
1716
import { formatRoleAssignmentError } from './utils';
1817

19-
const allRolesMetadata = [...courseRolesMetadata, ...libraryRolesMetadata];
20-
2118
const STEPS = {
2219
SELECT_USERS_AND_ROLE: 'select-users-and-role',
2320
DEFINE_APPLICATION_SCOPE: 'define-application-scope',

0 commit comments

Comments
 (0)