Skip to content

Commit 62dc7bf

Browse files
fix: header table change to avoid key error due to paragon, missing flag added
1 parent 2b3b480 commit 62dc7bf

3 files changed

Lines changed: 55 additions & 31 deletions

File tree

src/authz-module/components/utils.test.tsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
44
import { renderWrapper } from '@src/setupTest';
55
import { getCellHeader } from './utils';
66

7-
const renderCellHeader = (columnId: string, columnTitle: string, filtersApplied: string[]) => {
8-
const component = getCellHeader(columnId, columnTitle, filtersApplied);
9-
return renderWrapper(<div>{component}</div>);
10-
};
11-
127
describe('getCellHeader', () => {
138
beforeEach(() => {
149
initializeMockApp({
@@ -21,64 +16,74 @@ describe('getCellHeader', () => {
2116
});
2217

2318
it('displays column title without filter icon when no filters are applied', () => {
24-
const { container } = renderCellHeader('scope', 'Scope', []);
25-
26-
expect(screen.getByText('Scope')).toBeInTheDocument();
27-
expect(container.querySelector('svg')).not.toBeInTheDocument();
19+
const result = getCellHeader('scope', 'Scope', []);
20+
expect(result).toBe('Scope');
2821
});
2922

3023
it('displays column title without filter icon when column is not in filters applied', () => {
31-
const { container } = renderCellHeader('scope', 'Scope', ['role', 'organization']);
24+
const result = getCellHeader('scope', 'Scope', ['role', 'organization']);
25+
expect(result).toBe('Scope');
26+
});
3227

33-
expect(screen.getByText('Scope')).toBeInTheDocument();
34-
expect(container.querySelector('svg')).not.toBeInTheDocument();
28+
it('returns a component function when column has filter applied', () => {
29+
const result = getCellHeader('scope', 'Scope', ['scope', 'role']);
30+
expect(typeof result).toBe('function');
3531
});
3632

37-
it('displays column title with filter icon when column has filter applied', () => {
38-
const { container } = renderCellHeader('scope', 'Scope', ['scope', 'role']);
33+
it('renders filter icon and title when component is rendered', () => {
34+
const Header = getCellHeader('scope', 'Scope', ['scope', 'role']) as React.FC;
35+
renderWrapper(<Header />);
3936

4037
expect(screen.getByText('Scope')).toBeInTheDocument();
41-
expect(container.querySelector('svg')).toBeInTheDocument();
38+
expect(screen.getByText('Scope').closest('span')).toHaveClass('d-flex', 'flex-row', 'align-items-center');
4239
});
4340

4441
it('displays filter icon only for matching column when multiple filters applied', () => {
45-
const { container } = renderCellHeader('organization', 'Organization', ['scope', 'organization', 'role']);
42+
const Header = getCellHeader('organization', 'Organization', ['scope', 'organization', 'role']) as React.FC;
43+
renderWrapper(<Header />);
4644

4745
expect(screen.getByText('Organization')).toBeInTheDocument();
48-
expect(container.querySelector('svg')).toBeInTheDocument();
4946
});
5047

5148
it('handles empty column title', () => {
52-
const { container } = renderCellHeader('scope', '', ['scope']);
49+
const Header = getCellHeader('scope', '', ['scope']) as React.FC;
50+
const { container } = renderWrapper(<Header />);
5351

5452
expect(container.querySelector('svg')).toBeInTheDocument();
5553
});
5654

5755
it('handles special characters in column title', () => {
58-
const { container } = renderCellHeader('scope', 'Scope & Context', ['scope']);
56+
const Header = getCellHeader('scope', 'Scope & Context', ['scope']) as React.FC;
57+
renderWrapper(<Header />);
5958

6059
expect(screen.getByText('Scope & Context')).toBeInTheDocument();
61-
expect(container.querySelector('svg')).toBeInTheDocument();
6260
});
6361

6462
it('is case sensitive when matching column ID', () => {
65-
const { container } = renderCellHeader('scope', 'Scope', ['SCOPE', 'Role']);
66-
67-
expect(screen.getByText('Scope')).toBeInTheDocument();
68-
expect(container.querySelector('svg')).not.toBeInTheDocument();
63+
const result = getCellHeader('scope', 'Scope', ['SCOPE', 'Role']);
64+
expect(result).toBe('Scope');
6965
});
7066

7167
it('handles long column titles with filters', () => {
72-
const { container } = renderCellHeader('organization', 'Very Long Organization Column Title', ['organization']);
68+
const Header = getCellHeader('organization', 'Very Long Organization Column Title', ['organization']) as React.FC;
69+
renderWrapper(<Header />);
7370

7471
expect(screen.getByText('Very Long Organization Column Title')).toBeInTheDocument();
75-
expect(container.querySelector('svg')).toBeInTheDocument();
7672
});
7773

78-
it('displays correct structure when filter is applied', () => {
79-
renderCellHeader('role', 'Role', ['role']);
74+
it('produces unique toString per column to avoid duplicate React keys', () => {
75+
const scopeHeader = getCellHeader('scope', 'Scope', ['scope', 'role']);
76+
const roleHeader = getCellHeader('role', 'Role', ['scope', 'role']);
77+
78+
// When used in template literals (as Paragon does for keys), each column
79+
// must produce a unique string to prevent duplicate key warnings.
80+
expect(`${scopeHeader}`).not.toBe(`${roleHeader}`);
81+
expect(`${scopeHeader}`).toBe('FilteredHeader_scope');
82+
expect(`${roleHeader}`).toBe('FilteredHeader_role');
83+
});
8084

81-
const container = screen.getByText('Role').closest('span');
82-
expect(container).toHaveClass('d-flex', 'flex-row', 'align-items-center');
85+
it('toString does not produce [object Object]', () => {
86+
const result = getCellHeader('scope', 'Scope', ['scope']);
87+
expect(`${result}`).not.toBe('[object Object]');
8388
});
8489
});
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
import { Icon } from '@openedx/paragon';
22
import { FilterList } from '@openedx/paragon/icons';
33

4+
/**
5+
* Returns a header value for a DataTable column that shows a filter icon
6+
* when the column has an active filter.
7+
*
8+
* When a filter is active, returns a **component function** with a custom
9+
* toString() override. This is necessary because Paragon's TableRow builds
10+
* cell keys via: `${cell.column.Header}${row.id}`
11+
*
12+
* - A JSX element stringifies to "[object Object]", causing duplicate keys.
13+
* - A function with a custom toString() produces a unique string per column.
14+
*
15+
* react-table's render('Header') calls the function as a component, so the
16+
* JSX is still rendered correctly in the table header.
17+
*/
418
export const getCellHeader = (columnId: string, columnTitle: string, filtersApplied: string[]) => {
519
if (filtersApplied.includes(columnId)) {
6-
return (
20+
const FilteredHeader = () => (
721
<span className="d-flex flex-row align-items-center">
822
<Icon src={FilterList} size="sm" className="mr-2" />
923
{columnTitle}
1024
</span>
1125
);
26+
FilteredHeader.displayName = `FilteredHeader_${columnId}`;
27+
FilteredHeader.toString = () => `FilteredHeader_${columnId}`;
28+
return FilteredHeader;
1229
}
1330
return columnTitle;
1431
};

src/authz-module/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ export const rolesObject = [
495495
userCount: 1,
496496
name: 'Course Editor',
497497
description: 'building and maintaining course content and supporting assets, without operational controls or high impact actions that can affect a live course.',
498+
disabled: true,
498499
},
499500
{
500501
role: 'course_auditor',
@@ -513,6 +514,7 @@ export const rolesObject = [
513514
userCount: 1,
514515
name: 'Course Auditor',
515516
description: ' QA, compliance review, content review, and general oversight, no changes in Studio.',
517+
disabled: true,
516518
},
517519

518520
];

0 commit comments

Comments
 (0)