Skip to content

Commit 215a1c4

Browse files
committed
feat: create the table team management table
1 parent 211f213 commit 215a1c4

3 files changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { ROUTES } from '@src/authz-module/constants';
4+
import { renderWrapper } from '@src/setupTest';
5+
import TeamTable from './TeamTable';
6+
import { useTeamMembers } from '../data/hooks';
7+
import { useLibraryAuthZ } from '../context';
8+
9+
const mockNavigate = jest.fn();
10+
jest.mock('react-router', () => ({
11+
...jest.requireActual('react-router'),
12+
useNavigate: () => mockNavigate,
13+
}));
14+
15+
jest.mock('../data/hooks', () => ({
16+
useTeamMembers: jest.fn(),
17+
}));
18+
19+
jest.mock('../context', () => ({
20+
useLibraryAuthZ: jest.fn(),
21+
}));
22+
23+
describe('TeamTable', () => {
24+
const mockTeamMembers = [
25+
{
26+
displayName: 'Alice',
27+
email: 'alice@example.com',
28+
roles: ['Admin', 'Editor'],
29+
username: 'alice',
30+
},
31+
{
32+
displayName: 'Bob',
33+
email: 'bob@example.com',
34+
roles: ['Viewer'],
35+
username: 'bob',
36+
},
37+
];
38+
39+
const mockAuthZ = {
40+
libraryId: 'lib:123',
41+
canManageTeam: true,
42+
username: 'alice',
43+
};
44+
45+
beforeEach(() => {
46+
jest.clearAllMocks();
47+
});
48+
49+
it('shows skeletons while loading', () => {
50+
(useTeamMembers as jest.Mock).mockReturnValue({
51+
data: null,
52+
isLoading: true,
53+
});
54+
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
55+
56+
renderWrapper(<TeamTable />);
57+
58+
const skeletons = screen.getAllByText('', { selector: '[aria-busy="true"]' });
59+
expect(skeletons.length).toBeGreaterThan(0);
60+
});
61+
62+
it('renders team member data after loading', () => {
63+
(useTeamMembers as jest.Mock).mockReturnValue({
64+
data: mockTeamMembers,
65+
isLoading: false,
66+
});
67+
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
68+
69+
renderWrapper(<TeamTable />);
70+
71+
expect(screen.getByText('Alice')).toBeInTheDocument();
72+
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
73+
expect(screen.getByText('Admin')).toBeInTheDocument();
74+
expect(screen.getByText('Editor')).toBeInTheDocument();
75+
76+
expect(screen.getByText('Bob')).toBeInTheDocument();
77+
expect(screen.getByText('bob@example.com')).toBeInTheDocument();
78+
expect(screen.getByText('Viewer')).toBeInTheDocument();
79+
});
80+
81+
it('renders Edit button only for users with than can manage team members (current user can not edit themselves)', async () => {
82+
(useTeamMembers as jest.Mock).mockReturnValue({
83+
data: mockTeamMembers,
84+
isLoading: false,
85+
});
86+
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
87+
88+
renderWrapper(<TeamTable />);
89+
90+
const editButtons = screen.queryAllByText('Edit');
91+
// Should not find Edit button for current user
92+
expect(editButtons).toHaveLength(1);
93+
94+
await userEvent.click(editButtons[0]);
95+
expect(mockNavigate).toHaveBeenCalledWith(
96+
`/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', 'alice')}`,
97+
);
98+
});
99+
100+
it('does not render Edit button if canManageTeam is false', () => {
101+
(useTeamMembers as jest.Mock).mockReturnValue({
102+
data: mockTeamMembers,
103+
isLoading: false,
104+
});
105+
(useLibraryAuthZ as jest.Mock).mockReturnValue({
106+
...mockAuthZ,
107+
canManageTeam: false,
108+
});
109+
110+
renderWrapper(<TeamTable />);
111+
112+
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
113+
});
114+
115+
it('does not render Edit button while loading', () => {
116+
(useTeamMembers as jest.Mock).mockReturnValue({
117+
data: null,
118+
isLoading: true,
119+
});
120+
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
121+
122+
renderWrapper(<TeamTable />);
123+
124+
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
125+
});
126+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useMemo } from 'react';
2+
import { useNavigate } from 'react-router';
3+
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import {
5+
DataTable, Button, Chip, Skeleton,
6+
} from '@openedx/paragon';
7+
import { Edit } from '@openedx/paragon/icons';
8+
import { ROUTES, TableCellValue, TeamMember } from '@src/authz-module/constants';
9+
import { useTeamMembers } from '../data/hooks';
10+
import { useLibraryAuthZ } from '../context';
11+
import messages from './messages';
12+
13+
const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
14+
username: 'skeleton',
15+
name: '',
16+
email: '',
17+
roles: [],
18+
}));
19+
20+
type CellProps = TableCellValue<TeamMember>;
21+
22+
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
23+
<Skeleton width="180px" />
24+
) : (
25+
row.original.email
26+
));
27+
28+
const NameCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
29+
<Skeleton width="180px" />
30+
) : (
31+
row.original.displayName
32+
));
33+
34+
const RolesCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
35+
<Skeleton width="80px" />
36+
) : (
37+
row.original.roles.map((role) => (
38+
<Chip key={`${row.original.username}-role-${role}`}>{role}</Chip>
39+
))
40+
));
41+
42+
const TeamTable = () => {
43+
const intl = useIntl();
44+
const { libraryId, canManageTeam, username } = useLibraryAuthZ();
45+
46+
// TODO: Display error in the notification system
47+
const {
48+
data: teamMembers, isLoading, isError
49+
} = useTeamMembers(libraryId);
50+
51+
const rows = isError ? [] : (teamMembers || SKELETON_ROWS);
52+
53+
const navigate = useNavigate();
54+
55+
const columns = useMemo(() => [
56+
{
57+
Header: intl.formatMessage(messages['library.authz.team.table.display.name']),
58+
accessor: 'displayName',
59+
Cell: NameCell,
60+
},
61+
{
62+
Header: intl.formatMessage(messages['library.authz.team.table.email']),
63+
accessor: 'email',
64+
Cell: EmailCell,
65+
},
66+
{
67+
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
68+
accessor: 'roles',
69+
Cell: RolesCell,
70+
},
71+
], [isLoading]);
72+
73+
return (
74+
<DataTable
75+
isPaginated
76+
data={rows}
77+
itemCount={rows?.length}
78+
additionalColumns={[
79+
{
80+
id: 'action',
81+
Header: intl.formatMessage(messages['library.authz.team.table.action']),
82+
// eslint-disable-next-line react/no-unstable-nested-components
83+
Cell: ({ row }: CellProps) => (
84+
canManageTeam && row.original.username !== username && !isLoading ? (
85+
<Button
86+
iconBefore={Edit}
87+
variant="link"
88+
size="sm"
89+
// TODO: update the view with the team member view
90+
onClick={() => navigate(`/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', username)}`)}
91+
>
92+
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
93+
</Button>
94+
) : null),
95+
},
96+
]}
97+
initialState={{
98+
pageSize: 10,
99+
}}
100+
columns={columns}
101+
/>
102+
);
103+
};
104+
105+
export default TeamTable;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
'library.authz.team.table.display.name': {
5+
id: 'library.authz.team.table.display.name',
6+
defaultMessage: 'Name',
7+
description: 'Libraries team management table name column header',
8+
},
9+
'library.authz.team.table.email': {
10+
id: 'library.team.table.email',
11+
defaultMessage: 'Email',
12+
description: 'Libraries team management table email column header',
13+
},
14+
'library.authz.team.table.roles': {
15+
id: 'library.authz.team.table.roles',
16+
defaultMessage: 'Roles',
17+
description: 'Libraries team management table roles column header',
18+
},
19+
'library.authz.team.table.action': {
20+
id: 'library.authz.team.table.action',
21+
defaultMessage: 'Action',
22+
description: 'Libraries team management table action column header',
23+
},
24+
'authz.libraries.team.table.edit.action': {
25+
id: 'authz.libraries.team.table.edit.action',
26+
defaultMessage: 'Edit',
27+
description: 'Edit action',
28+
},
29+
});
30+
31+
export default messages;

0 commit comments

Comments
 (0)