Skip to content

Commit 1903428

Browse files
jacobo-dominguez-wguarbrandes
authored andcommitted
fix: table cells refactor to get rid of eslint nested components ignore
1 parent 56edd45 commit 1903428

File tree

4 files changed

+274
-63
lines changed

4 files changed

+274
-63
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { IntlProvider } from '@edx/frontend-platform/i18n';
4+
import { useNavigate } from 'react-router-dom';
5+
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
6+
import { useTeamMembers } from '@src/authz-module/data/hooks';
7+
import {
8+
EmailCell,
9+
NameCell,
10+
ActionCell,
11+
RolesCell,
12+
} from './Cells';
13+
14+
jest.mock('react-router-dom', () => ({
15+
useNavigate: jest.fn(),
16+
}));
17+
18+
jest.mock('@src/authz-module/libraries-manager/context', () => ({
19+
useLibraryAuthZ: jest.fn(),
20+
}));
21+
22+
jest.mock('@src/authz-module/data/hooks', () => ({
23+
useTeamMembers: jest.fn(),
24+
}));
25+
26+
jest.mock('../hooks/useQuerySettings', () => ({
27+
useQuerySettings: jest.fn(() => ({
28+
querySettings: { page: 1, limit: 10 },
29+
})),
30+
}));
31+
32+
const mockNavigate = useNavigate as jest.Mock;
33+
const mockUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;
34+
const mockUseTeamMembers = useTeamMembers as jest.Mock;
35+
36+
const renderWithIntl = (component: React.ReactElement) => render(
37+
<IntlProvider locale="en" messages={{}}>
38+
{component}
39+
</IntlProvider>,
40+
);
41+
42+
const mockTeamMember = {
43+
username: 'john.doe',
44+
fullName: 'John Doe',
45+
46+
roles: ['instructor', 'author'],
47+
createdAt: '2023-01-01T00:00:00Z',
48+
};
49+
50+
const mockSkeletonMember = {
51+
username: 'skeleton',
52+
fullName: '',
53+
email: '',
54+
roles: [],
55+
createdAt: '',
56+
};
57+
58+
const mockCellProps = {
59+
row: { original: mockTeamMember },
60+
};
61+
62+
const mockSkeletonCellProps = {
63+
row: { original: mockSkeletonMember },
64+
};
65+
66+
describe('Table Cells', () => {
67+
beforeEach(() => {
68+
jest.clearAllMocks();
69+
mockUseLibraryAuthZ.mockReturnValue({
70+
username: 'current.user',
71+
libraryId: 'lib123',
72+
canManageTeam: true,
73+
roles: [
74+
{ role: 'instructor', name: 'Instructor' },
75+
{ role: 'author', name: 'Author' },
76+
],
77+
});
78+
mockUseTeamMembers.mockReturnValue({ isLoading: false });
79+
mockNavigate.mockReturnValue(jest.fn());
80+
});
81+
82+
describe('EmailCell', () => {
83+
it('displays user email', () => {
84+
renderWithIntl(<EmailCell {...mockCellProps} />);
85+
expect(screen.getByText('[email protected]')).toBeInTheDocument();
86+
});
87+
it('shows loading skeleton for loading state', () => {
88+
renderWithIntl(<EmailCell {...mockSkeletonCellProps} />);
89+
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
90+
});
91+
});
92+
93+
describe('NameCell', () => {
94+
it('displays username for regular user', () => {
95+
renderWithIntl(<NameCell {...mockCellProps} />);
96+
expect(screen.getByText('john.doe')).toBeInTheDocument();
97+
});
98+
99+
it('displays current user indicator for logged in user', () => {
100+
const currentUserProps = {
101+
...mockCellProps,
102+
row: { original: { ...mockTeamMember, username: 'current.user' } },
103+
};
104+
renderWithIntl(<NameCell {...currentUserProps} />);
105+
expect(screen.getByText('current.user')).toBeInTheDocument();
106+
expect(screen.getByText('current.user').parentElement).toBeInTheDocument();
107+
});
108+
it('shows loading skeleton for loading state', () => {
109+
renderWithIntl(<NameCell {...mockSkeletonCellProps} />);
110+
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
111+
});
112+
});
113+
114+
describe('ActionCell', () => {
115+
it('renders edit button for manageable team member', () => {
116+
renderWithIntl(<ActionCell {...mockCellProps} />);
117+
const editButton = screen.getByRole('button');
118+
expect(editButton).toBeInTheDocument();
119+
expect(document.querySelector('.pgn__icon')).toBeInTheDocument();
120+
expect(document.querySelector('svg')).toBeInTheDocument();
121+
});
122+
123+
it('navigates to user page when edit button is clicked', async () => {
124+
const user = userEvent.setup();
125+
const navigateMock = jest.fn();
126+
mockNavigate.mockReturnValue(navigateMock);
127+
renderWithIntl(<ActionCell {...mockCellProps} />);
128+
const editButton = screen.getByRole('button');
129+
await user.click(editButton);
130+
expect(navigateMock).toHaveBeenCalledWith('/authz/libraries/lib123/john.doe');
131+
});
132+
133+
it('does not render edit button for current user', () => {
134+
const currentUserProps = {
135+
...mockCellProps,
136+
row: { original: { ...mockTeamMember, username: 'current.user' } },
137+
};
138+
renderWithIntl(<ActionCell {...currentUserProps} />);
139+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
140+
});
141+
142+
it('does not render edit button when user cannot manage team', () => {
143+
mockUseLibraryAuthZ.mockReturnValue({
144+
username: 'current.user',
145+
libraryId: 'lib123',
146+
canManageTeam: false,
147+
roles: [],
148+
});
149+
renderWithIntl(<ActionCell {...mockCellProps} />);
150+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
151+
});
152+
153+
it('does not render edit button during loading', () => {
154+
mockUseTeamMembers.mockReturnValue({ isLoading: true });
155+
156+
renderWithIntl(<ActionCell {...mockCellProps} />);
157+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
158+
});
159+
});
160+
161+
describe('RolesCell', () => {
162+
it('displays role chips for user roles', () => {
163+
renderWithIntl(<RolesCell {...mockCellProps} />);
164+
expect(screen.getByText('Instructor')).toBeInTheDocument();
165+
expect(screen.getByText('Author')).toBeInTheDocument();
166+
});
167+
168+
it('shows loading skeleton for loading state', () => {
169+
renderWithIntl(<RolesCell {...mockSkeletonCellProps} />);
170+
expect(document.querySelector('.react-loading-skeleton')).toBeInTheDocument();
171+
});
172+
173+
it('handles user with no roles', () => {
174+
const noRolesProps = {
175+
...mockCellProps,
176+
row: { original: { ...mockTeamMember, roles: [] } },
177+
};
178+
renderWithIntl(<RolesCell {...noRolesProps} />);
179+
expect(screen.queryByText('Instructor')).not.toBeInTheDocument();
180+
expect(screen.queryByText('Author')).not.toBeInTheDocument();
181+
});
182+
});
183+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { Button, Chip, Skeleton } from '@openedx/paragon';
3+
import { Edit } from '@openedx/paragon/icons';
4+
import { TableCellValue, TeamMember } from '@src/types';
5+
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
6+
import { useNavigate } from 'react-router-dom';
7+
import { useTeamMembers } from '@src/authz-module/data/hooks';
8+
import { SKELETON_ROWS } from '@src/authz-module/libraries-manager/constants';
9+
import { useQuerySettings } from '../hooks/useQuerySettings';
10+
import messages from '../messages';
11+
12+
type CellProps = TableCellValue<TeamMember>;
13+
14+
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
15+
<Skeleton width="180px" />
16+
) : (
17+
row.original.email
18+
));
19+
20+
const NameCell = ({ row }: CellProps) => {
21+
const intl = useIntl();
22+
const { username } = useLibraryAuthZ();
23+
24+
if (row.original.username === SKELETON_ROWS[0].username) {
25+
return <Skeleton width="180px" />;
26+
}
27+
28+
if (row.original.username === username) {
29+
return (
30+
<span>
31+
{username}
32+
<span className="text-gray-500">{intl.formatMessage(messages['library.authz.team.table.username.current'])}</span>
33+
</span>
34+
);
35+
}
36+
return row.original.username;
37+
};
38+
39+
const ActionCell = ({ row }: CellProps) => {
40+
const intl = useIntl();
41+
const {
42+
libraryId, canManageTeam, username,
43+
} = useLibraryAuthZ();
44+
const navigate = useNavigate();
45+
const { querySettings } = useQuerySettings();
46+
const { isLoading } = useTeamMembers(libraryId, querySettings);
47+
return (
48+
canManageTeam && row.original.username !== username && !isLoading ? (
49+
<Button
50+
iconBefore={Edit}
51+
variant="link"
52+
size="sm"
53+
onClick={() => navigate(`/authz/libraries/${libraryId}/${row.original.username}`)}
54+
>
55+
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
56+
</Button>
57+
) : null);
58+
};
59+
60+
const RolesCell = ({ row }: CellProps) => {
61+
const { roles } = useLibraryAuthZ();
62+
const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record<string, string>);
63+
return (row.original.username === SKELETON_ROWS[0].username ? (
64+
<Skeleton width="80px" />
65+
) : (
66+
row.original.roles.map((role) => (
67+
<Chip key={`${row.original.username}-role-${role}`}>{roleLabels[role]}</Chip>
68+
))
69+
));
70+
};
71+
72+
export {
73+
EmailCell, NameCell, ActionCell, RolesCell,
74+
};

