fix: table cells refactor to get rid of eslint nested components ignore
This commit is contained in:
committed by
Adolfo R. Brandes
parent
56edd458c3
commit
19034282fc
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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: [],
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user