diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx
new file mode 100644
index 0000000..fe7c940
--- /dev/null
+++ b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx
@@ -0,0 +1,183 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { useNavigate } from 'react-router-dom';
+import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
+import { useTeamMembers } from '@src/authz-module/data/hooks';
+import {
+ EmailCell,
+ NameCell,
+ ActionCell,
+ RolesCell,
+} from './Cells';
+
+jest.mock('react-router-dom', () => ({
+ useNavigate: jest.fn(),
+}));
+
+jest.mock('@src/authz-module/libraries-manager/context', () => ({
+ useLibraryAuthZ: jest.fn(),
+}));
+
+jest.mock('@src/authz-module/data/hooks', () => ({
+ useTeamMembers: jest.fn(),
+}));
+
+jest.mock('../hooks/useQuerySettings', () => ({
+ useQuerySettings: jest.fn(() => ({
+ querySettings: { page: 1, limit: 10 },
+ })),
+}));
+
+const mockNavigate = useNavigate as jest.Mock;
+const mockUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;
+const mockUseTeamMembers = useTeamMembers as jest.Mock;
+
+const renderWithIntl = (component: React.ReactElement) => render(
+
+ {component}
+ ,
+);
+
+const mockTeamMember = {
+ username: 'john.doe',
+ fullName: 'John Doe',
+ email: 'john.doe@example.com',
+ roles: ['instructor', 'author'],
+ createdAt: '2023-01-01T00:00:00Z',
+};
+
+const mockSkeletonMember = {
+ username: 'skeleton',
+ fullName: '',
+ email: '',
+ roles: [],
+ createdAt: '',
+};
+
+const mockCellProps = {
+ row: { original: mockTeamMember },
+};
+
+const mockSkeletonCellProps = {
+ row: { original: mockSkeletonMember },
+};
+
+describe('Table Cells', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseLibraryAuthZ.mockReturnValue({
+ username: 'current.user',
+ libraryId: 'lib123',
+ canManageTeam: true,
+ roles: [
+ { role: 'instructor', name: 'Instructor' },
+ { role: 'author', name: 'Author' },
+ ],
+ });
+ mockUseTeamMembers.mockReturnValue({ isLoading: false });
+ mockNavigate.mockReturnValue(jest.fn());
+ });
+
+ describe('EmailCell', () => {
+ it('displays user email', () => {
+ renderWithIntl();
+ expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
+ });
+ it('shows loading skeleton for loading state', () => {
+ renderWithIntl();
+ expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
+ });
+ });
+
+ describe('NameCell', () => {
+ it('displays username for regular user', () => {
+ renderWithIntl();
+ expect(screen.getByText('john.doe')).toBeInTheDocument();
+ });
+
+ it('displays current user indicator for logged in user', () => {
+ const currentUserProps = {
+ ...mockCellProps,
+ row: { original: { ...mockTeamMember, username: 'current.user' } },
+ };
+ renderWithIntl();
+ expect(screen.getByText('current.user')).toBeInTheDocument();
+ expect(screen.getByText('current.user').parentElement).toBeInTheDocument();
+ });
+ it('shows loading skeleton for loading state', () => {
+ renderWithIntl();
+ expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
+ });
+ });
+
+ describe('ActionCell', () => {
+ it('renders edit button for manageable team member', () => {
+ renderWithIntl();
+ const editButton = screen.getByRole('button');
+ expect(editButton).toBeInTheDocument();
+ expect(document.querySelector('.pgn__icon')).toBeInTheDocument();
+ expect(document.querySelector('svg')).toBeInTheDocument();
+ });
+
+ it('navigates to user page when edit button is clicked', async () => {
+ const user = userEvent.setup();
+ const navigateMock = jest.fn();
+ mockNavigate.mockReturnValue(navigateMock);
+ renderWithIntl();
+ const editButton = screen.getByRole('button');
+ await user.click(editButton);
+ expect(navigateMock).toHaveBeenCalledWith('/authz/libraries/lib123/john.doe');
+ });
+
+ it('does not render edit button for current user', () => {
+ const currentUserProps = {
+ ...mockCellProps,
+ row: { original: { ...mockTeamMember, username: 'current.user' } },
+ };
+ renderWithIntl();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('does not render edit button when user cannot manage team', () => {
+ mockUseLibraryAuthZ.mockReturnValue({
+ username: 'current.user',
+ libraryId: 'lib123',
+ canManageTeam: false,
+ roles: [],
+ });
+ renderWithIntl();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('does not render edit button during loading', () => {
+ mockUseTeamMembers.mockReturnValue({ isLoading: true });
+
+ renderWithIntl();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('RolesCell', () => {
+ it('displays role chips for user roles', () => {
+ renderWithIntl();
+ expect(screen.getByText('Instructor')).toBeInTheDocument();
+ expect(screen.getByText('Author')).toBeInTheDocument();
+ });
+
+ it('shows loading skeleton for loading state', () => {
+ renderWithIntl();
+ expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
+ });
+
+ it('handles user with no roles', () => {
+ const noRolesProps = {
+ ...mockCellProps,
+ row: { original: { ...mockTeamMember, roles: [] } },
+ };
+ renderWithIntl();
+ expect(screen.queryByText('Instructor')).not.toBeInTheDocument();
+ expect(screen.queryByText('Author')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx
new file mode 100644
index 0000000..9f16099
--- /dev/null
+++ b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx
@@ -0,0 +1,74 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Button, Chip, Skeleton } from '@openedx/paragon';
+import { Edit } from '@openedx/paragon/icons';
+import { TableCellValue, TeamMember } from '@src/types';
+import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
+import { useNavigate } from 'react-router-dom';
+import { useTeamMembers } from '@src/authz-module/data/hooks';
+import { SKELETON_ROWS } from '@src/authz-module/libraries-manager/constants';
+import { useQuerySettings } from '../hooks/useQuerySettings';
+import messages from '../messages';
+
+type CellProps = TableCellValue;
+
+const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
+
+) : (
+ row.original.email
+));
+
+const NameCell = ({ row }: CellProps) => {
+ const intl = useIntl();
+ const { username } = useLibraryAuthZ();
+
+ if (row.original.username === SKELETON_ROWS[0].username) {
+ return ;
+ }
+
+ if (row.original.username === username) {
+ return (
+
+ {username}
+ {intl.formatMessage(messages['library.authz.team.table.username.current'])}
+
+ );
+ }
+ return row.original.username;
+};
+
+const ActionCell = ({ row }: CellProps) => {
+ const intl = useIntl();
+ const {
+ libraryId, canManageTeam, username,
+ } = useLibraryAuthZ();
+ const navigate = useNavigate();
+ const { querySettings } = useQuerySettings();
+ const { isLoading } = useTeamMembers(libraryId, querySettings);
+ return (
+ canManageTeam && row.original.username !== username && !isLoading ? (
+
+ ) : null);
+};
+
+const RolesCell = ({ row }: CellProps) => {
+ const { roles } = useLibraryAuthZ();
+ const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record);
+ return (row.original.username === SKELETON_ROWS[0].username ? (
+
+ ) : (
+ row.original.roles.map((role) => (
+ {roleLabels[role]}
+ ))
+ ));
+};
+
+export {
+ EmailCell, NameCell, ActionCell, RolesCell,
+};
diff --git a/src/authz-module/libraries-manager/components/TeamTable/index.tsx b/src/authz-module/libraries-manager/components/TeamTable/index.tsx
index 851e3d0..3763bd8 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/index.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/index.tsx
@@ -1,70 +1,37 @@
import { useEffect, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
import debounce from 'lodash.debounce';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
- DataTable, Button, Chip, Skeleton,
+ DataTable,
TextFilter,
CheckboxFilter,
TableFooter,
} from '@openedx/paragon';
-import { Edit } from '@openedx/paragon/icons';
-import { TableCellValue, TeamMember } from '@src/types';
+
import { useTeamMembers } from '@src/authz-module/data/hooks';
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext';
+import { SKELETON_ROWS } from '@src/authz-module/libraries-manager/constants';
import { useQuerySettings } from './hooks/useQuerySettings';
import TableControlBar from './components/TableControlBar';
import messages from './messages';
-
-const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
- username: 'skeleton',
- name: '',
- email: '',
- roles: [],
-}));
+import {
+ ActionCell, EmailCell, NameCell, RolesCell,
+} from './components/Cells';
const DEFAULT_PAGE_SIZE = 10;
-type CellProps = TableCellValue;
-
-const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
-
-) : (
- row.original.email
-));
-
-const NameCell = ({ row }: CellProps) => {
- const intl = useIntl();
- const { username } = useLibraryAuthZ();
-
- if (row.original.username === SKELETON_ROWS[0].username) {
- return ;
- }
-
- if (row.original.username === username) {
- return (
-
- {username}
- {intl.formatMessage(messages['library.authz.team.table.username.current'])}
-
- );
- }
- return row.original.username;
-};
-
const TeamTable = () => {
const intl = useIntl();
const {
- libraryId, canManageTeam, username, roles,
+ libraryId, roles,
} = useLibraryAuthZ();
- const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record);
const { showErrorToast } = useToastManager();
const { querySettings, handleTableFetch } = useQuerySettings();
const {
- data: teamMembers, isLoading, isError, error, refetch,
+ data: teamMembers, isError, error, refetch,
} = useTeamMembers(libraryId, querySettings);
if (error) {
@@ -74,8 +41,6 @@ const TeamTable = () => {
const rows = isError ? [] : (teamMembers?.results || SKELETON_ROWS);
const pageCount = teamMembers?.count ? Math.ceil(teamMembers.count / DEFAULT_PAGE_SIZE) : 1;
- const navigate = useNavigate();
-
const adaptedFilterChoices = useMemo(
() => roles.map((role) => ({
name: role.name,
@@ -108,18 +73,7 @@ const TeamTable = () => {
{
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 ? (
-
- ) : null),
+ Cell: ActionCell,
},
]}
columns={
@@ -140,14 +94,7 @@ const TeamTable = () => {
{
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
accessor: 'roles',
- // eslint-disable-next-line react/no-unstable-nested-components
- Cell: ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
-
- ) : (
- row.original.roles.map((role) => (
- {roleLabels[role]}
- ))
- )),
+ Cell: RolesCell,
Filter: CheckboxFilter,
filter: 'includesValue',
filterChoices: Object.values(adaptedFilterChoices),
diff --git a/src/authz-module/libraries-manager/constants.ts b/src/authz-module/libraries-manager/constants.ts
index 858c32f..4bf92f0 100644
--- a/src/authz-module/libraries-manager/constants.ts
+++ b/src/authz-module/libraries-manager/constants.ts
@@ -32,3 +32,10 @@ export const libraryPermissions: PermissionMetadata[] = [
{ key: 'manage_library_team', resource: 'library_team', description: 'View the list of users who have access to the library.' },
{ key: 'view_library_team', resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
];
+
+export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
+ username: 'skeleton',
+ name: '',
+ email: '',
+ roles: [],
+}));