Skip to content

Commit bcb2376

Browse files
committed
feat: add skeleton and improve testing
1 parent a388d5d commit bcb2376

5 files changed

Lines changed: 114 additions & 45 deletions

File tree

src/authz-module/components/PermissionTable.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import { Check, Close } from "@openedx/paragon/icons";
2-
import { actionsDictionary } from "./RoleCard/constants";
3-
import { Icon } from "@openedx/paragon";
1+
import { Check, Close } from '@openedx/paragon/icons';
2+
import { Icon } from '@openedx/paragon';
3+
import { Role } from '@src/types';
4+
import { PermissionMatrix } from '@src/authz-module/libraries-manager/utils';
5+
import { actionsDictionary } from './RoleCard/constants';
46

5-
const PermissionTable = ({ permissionsTable, roles }) => (
6-
<table className="pgn__data-table bg-light-100">
7+
type PermissionTableProps = {
8+
roles: Role[];
9+
permissionsTable: PermissionMatrix;
10+
};
11+
12+
const PermissionTable = ({ permissionsTable, roles }: PermissionTableProps) => (
13+
<table className="pgn__data-table bg-light-100" data-testid="permissions-matrix">
714
<thead>
815
<tr>
9-
<th className='bg-light-100'></th>
16+
<th className="bg-light-100" aria-hidden="true" />
1017
{roles.map(role => (
1118
<th key={role.name} className="text-center bg-light-100 py-3">{role.name}</th>
1219
))}
@@ -22,9 +29,9 @@ const PermissionTable = ({ permissionsTable, roles }) => (
2229
</tr>
2330
{
2431
resourceGroup.permissions.map(permission => (
25-
<tr key={permission.key} className='border-top'>
26-
<td className="text-start d-flex align-items-center small px-4 py-3">{
27-
<Icon className="d-inline-block mr-2" size="sm" src={actionsDictionary[permission.actionKey]} />}
32+
<tr key={permission.key} className="border-top">
33+
<td className="text-start d-flex align-items-center small px-4 py-3">
34+
<Icon className="d-inline-block mr-2" size="sm" src={actionsDictionary[permission.actionKey]} />
2835
{permission.label}
2936
</td>
3037
{roles.map(role => (
@@ -41,4 +48,4 @@ const PermissionTable = ({ permissionsTable, roles }) => (
4148
</table>
4249
);
4350

44-
export default PermissionTable;
51+
export default PermissionTable;

src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, screen } from '@testing-library/react';
1+
import { fireEvent, screen, within } from '@testing-library/react';
22
import { renderWrapper } from '@src/setupTest';
33
import { initializeMockApp } from '@edx/frontend-platform/testing';
44
import { useLibrary } from '@src/authz-module/data/hooks';
@@ -57,9 +57,9 @@ describe('LibrariesTeamManager', () => {
5757
],
5858
permissions: [
5959
{ key: 'view_library', label: 'view', resource: 'library' },
60-
{ key: 'edit_library', name: 'edit', resource: 'library' },
60+
{ key: 'edit_library', label: 'edit', resource: 'library' },
6161
],
62-
resources: [{ key: 'library', displayName: 'Library' }],
62+
resources: [{ key: 'library', label: 'Library' }],
6363
canManageTeam: true,
6464
});
6565

@@ -95,10 +95,26 @@ describe('LibrariesTeamManager', () => {
9595
fireEvent.click(rolesTab);
9696

9797
const roleCards = await screen.findAllByTestId('role-card');
98-
99-
expect(roleCards.length).toBeGreaterThan(0);
100-
expect(screen.getByText('Instructor')).toBeInTheDocument();
98+
const rolesScope = within(roleCards[0]);
99+
expect(roleCards.length).toBe(1);
100+
expect(rolesScope.getByText('Instructor')).toBeInTheDocument();
101101
expect(screen.getByText(/Can manage content/i)).toBeInTheDocument();
102102
expect(screen.getByText(/1 permissions/i)).toBeInTheDocument();
103103
});
104+
105+
it('renders role matrix when "Permissions" tab is selected', async () => {
106+
renderWrapper(<LibrariesTeamManager />);
107+
108+
// Click on "Permissions" tab
109+
const permissionsTab = await screen.findByRole('tab', { name: /permissions/i });
110+
fireEvent.click(permissionsTab);
111+
112+
const permissionsMatrix = await screen.findByTestId('permissions-matrix');
113+
const metrixScope = within(permissionsMatrix);
114+
115+
expect(metrixScope.getByText('Instructor')).toBeInTheDocument();
116+
expect(metrixScope.getByText('Library')).toBeInTheDocument();
117+
expect(metrixScope.getByText('edit')).toBeInTheDocument();
118+
expect(metrixScope.getByText('view')).toBeInTheDocument();
119+
});
104120
});

src/authz-module/libraries-manager/LibrariesTeamManager.tsx

Lines changed: 8 additions & 5 deletions
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';
3-
import { Container, Tab, Tabs } from '@openedx/paragon';
3+
import {
4+
Container, Skeleton, Tab, Tabs,
5+
} from '@openedx/paragon';
46
import { useLibrary } from '@src/authz-module/data/hooks';
57
import TeamTable from './components/TeamTable';
68
import AuthZLayout from '../components/AuthZLayout';
@@ -27,9 +29,9 @@ const LibrariesTeamManager = () => {
2729
})), [roles, permissions, resources, intl]);
2830

2931
const permissionsTable = useMemo(() => {
30-
if (!roles || !permissions || !resources) return [];
32+
if (!roles || !permissions || !resources) { return []; }
3133
return buildPermissionMatrix(roles, permissions, resources, intl);
32-
}, [roles, permissions, resources]);
34+
}, [roles, permissions, resources, intl]);
3335

