diff --git a/src/authz-module/libraries-manager/components/TeamTable.test.tsx b/src/authz-module/libraries-manager/components/TeamTable.test.tsx new file mode 100644 index 0000000..d9cf678 --- /dev/null +++ b/src/authz-module/libraries-manager/components/TeamTable.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + }); +}); diff --git a/src/authz-module/libraries-manager/components/TeamTable.tsx b/src/authz-module/libraries-manager/components/TeamTable.tsx new file mode 100644 index 0000000..48a7dad --- /dev/null +++ b/src/authz-module/libraries-manager/components/TeamTable.tsx @@ -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; + +const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? ( + +) : ( + row.original.email +)); + +const NameCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? ( + +) : ( + row.original.displayName +)); + +const RolesCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? ( + +) : ( + row.original.roles.map((role) => ( + {role} + )) +)); + +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 ( + ( + canManageTeam && row.original.username !== username && !isLoading ? ( + + ) : null), + }, + ]} + initialState={{ + pageSize: 10, + }} + columns={columns} + /> + ); +}; + +export default TeamTable; diff --git a/src/authz-module/libraries-manager/components/messages.ts b/src/authz-module/libraries-manager/components/messages.ts new file mode 100644 index 0000000..70181e4 --- /dev/null +++ b/src/authz-module/libraries-manager/components/messages.ts @@ -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;