src/authz-module/libraries-manager/components/TeamTable/index.tsx

Lines changed: 10 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,37 @@
11
import { useEffect, useMemo } from 'react';
2-
import { useNavigate } from 'react-router-dom';
32
import debounce from 'lodash.debounce';
43
import { useIntl } from '@edx/frontend-platform/i18n';
54
import {
6-
DataTable, Button, Chip, Skeleton,
5+
DataTable,
76
TextFilter,
87
CheckboxFilter,
98
TableFooter,
109
} from '@openedx/paragon';
11-
import { Edit } from '@openedx/paragon/icons';
12-
import { TableCellValue, TeamMember } from '@src/types';
10+
1311
import { useTeamMembers } from '@src/authz-module/data/hooks';
1412
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
1513
import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext';
14+
import { SKELETON_ROWS } from '@src/authz-module/libraries-manager/constants';
1615
import { useQuerySettings } from './hooks/useQuerySettings';
1716
import TableControlBar from './components/TableControlBar';
1817
import messages from './messages';
19-
20-
const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
21-
username: 'skeleton',
22-
name: '',
23-
email: '',
24-
roles: [],
25-
}));
18+
import {
19+
ActionCell, EmailCell, NameCell, RolesCell,
20+
} from './components/Cells';
2621

2722
const DEFAULT_PAGE_SIZE = 10;
2823