3436
return (
3537
<div className="authz-libraries">
@@ -64,12 +66,13 @@ const LibrariesTeamManager = () => {
6466
</Tab>
6567
<Tab eventKey="permissions" title={intl.formatMessage(messages['library.authz.tabs.permissions'])}>
6668
<Container className="p-5 container-mw-lg">
67-
<PermissionTable permissionsTable={permissionsTable} roles={libraryRoles} />
69+
{!permissionsTable ? <Skeleton count={2} height={200} />
70+
: <PermissionTable permissionsTable={permissionsTable} roles={roles} />}
6871
</Container>
6972
</Tab>
7073
</Tabs>
7174
</AuthZLayout>
72-
</div >
75+
</div>
7376
);
7477
};
7578

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { buildPermissionsByRoleMatrix } from './utils';
1+
import { buildPermissionMatrix, buildPermissionsByRoleMatrix } from './utils';
2+
3+
const intl = { formatMessage: jest.fn((msg: any) => msg.defaultMessage) };
4+
const permissions = [
5+
{ key: 'create_library', resource: 'library', label: 'Create Library' },
6+
{ key: 'edit_library', resource: 'library', label: 'Edit Library' },
7+
];
8+
const resources = [
9+
{ key: 'library', label: 'Library', description: '' },
10+
];
211

