feat: create the table team management table
This commit is contained in:
committed by
Adolfo R. Brandes
parent
26e28aeb96
commit
21a3b9278b
126
src/authz-module/libraries-manager/components/TeamTable.test.tsx
Normal file
126
src/authz-module/libraries-manager/components/TeamTable.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
105
src/authz-module/libraries-manager/components/TeamTable.tsx
Normal file
105
src/authz-module/libraries-manager/components/TeamTable.tsx
Normal 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;
|
||||
31
src/authz-module/libraries-manager/components/messages.ts
Normal file
31
src/authz-module/libraries-manager/components/messages.ts
Normal 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;
|
||||
Reference in New Issue
Block a user