feat: create the table team management table

This commit is contained in:
Diana Olarte
2025-09-24 23:00:59 +10:00
committed by Adolfo R. Brandes
parent 26e28aeb96
commit 21a3b9278b
3 changed files with 262 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ROUTES } from '@src/authz-module/constants';
import { renderWrapper } from '@src/setupTest';
import TeamTable from './TeamTable';
import { useTeamMembers } from '../data/hooks';
import { useLibraryAuthZ } from '../context';
const mockNavigate = jest.fn();
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useNavigate: () => mockNavigate,
}));
jest.mock('../data/hooks', () => ({
useTeamMembers: jest.fn(),
}));
jest.mock('../context', () => ({
useLibraryAuthZ: jest.fn(),
}));
describe('TeamTable', () => {
const mockTeamMembers = [
{
displayName: 'Alice',
email: 'alice@example.com',
roles: ['Admin', 'Editor'],
username: 'alice',
},
{
displayName: 'Bob',
email: 'bob@example.com',
roles: ['Viewer'],
username: 'bob',
},
];
const mockAuthZ = {
libraryId: 'lib:123',
canManageTeam: true,
username: 'alice',
};
beforeEach(() => {
jest.clearAllMocks();
});
it('shows skeletons while loading', () => {
(useTeamMembers as jest.Mock).mockReturnValue({
data: null,
isLoading: true,
});
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
renderWrapper(<TeamTable />);
const skeletons = screen.getAllByText('', { selector: '[aria-busy="true"]' });
expect(skeletons.length).toBeGreaterThan(0);
});
it('renders team member data after loading', () => {
(useTeamMembers as jest.Mock).mockReturnValue({
data: mockTeamMembers,
isLoading: false,
});
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
renderWrapper(<TeamTable />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
expect(screen.getByText('Editor')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
expect(screen.getByText('bob@example.com')).toBeInTheDocument();
expect(screen.getByText('Viewer')).toBeInTheDocument();
});
it('renders Edit button only for users with than can manage team members (current user can not edit themselves)', async () => {
(useTeamMembers as jest.Mock).mockReturnValue({
data: mockTeamMembers,
isLoading: false,
});
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
renderWrapper(<TeamTable />);
const editButtons = screen.queryAllByText('Edit');
// Should not find Edit button for current user
expect(editButtons).toHaveLength(1);
await userEvent.click(editButtons[0]);
expect(mockNavigate).toHaveBeenCalledWith(
`/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', 'alice')}`,
);
});
it('does not render Edit button if canManageTeam is false', () => {
(useTeamMembers as jest.Mock).mockReturnValue({
data: mockTeamMembers,
isLoading: false,
});
(useLibraryAuthZ as jest.Mock).mockReturnValue({
...mockAuthZ,
canManageTeam: false,
});
renderWrapper(<TeamTable />);
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
});
it('does not render Edit button while loading', () => {
(useTeamMembers as jest.Mock).mockReturnValue({
data: null,
isLoading: true,
});
(useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
renderWrapper(<TeamTable />);
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,105 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
DataTable, Button, Chip, Skeleton,
} from '@openedx/paragon';
import { Edit } from '@openedx/paragon/icons';
import { ROUTES, TableCellValue, TeamMember } from '@src/authz-module/constants';
import { useTeamMembers } from '../data/hooks';
import { useLibraryAuthZ } from '../context';
import messages from './messages';
const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
username: 'skeleton',
name: '',
email: '',
roles: [],
}));
type CellProps = TableCellValue<TeamMember>;
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
<Skeleton width="180px" />
) : (
row.original.email
));
const NameCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
<Skeleton width="180px" />
) : (
row.original.displayName
));
const RolesCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
<Skeleton width="80px" />
) : (
row.original.roles.map((role) => (
<Chip key={`${row.original.username}-role-${role}`}>{role}</Chip>
))
));
const TeamTable = () => {
const intl = useIntl();
const { libraryId, canManageTeam, username } = useLibraryAuthZ();
// TODO: Display error in the notification system
const {
data: teamMembers, isLoading, isError
} = useTeamMembers(libraryId);
const rows = isError ? [] : (teamMembers || SKELETON_ROWS);
const navigate = useNavigate();
const columns = useMemo(() => [
{
Header: intl.formatMessage(messages['library.authz.team.table.display.name']),
accessor: 'displayName',
Cell: NameCell,
},
{
Header: intl.formatMessage(messages['library.authz.team.table.email']),
accessor: 'email',
Cell: EmailCell,
},
{
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
accessor: 'roles',
Cell: RolesCell,
},
], [isLoading]);
return (
<DataTable
isPaginated
data={rows}
itemCount={rows?.length}
additionalColumns={[
{
id: 'action',
Header: intl.formatMessage(messages['library.authz.team.table.action']),
// eslint-disable-next-line react/no-unstable-nested-components
Cell: ({ row }: CellProps) => (
canManageTeam && row.original.username !== username && !isLoading ? (
<Button
iconBefore={Edit}
variant="link"
size="sm"
// TODO: update the view with the team member view
onClick={() => navigate(`/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', username)}`)}
>
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
</Button>
) : null),
},
]}
initialState={{
pageSize: 10,
}}
columns={columns}
/>
);
};
export default TeamTable;

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'library.authz.team.table.display.name': {
id: 'library.authz.team.table.display.name',
defaultMessage: 'Name',
description: 'Libraries team management table name column header',
},
'library.authz.team.table.email': {
id: 'library.team.table.email',
defaultMessage: 'Email',
description: 'Libraries team management table email column header',
},
'library.authz.team.table.roles': {
id: 'library.authz.team.table.roles',
defaultMessage: 'Roles',
description: 'Libraries team management table roles column header',
},
'library.authz.team.table.action': {
id: 'library.authz.team.table.action',
defaultMessage: 'Action',
description: 'Libraries team management table action column header',
},
'authz.libraries.team.table.edit.action': {
id: 'authz.libraries.team.table.edit.action',
defaultMessage: 'Edit',
description: 'Edit action',
},
});
export default messages;