fix: table cells refactor to get rid of eslint nested components ignore

This commit is contained in:
jacobo-dominguez-wgu
2025-11-04 12:07:21 -06:00
committed by Adolfo R. Brandes
parent 56edd458c3
commit 19034282fc
4 changed files with 274 additions and 63 deletions

View File

@@ -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(
<IntlProvider locale="en" messages={{}}>
{component}
</IntlProvider>,
);
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(<EmailCell {...mockCellProps} />);
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});
it('shows loading skeleton for loading state', () => {
renderWithIntl(<EmailCell {...mockSkeletonCellProps} />);
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
});
});
describe('NameCell', () => {
it('displays username for regular user', () => {
renderWithIntl(<NameCell {...mockCellProps} />);
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(<NameCell {...currentUserProps} />);
expect(screen.getByText('current.user')).toBeInTheDocument();
expect(screen.getByText('current.user').parentElement).toBeInTheDocument();
});
it('shows loading skeleton for loading state', () => {
renderWithIntl(<NameCell {...mockSkeletonCellProps} />);
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
});
});
describe('ActionCell', () => {
it('renders edit button for manageable team member', () => {
renderWithIntl(<ActionCell {...mockCellProps} />);
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(<ActionCell {...mockCellProps} />);
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(<ActionCell {...currentUserProps} />);
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(<ActionCell {...mockCellProps} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('does not render edit button during loading', () => {
mockUseTeamMembers.mockReturnValue({ isLoading: true });
renderWithIntl(<ActionCell {...mockCellProps} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
describe('RolesCell', () => {
it('displays role chips for user roles', () => {
renderWithIntl(<RolesCell {...mockCellProps} />);
expect(screen.getByText('Instructor')).toBeInTheDocument();
expect(screen.getByText('Author')).toBeInTheDocument();
});
it('shows loading skeleton for loading state', () => {
renderWithIntl(<RolesCell {...mockSkeletonCellProps} />);
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
});
it('handles user with no roles', () => {
const noRolesProps = {
...mockCellProps,
row: { original: { ...mockTeamMember, roles: [] } },
};
renderWithIntl(<RolesCell {...noRolesProps} />);
expect(screen.queryByText('Instructor')).not.toBeInTheDocument();
expect(screen.queryByText('Author')).not.toBeInTheDocument();
});
});
});

View File

@@ -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<TeamMember>;
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
<Skeleton width="180px" />
) : (
row.original.email
));
const NameCell = ({ row }: CellProps) => {
const intl = useIntl();
const { username } = useLibraryAuthZ();
if (row.original.username === SKELETON_ROWS[0].username) {
return <Skeleton width="180px" />;
}
if (row.original.username === username) {
return (
<span>
{username}
<span className="text-gray-500">{intl.formatMessage(messages['library.authz.team.table.username.current'])}</span>
</span>
);
}
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 ? (
<Button
iconBefore={Edit}
variant="link"
size="sm"
onClick={() => navigate(`/authz/libraries/${libraryId}/${row.original.username}`)}
>
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
</Button>
) : null);
};
const RolesCell = ({ row }: CellProps) => {
const { roles } = useLibraryAuthZ();
const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record<string, string>);
return (row.original.username === SKELETON_ROWS[0].username ? (
<Skeleton width="80px" />
) : (
row.original.roles.map((role) => (
<Chip key={`${row.original.username}-role-${role}`}>{roleLabels[role]}</Chip>
))
));
};
export {
EmailCell, NameCell, ActionCell, RolesCell,
};

View File

@@ -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<TeamMember>;
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
<Skeleton width="180px" />
) : (
row.original.email
));
const NameCell = ({ row }: CellProps) => {
const intl = useIntl();
const { username } = useLibraryAuthZ();
if (row.original.username === SKELETON_ROWS[0].username) {
return <Skeleton width="180px" />;
}
if (row.original.username === username) {
return (
<span>
{username}
<span className="text-gray-500">{intl.formatMessage(messages['library.authz.team.table.username.current'])}</span>
</span>
);
}
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<string, string>);
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 ? (
<Button
iconBefore={Edit}
variant="link"
size="sm"
onClick={() => navigate(`/authz/libraries/${libraryId}/${row.original.username}`)}
>
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
</Button>
) : 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 ? (
<Skeleton width="80px" />
) : (
row.original.roles.map((role) => (
<Chip key={`${row.original.username}-role-${role}`}>{roleLabels[role]}</Chip>
))
)),
Cell: RolesCell,
Filter: CheckboxFilter,
filter: 'includesValue',
filterChoices: Object.values(adaptedFilterChoices),

View File

@@ -49,3 +49,10 @@ export const libraryPermissions: PermissionMetadata[] = [
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.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: [],
}));