29-
type CellProps = TableCellValue<TeamMember>;
30-
31-
const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
32-
<Skeleton width="180px" />
33-
) : (
34-
row.original.email
35-
));
36-
37-
const NameCell = ({ row }: CellProps) => {
38-
const intl = useIntl();
39-
const { username } = useLibraryAuthZ();
40-
41-
if (row.original.username === SKELETON_ROWS[0].username) {
42-
return <Skeleton width="180px" />;
43-
}
44-
45-
if (row.original.username === username) {
46-
return (
47-
<span>
48-
{username}
49-
<span className="text-gray-500">{intl.formatMessage(messages['library.authz.team.table.username.current'])}</span>
50-
</span>
51-
);
52-
}
53-
return row.original.username;
54-
};
55-
5624
const TeamTable = () => {
5725
const intl = useIntl();
5826
const {
59-
libraryId, canManageTeam, username, roles,
27+
libraryId, roles,
6028
} = useLibraryAuthZ();
61-
const roleLabels = roles.reduce((acc, role) => ({ ...acc, [role.role]: role.name }), {} as Record<string, string>);
6229
const { showErrorToast } = useToastManager();
6330

6431
const { querySettings, handleTableFetch } = useQuerySettings();
6532

6633
const {
67-
data: teamMembers, isLoading, isError, error, refetch,
34+
data: teamMembers, isError, error, refetch,
6835
} = useTeamMembers(libraryId, querySettings);
6936

7037
if (error) {
@@ -74,8 +41,6 @@ const TeamTable = () => {
7441
const rows = isError ? [] : (teamMembers?.results || SKELETON_ROWS);
7542
const pageCount = teamMembers?.count ? Math.ceil(teamMembers.count / DEFAULT_PAGE_SIZE) : 1;
7643

77-
const navigate = useNavigate();
78-
7944
const adaptedFilterChoices = useMemo(
8045
() => roles.map((role) => ({
8146
name: role.name,
@@ -108,18 +73,7 @@ const TeamTable = () => {
10873
{
10974
id: 'action',
11075
Header: intl.formatMessage(messages['library.authz.team.table.action']),
111-
// eslint-disable-next-line react/no-unstable-nested-components
112-
Cell: ({ row }: CellProps) => (
113-
canManageTeam && row.original.username !== username && !isLoading ? (
114-
<Button
115-
iconBefore={Edit}
116-
variant="link"
117-
size="sm"
118-
onClick={() => navigate(`/authz/libraries/${libraryId}/${row.original.username}`)}
119-
>
120-
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
121-
</Button>
122-
) : null),
76+
Cell: ActionCell,
12377
},
12478
]}
12579
columns={
@@ -140,14 +94,7 @@ const TeamTable = () => {
14094
{
14195
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
14296
accessor: 'roles',
143-
// eslint-disable-next-line react/no-unstable-nested-components
144-
Cell: ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
145-
<Skeleton width="80px" />
146-
) : (
147-
row.original.roles.map((role) => (
148-
<Chip key={`${row.original.username}-role-${role}`}>{roleLabels[role]}</Chip>
149-
))
150-
)),
97+
Cell: RolesCell,
15198
Filter: CheckboxFilter,
15299
filter: 'includesValue',
153100
filterChoices: Object.values(adaptedFilterChoices),

src/authz-module/libraries-manager/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ export const libraryPermissions: PermissionMetadata[] = [
4949
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
5050
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
5151
];
52+
53+
export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
54+
username: 'skeleton',
55+
name: '',
56+
email: '',
57+
roles: [],
58+
}));

0 commit comments

Comments
 (0)