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 { useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
DataTable, Button, Chip, Skeleton,
|
DataTable,
|
||||||
TextFilter,
|
TextFilter,
|
||||||
CheckboxFilter,
|
CheckboxFilter,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { Edit } from '@openedx/paragon/icons';
|
|
||||||
import { TableCellValue, TeamMember } from '@src/types';
|
|
||||||
import { useTeamMembers } from '@src/authz-module/data/hooks';
|
import { useTeamMembers } from '@src/authz-module/data/hooks';
|
||||||
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
|
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
|
||||||
import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext';
|
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 { useQuerySettings } from './hooks/useQuerySettings';
|
||||||
import TableControlBar from './components/TableControlBar';
|
import TableControlBar from './components/TableControlBar';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import {
|
||||||
const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
|
ActionCell, EmailCell, NameCell, RolesCell,
|
||||||
username: 'skeleton',
|
} from './components/Cells';
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
roles: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 10;
|
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 TeamTable = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
libraryId, canManageTeam, username, roles,
|
libraryId, roles,
|
||||||
} = useLibraryAuthZ();
|
} = useLibraryAuthZ();
|
||||||
const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record<string, string>);
|
|
||||||
const { showErrorToast } = useToastManager();
|
const { showErrorToast } = useToastManager();
|
||||||
|
|
||||||
const { querySettings, handleTableFetch } = useQuerySettings();
|
const { querySettings, handleTableFetch } = useQuerySettings();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: teamMembers, isLoading, isError, error, refetch,
|
data: teamMembers, isError, error, refetch,
|
||||||
} = useTeamMembers(libraryId, querySettings);
|
} = useTeamMembers(libraryId, querySettings);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -74,8 +41,6 @@ const TeamTable = () => {
|
|||||||
const rows = isError ? [] : (teamMembers?.results || SKELETON_ROWS);
|
const rows = isError ? [] : (teamMembers?.results || SKELETON_ROWS);
|
||||||
const pageCount = teamMembers?.count ? Math.ceil(teamMembers.count / DEFAULT_PAGE_SIZE) : 1;
|
const pageCount = teamMembers?.count ? Math.ceil(teamMembers.count / DEFAULT_PAGE_SIZE) : 1;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const adaptedFilterChoices = useMemo(
|
const adaptedFilterChoices = useMemo(
|
||||||
() => roles.map((role) => ({
|
() => roles.map((role) => ({
|
||||||
name: role.name,
|
name: role.name,
|
||||||
@@ -108,18 +73,7 @@ const TeamTable = () => {
|
|||||||
{
|
{
|
||||||
id: 'action',
|
id: 'action',
|
||||||
Header: intl.formatMessage(messages['library.authz.team.table.action']),
|
Header: intl.formatMessage(messages['library.authz.team.table.action']),
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
Cell: ActionCell,
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
columns={
|
columns={
|
||||||
@@ -140,14 +94,7 @@ const TeamTable = () => {
|
|||||||
{
|
{
|
||||||
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
|
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
|
||||||
accessor: 'roles',
|
accessor: 'roles',
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
Cell: RolesCell,
|
||||||
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>
|
|
||||||
))
|
|
||||||
)),
|
|
||||||
Filter: CheckboxFilter,
|
Filter: CheckboxFilter,
|
||||||
filter: 'includesValue',
|
filter: 'includesValue',
|
||||||
filterChoices: Object.values(adaptedFilterChoices),
|
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.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.' },
|
{ 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