312
describe('buildPermissionsByRoleMatrix', () => {
413
it('returns permissions matrix for given role', () => {
514
const rolePermissions = ['create_library'];
6-
const permissions = [
7-
{ key: 'create_library', resource: 'library', label: 'Create Library' },
8-
{ key: 'edit_library', resource: 'library', label: 'Edit Library' },
9-
];
10-
const resources = [
11-
{ key: 'library', label: 'Library', description: '' },
12-
];
1315

14-
const intl = { formatMessage: jest.fn((msg: any) => msg.defaultMessage) };
1516
const matrix = buildPermissionsByRoleMatrix({
1617
rolePermissions, permissions, resources, intl,
1718
}) as Array<{ key: string; actions: Array<{ disabled: boolean }> }>;
@@ -21,3 +22,49 @@ describe('buildPermissionsByRoleMatrix', () => {
2122
expect(matrix[0].actions[1].disabled).toBe(true);
2223
});
2324
});
25+
26+
describe('buildPermissionsByRoleMatrix', () => {
27+
it('should build permission matrix grouped by resources with role access mapped', () => {
28+
const roles = [
29+
{
30+
name: 'admin', permissions: ['create_library', 'edit_library'], userCount: 2, role: 'admin', description: '',
31+
},
32+
{
33+
name: 'editor', permissions: ['edit_library'], userCount: 2, role: 'editor', description: '',
34+
},
35+
{
36+
name: 'guest', permissions: [], userCount: 2, role: 'guest', description: '',
37+
},
38+
];
39+
const matrix = buildPermissionMatrix(roles, permissions, resources, intl);
40+
41+
expect(matrix).toEqual([
42+
{
43+
resource: 'library',
44+
resourceLabel: 'Library',
45+
permissions: [
46+
{
47+
key: 'create_library',
48+
actionKey: 'create',
49+
label: 'Create Library',
50+
roles: {
51+
admin: true,
52+
editor: false,
53+
guest: false,
54+
},
55+
},
56+
{
57+
key: 'edit_library',
58+
actionKey: 'edit',
59+
label: 'Edit Library',
60+
roles: {
61+
admin: true,
62+
editor: true,
63+
guest: false,
64+
},
65+
},
66+
],
67+
},
68+
]);
69+
});
70+
});

src/authz-module/libraries-manager/utils.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IntlShape } from '@edx/frontend-platform/i18n';
22
import { actionKeys } from '@src/authz-module/components/RoleCard/constants';
3+
import { PermissionMetadata, ResourceMetadata, Role } from '@src/types';
34
import actionMessages from '../components/RoleCard/messages';
4-
import { PermissionMetadata, ResourceMetadata, Role } from 'types';
55

66
/**
77
* Derives the localized label and action key for a given permission.
@@ -18,13 +18,13 @@ import { PermissionMetadata, ResourceMetadata, Role } from 'types';
1818
*
1919
* @returns An object containing:
2020
* - `label`: The human-readable, localized label for the permission.
21-
* - `actionKey`: A string representing icon to be displayed (e.g., `'Read'`, `'Edit'`), or `undefined` if not matched.
21+
* - `actionKey`: A string representing icon to be displayed (e.g., `'Read'`, `'Edit'`), or '' if not matched.
2222
*/
2323
function getPermissionMetadata(
2424
permission: PermissionMetadata,
2525
intl: IntlShape,
26-
): { label: string; actionKey: string | undefined } {
27-
const actionKey = actionKeys.find(action => permission.key.includes(action));
26+
): { label: string; actionKey: string } {
27+
const actionKey = actionKeys.find(action => permission.key.includes(action)) || '';
2828
let messageKey = `authz.permissions.actions.${actionKey}`;
2929
let messageResource = '';
3030

@@ -48,13 +48,12 @@ const buildPermissionsByRoleMatrix = ({
4848
const allowedPermissions = new Set(rolePermissions);
4949

5050
permissions.forEach((permission) => {
51-
const resourceLabel =
52-
resources.find((r) => r.key === permission.resource)?.label ||
53-
permission.resource;
51+
const resourceLabel = resources.find((r) => r.key === permission.resource)?.label
52+
|| permission.resource;
5453

5554
const { label, actionKey } = getPermissionMetadata(permission, intl);
5655

57-
if (!actionKey) return; // Skip unknown actions
56+
if (!actionKey) { return; } // Skip unknown actions
5857

5958
// Initialize resource group if not already present
6059
if (!permissionsMatrix[permission.resource]) {
@@ -75,15 +74,13 @@ const buildPermissionsByRoleMatrix = ({
7574
return Object.values(permissionsMatrix);
7675
};
7776

78-
79-
80-
81-
type PermissionMatrix = {
77+
export type PermissionMatrix = {
8278
resource: string;
8379
resourceLabel: string;
8480
permissions: {
8581
key: string;
8682
label: string;
83+
actionKey: string;
8784
roles: Record<string, boolean>;
8885
}[];
8986
}[];
@@ -109,12 +106,12 @@ export function buildPermissionMatrix(
109106
intl: IntlShape,
110107
): PermissionMatrix {
111108
const permissionsByResource = permissions.reduce<Record<string, PermissionMetadata[]>>((acc, perm) => {
112-
if (!acc[perm.resource]) acc[perm.resource] = [];
109+
if (!acc[perm.resource]) { acc[perm.resource] = []; }
113110
acc[perm.resource].push(perm);
114111
return acc;
115112
}, {});
116113

117-
const matrix: PermissionMatrix = resources.map(resource => {
114+
const matrix = resources.map(resource => {
118115
const resourcePermissions = permissionsByResource[resource.key] || [];
119116

120117
const permissionRows = resourcePermissions.map(permission => {
@@ -144,5 +141,4 @@ export function buildPermissionMatrix(
144141
return matrix;
145142
}
146143

147-
148144
export { buildPermissionsByRoleMatrix };

0 commit comments

Comments
 (0)