From 2a4fc946992992197e4cacc3028af08fabb15c2d Mon Sep 17 00:00:00 2001 From: jacobo-dominguez-wgu Date: Tue, 4 Nov 2025 12:07:21 -0600 Subject: [PATCH] fix: table cells refactor to get rid of eslint nested components ignore --- .../TeamTable/components/Cells.test.tsx | 183 ++++++++++++++++++ .../components/TeamTable/components/Cells.tsx | 74 +++++++ .../components/TeamTable/index.tsx | 73 +------ .../libraries-manager/constants.ts | 7 + 4 files changed, 274 insertions(+), 63 deletions(-) create mode 100644 src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx create mode 100644 src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx 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: [], +}));