diff --git a/client/package-lock.json b/client/package-lock.json index 1ca93bbfe..e1034f0ad 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -33,6 +33,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "@tanstack/react-table": "^9.1.2", "@tiptap/react": "^2.0.4", "@tiptap/starter-kit": "^2.0.4", "antd": "^4.10.2", @@ -56,7 +57,6 @@ "react-query": "^3.39.1", "react-router-dom": "^6.30.4", "react-syntax-highlighter": "^16.1.0", - "react-table": "^7.7.0", "recharts": "^2.1.10", "safe-stable-stringify": "^2.4.2", "sonner": "^1.5.0", @@ -83,7 +83,6 @@ "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.0.0", "@types/react-syntax-highlighter": "^13.5.2", - "@types/react-table": "^7.7.20", "@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/parser": "^8.57.2", "@vitejs/plugin-react": "^5.2.0", @@ -4422,6 +4421,70 @@ "node": ">=4" } }, + "node_modules/@tanstack/react-table": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-9.1.2.tgz", + "integrity": "sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-store": "^0.11.0", + "@tanstack/table-core": "9.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@tanstack/react-table/node_modules/@tanstack/react-store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.1.tgz", + "integrity": "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.11.1", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.1.tgz", + "integrity": "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/table-core": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-9.1.2.tgz", + "integrity": "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.11.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -5308,16 +5371,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-table": { - "version": "7.7.20", - "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz", - "integrity": "sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -12292,18 +12345,6 @@ "react": ">= 0.14.0" } }, - "node_modules/react-table": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", - "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" - } - }, "node_modules/react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -14364,6 +14405,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index 5f4899b85..64f1f3529 100644 --- a/client/package.json +++ b/client/package.json @@ -42,6 +42,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "@tanstack/react-table": "^9.1.2", "@tiptap/react": "^2.0.4", "@tiptap/starter-kit": "^2.0.4", "antd": "^4.10.2", @@ -65,7 +66,6 @@ "react-query": "^3.39.1", "react-router-dom": "^6.30.4", "react-syntax-highlighter": "^16.1.0", - "react-table": "^7.7.0", "recharts": "^2.1.10", "safe-stable-stringify": "^2.4.2", "sonner": "^1.5.0", @@ -92,7 +92,6 @@ "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.0.0", "@types/react-syntax-highlighter": "^13.5.2", - "@types/react-table": "^7.7.20", "@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/parser": "^8.57.2", "@vitejs/plugin-react": "^5.2.0", diff --git a/client/src/webpages/dashboard/actions/ActionsDashboard.tsx b/client/src/webpages/dashboard/actions/ActionsDashboard.tsx index bb28d962c..8dbffc2a5 100644 --- a/client/src/webpages/dashboard/actions/ActionsDashboard.tsx +++ b/client/src/webpages/dashboard/actions/ActionsDashboard.tsx @@ -153,53 +153,61 @@ export default function ActionsDashboard() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Penalty', - accessor: 'penalty', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'penalty', - }), - filter: 'includes', - sortType: userPenaltySeveritySort, + header: 'Penalty', + accessorKey: 'penalty', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'penalty', + }), + }, + filterFn: 'includes' as const, + sortFn: userPenaltySeveritySort, }, { - Header: 'ID', - accessor: 'id', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'id', - }), - filter: 'text', - canSort: false, + header: 'ID', + accessorKey: 'id', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'id', + }), + }, + filterFn: 'text' as const, + enableSorting: false, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx b/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx index 96e10840f..ecaf33fea 100644 --- a/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx +++ b/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx @@ -92,36 +92,40 @@ export default function HashBanksDashboard() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Status', - accessor: 'enabled_ratio', - canSort: false, + header: 'Status', + accessorKey: 'enabled_ratio', + enableSorting: false, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/banks/location/LocationBanksDashboard.tsx b/client/src/webpages/dashboard/banks/location/LocationBanksDashboard.tsx index 5968f01ae..ece0c92bc 100644 --- a/client/src/webpages/dashboard/banks/location/LocationBanksDashboard.tsx +++ b/client/src/webpages/dashboard/banks/location/LocationBanksDashboard.tsx @@ -107,31 +107,35 @@ export default function LocationBanksDashboard() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/banks/text/TextBanksDashboard.tsx b/client/src/webpages/dashboard/banks/text/TextBanksDashboard.tsx index 3a3acd365..2764ed7cf 100644 --- a/client/src/webpages/dashboard/banks/text/TextBanksDashboard.tsx +++ b/client/src/webpages/dashboard/banks/text/TextBanksDashboard.tsx @@ -112,42 +112,48 @@ export default function TextBanksDashboard() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Type', - accessor: 'type', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'type', - }), - filter: 'includes', - canSort: false, + header: 'Type', + accessorKey: 'type', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'type', + }), + }, + filterFn: 'includes' as const, + enableSorting: false, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/components/table/Table.test.tsx b/client/src/webpages/dashboard/components/table/Table.test.tsx new file mode 100644 index 000000000..eae7063a3 --- /dev/null +++ b/client/src/webpages/dashboard/components/table/Table.test.tsx @@ -0,0 +1,392 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { describe, expect, it, vi } from 'vitest'; + +import { + ColumnProps, + DefaultColumnFilter, + NumberRangeColumnFilter, +} from './filters'; +import { stringSort } from './sort'; +import Table, { TableColumnDef } from './Table'; + +type TableRow = { + name: ReactNode; + status: string; + values: { id: string; name: string; status: string }; +}; + +const columns = [ + { + header: 'Name', + accessorKey: 'name', + cell: ({ getValue }) => getValue(), + sortFn: stringSort, + sortDescFirst: false, + }, + { header: 'Status', accessorKey: 'status', enableSorting: false }, +] satisfies TableColumnDef[]; + +const data: TableRow[] = [ + { + name: Rendered Zulu, + status: 'Open', + values: { id: 'z', name: 'Zulu', status: 'Open' }, + }, + { + name: Rendered Alpha, + status: 'Closed', + values: { id: 'a', name: 'Alpha', status: 'Closed' }, + }, + { + name: Rendered Alpine, + status: 'Open', + values: { id: 'alpine', name: 'Alpine', status: 'Open' }, + }, +]; + +function renderTable( + tableColumns: TableColumnDef[] = columns, + props = {}, +) { + return render( + + + , + ); +} + +function renderedNames() { + return within(screen.getAllByRole('rowgroup')[1]) + .getAllByRole('row') + .map((row) => within(row).getAllByRole('cell')[0].textContent); +} + +function expectAllRowsVisible() { + expect(screen.getByText('Rendered Zulu')).toBeTruthy(); + expect(screen.getByText('Rendered Alpha')).toBeTruthy(); + expect(screen.getByText('Rendered Alpine')).toBeTruthy(); +} + +const filterFor = + (accessor: string, placeholder: string) => (props: ColumnProps) => + DefaultColumnFilter({ columnProps: props, accessor, placeholder }); + +describe('Table behavior', () => { + it('preserves grouped header spans above leaf headers', () => { + const groupedColumns = [ + { + header: 'Details', + columns, + }, + ] satisfies TableColumnDef[]; + + renderTable(groupedColumns); + + const headerRows = within(screen.getAllByRole('rowgroup')[0]).getAllByRole( + 'row', + ); + expect(headerRows).toHaveLength(2); + expect( + within(headerRows[0]) + .getByRole('columnheader', { name: 'Details' }) + .getAttribute('colspan'), + ).toBe('2'); + expect( + within(headerRows[1]).getByRole('columnheader', { name: /Name/ }), + ).toBeTruthy(); + expect( + within(headerRows[1]).getByRole('columnheader', { name: 'Status' }), + ).toBeTruthy(); + }); + + it('renders accessor values and sorts by raw values only on sortable headers', () => { + renderTable(); + + expect(screen.getByRole('columnheader', { name: /Name/ })).toBeTruthy(); + expect(screen.getByRole('columnheader', { name: 'Status' })).toBeTruthy(); + expectAllRowsVisible(); + + fireEvent.click(screen.getByRole('columnheader', { name: /Name/ })); + expect(renderedNames()).toEqual([ + 'Rendered Alpha', + 'Rendered Alpine', + 'Rendered Zulu', + ]); + + fireEvent.click(screen.getByRole('columnheader', { name: /Name/ })); + expect(renderedNames()).toEqual([ + 'Rendered Zulu', + 'Rendered Alpine', + 'Rendered Alpha', + ]); + + fireEvent.click(screen.getByRole('columnheader', { name: 'Status' })); + expect(renderedNames()).toEqual([ + 'Rendered Zulu', + 'Rendered Alpine', + 'Rendered Alpha', + ]); + }); + + it('does not offer filtering unless a column supplies a filter renderer', () => { + renderTable(); + expect(screen.queryByRole('button', { name: /filter/i })).toBeNull(); + }); + + it('stages multiple filters until Save and supports clearing and removing them', () => { + const filterColumns = [ + { + ...columns[0], + meta: { filter: filterFor('name', 'Filter names') }, + filterFn: 'text', + }, + { + ...columns[1], + meta: { filter: filterFor('status', 'Filter statuses') }, + filterFn: 'text', + }, + ] satisfies TableColumnDef[]; + renderTable(filterColumns); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + const filterMenu = screen + .getByRole('button', { name: 'Save' }) + .closest('.absolute'); + expect(filterMenu).not.toBeNull(); + fireEvent.click(within(filterMenu!).getByText('Name')); + fireEvent.change(screen.getByPlaceholderText('Filter names'), { + target: { value: 'alp' }, + }); + fireEvent.click(within(filterMenu!).getByText('Status')); + fireEvent.change(screen.getByPlaceholderText('Filter statuses'), { + target: { value: 'Closed' }, + }); + expectAllRowsVisible(); + + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByText('Rendered Alpha')).toBeTruthy(); + expect(screen.queryByText('Rendered Alpine')).toBeNull(); + expect(screen.queryByText('Rendered Zulu')).toBeNull(); + expect(screen.getByText('Name: alp')).toBeTruthy(); + expect(screen.getByText('Status: Closed')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + fireEvent.change(screen.getByPlaceholderText('Filter statuses'), { + target: { value: '' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByText('Rendered Alpha')).toBeTruthy(); + expect(screen.getByText('Rendered Alpine')).toBeTruthy(); + expect(screen.queryByText('Rendered Zulu')).toBeNull(); + expect(screen.queryByText('Status: Closed')).toBeNull(); + + const nameChip = screen.getByText('Name: alp'); + const closeIcon = nameChip.parentElement?.querySelector('svg'); + expect(closeIcon).not.toBeNull(); + fireEvent.click(closeIcon!); + expectAllRowsVisible(); + }); + + it('renders links and selects a clicked row using its raw data', () => { + const onSelectRow = vi.fn(); + renderTable(columns, { + rowLinkTo: (row: any) => `/rows/${row.original.values.id}`, + onSelectRow, + }); + + const bodyRows = within(screen.getAllByRole('rowgroup')[1]).getAllByRole( + 'row', + ); + expect( + bodyRows.map((row) => + within(row) + .getAllByRole('link') + .map((link) => link.getAttribute('href')), + ), + ).toEqual([ + ['/rows/z', '/rows/z'], + ['/rows/a', '/rows/a'], + ['/rows/alpine', '/rows/alpine'], + ]); + const alphaRow = screen.getByText('Rendered Alpha').closest('tr')!; + fireEvent.click(alphaRow); + expect(onSelectRow).toHaveBeenCalledTimes(1); + expect(onSelectRow.mock.calls[0][0].original.values.id).toBe('a'); + expect(alphaRow.classList.contains('bg-indigo-100')).toBe(true); + }); + + it('renders one collapsed summary cell per row instead of normal columns', () => { + renderTable(columns, { + isCollapsed: true, + collapsedColumnTitle: 'Summary', + renderCollapsedCell: (row: any) => + `Collapsed ${row.original.values.name}`, + }); + + expect(screen.getAllByRole('columnheader')).toHaveLength(1); + expect(screen.getByRole('columnheader', { name: 'Summary' })).toBeTruthy(); + expect(screen.getAllByRole('cell')).toHaveLength(3); + expect(screen.getByText('Collapsed Zulu')).toBeTruthy(); + expect(screen.getByText('Collapsed Alpha')).toBeTruthy(); + expect(screen.getByText('Collapsed Alpine')).toBeTruthy(); + expect(screen.queryByText('Open')).toBeNull(); + expect(screen.queryByText('Closed')).toBeNull(); + }); + + it('facets raw options by other filters but not the probed column filter', () => { + const facetData: TableRow[] = [ + { + name: Label One, + status: 'Open', + values: { id: 'group-a-1', name: 'Alpha', status: 'Open' }, + }, + { + name: Label Two, + status: 'Open', + values: { id: 'group-a-2', name: 'Bravo', status: 'Open' }, + }, + { + name: Label Three, + status: 'Open', + values: { id: 'group-b-1', name: 'Charlie', status: 'Open' }, + }, + { + name: Label Four, + status: 'Closed', + values: { id: 'group-a-3', name: 'Delta', status: 'Closed' }, + }, + ]; + const FacetFilter = (props: ColumnProps) => ( + <> +
+ {props.preFilteredRows + .map((row) => row.original.values.name) + .sort() + .join(',')} +
+ + + ); + const filterColumns: TableColumnDef[] = [ + { + ...columns[0], + meta: { filter: FacetFilter }, + filterFn: 'text', + }, + { + ...columns[1], + meta: { filter: filterFor('status', 'Filter statuses') }, + filterFn: 'text', + }, + { + header: 'ID', + id: 'id', + accessorFn: (row) => row.values.id, + cell: ({ row }) => `Rendered ${row.original.values.id}`, + meta: { filter: filterFor('id', 'Filter IDs') }, + filterFn: 'text', + }, + ]; + render( + +
+ , + ); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + let menu = screen + .getByRole('button', { name: 'Save' }) + .closest('.absolute')!; + fireEvent.click(within(menu).getByText('Name')); + expect(screen.getByTestId('facets').textContent).toBe( + 'Alpha,Bravo,Charlie,Delta', + ); + fireEvent.click(within(menu).getByText('Status')); + fireEvent.change(screen.getByPlaceholderText('Filter statuses'), { + target: { value: 'Open' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + expect(screen.getByTestId('facets').textContent).toBe( + 'Alpha,Bravo,Charlie', + ); + menu = screen + .getByRole('button', { name: 'Save' }) + .closest('.absolute')!; + fireEvent.click(within(menu).getByText('ID')); + fireEvent.change(screen.getByPlaceholderText('Filter IDs'), { + target: { value: 'group-a' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + expect(screen.getByTestId('facets').textContent).toBe('Alpha,Bravo'); + + fireEvent.change(screen.getByPlaceholderText('Filter facets'), { + target: { value: 'Alpha' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + expect(screen.getByTestId('facets').textContent).toBe('Alpha,Bravo'); + }); + + it('stages an inclusive numeric range and filters raw values on Save', () => { + type NumericRow = { + score: ReactNode; + values: { score: number }; + }; + const numericData: NumericRow[] = [ + { score: Low score, values: { score: 4 } }, + { score: Boundary score, values: { score: 5 } }, + { score: High score, values: { score: 8 } }, + ]; + const numericColumns = [ + { + header: 'Score', + accessorKey: 'score', + cell: ({ getValue }) => getValue(), + meta: { + filter: (props: ColumnProps) => + NumberRangeColumnFilter({ + columnProps: props, + accessor: 'score', + }), + }, + filterFn: 'range', + }, + ] satisfies TableColumnDef[]; + render( + +
+ , + ); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + const filterMenu = screen + .getByRole('button', { name: 'Save' }) + .closest('.absolute'); + expect(filterMenu).not.toBeNull(); + fireEvent.click(within(filterMenu!).getByText('Score')); + fireEvent.change(screen.getByPlaceholderText('min'), { + target: { value: '5' }, + }); + fireEvent.change(screen.getByPlaceholderText('max'), { + target: { value: '8' }, + }); + expect(screen.getByText('Low score')).toBeTruthy(); + expect(screen.getByText('Boundary score')).toBeTruthy(); + expect(screen.getByText('High score')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.queryByText('Low score')).toBeNull(); + expect(screen.getByText('Boundary score')).toBeTruthy(); + expect(screen.getByText('High score')).toBeTruthy(); + }); +}); diff --git a/client/src/webpages/dashboard/components/table/Table.tsx b/client/src/webpages/dashboard/components/table/Table.tsx index 83c4aefbb..5632edcff 100644 --- a/client/src/webpages/dashboard/components/table/Table.tsx +++ b/client/src/webpages/dashboard/components/table/Table.tsx @@ -1,19 +1,21 @@ -/* eslint-disable react/jsx-key */ import SortAmountAsc from '@/icons/lni/Text editor/sort-amount-asc.svg?react'; import SortAmountDsc from '@/icons/lni/Text editor/sort-amount-dsc.svg?react'; +import { flexRender, useTable } from '@tanstack/react-table'; import { ReactNode, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; -import { Column, Row, useFilters, useSortBy, useTable } from 'react-table'; -import { getFilterTypes } from './filters'; +import { features, TableColumnDef, TableData, TableRow } from './tableFeatures'; import TableFilter from './TableFilter'; -export default function Table( +export type TableRowData = TableData; +export type { TableColumnDef, TableRow } from './tableFeatures'; + +export default function Table>( props: { - columns: ReadonlyArray>; - data: readonly object[]; - onSelectRow?: (rowData: Row) => void; - rowLinkTo?: (rowData: Row) => string; + columns: TableColumnDef, any>[]; + data: readonly TData[]; + onSelectRow?: (row: TableRow) => void; + rowLinkTo?: (row: TableRow) => string; topLeftComponent?: ReactNode; topRightComponent?: ReactNode; customMaxHeight?: `max-h-[${number}px]`; @@ -26,9 +28,9 @@ export default function Table( | { isCollapsed?: boolean; collapsedColumnTitle?: string; - renderCollapsedCell?: (row: Row) => ReactNode; + renderCollapsedCell?: (row: TableRow) => ReactNode; } - | {} + | Record ), ) { const { @@ -50,16 +52,20 @@ export default function Table( } = 'isCollapsed' in props ? props : {}; const rowsAreSelectable = onSelectRow !== undefined; + const tableData = useMemo(() => [...data], [data]); - const filterTypes = useMemo(getFilterTypes, []); - - const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = - /* @ts-ignore */ - useTable({ columns, data, filterTypes }, useFilters, useSortBy); - + const table = useTable({ + features, + columns, + data: tableData, + defaultColumn: { + cell: ({ getValue }) => getValue() as ReactNode, + }, + }); + const rows = table.getRowModel().rows; const [selectedRow, setSelectedRow] = useState(null); - const selectRow = (row: Row, rowIndex: number) => { + const selectRow = (row: TableRow, rowIndex: number) => { if (rowsAreSelectable) { setSelectedRow(rowIndex); onSelectRow(row); @@ -79,11 +85,7 @@ export default function Table( > {topLeftComponent} {disableFilter ? null : ( - group.headers, - )} - /> + )} {topRightComponent} @@ -93,10 +95,10 @@ export default function Table( alwaysShowScrollbar ? 'scrollbar-show' : '' } ${customMaxHeight ?? 'max-h-[1200px]'}`} > -
+
- {headerGroups.map((headerGroup, _i) => ( - + {table.getHeaderGroups().map((headerGroup) => ( + {isCollapsed && collapsedColumnTitle ? ( ) : ( - headerGroup.headers.map((column, index) => { - // For some reason when we pass `columns` into useTable, - // the `headers` prop in headerGroups doesn't receive the - // canSort value from each of the columns (it's overwritten) - // to true always. So we pull the canSort value from the - // `columns` variable instead of the headerGroup.headers variable. - // NB: canSort defaults to true - const canSort = - columns.find( - /* @ts-ignore */ - (col) => col.Header === column.Header, - /* @ts-ignore */ - )!.canSort ?? true; - // If we don't set this on the header's column object directly, - // the user can still click the header to sort the row, even - // though the sort UI is hidden. - if (!canSort) { - /* @ts-ignore */ - column.canSort = false; - } + headerGroup.headers.map((header, index) => { + const sorted = header.column.getIsSorted(); return ( ))} - + {rows.map((row, rowIndex) => { - prepareRow(row); const cell = renderCollapsedCell && renderCollapsedCell(row); const cellWithWrapper = rowLinkTo ? ( ) : ( selectRow(row, rowIndex)} > - {row.cells.map((cell, index) => { + {row.getAllCells().map((cell, index, cells) => { const cellWithWrapper = rowLinkTo ? ( - {cell.render('Cell')} + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} ) : (
- {cell.render('Cell')} + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )}
); return (
@@ -104,32 +106,13 @@ export default function Table(
- {column.render('Header')} - {/* @ts-ignore */} - {canSort ? ( - /* @ts-ignore */ - column.isSortedDesc ? ( - /* @ts-ignore */ - ) : column.isSorted ? ( + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {header.column.getCanSort() ? ( + sorted === 'desc' ? ( + + ) : sorted === 'asc' ? ( ) : ( @@ -159,9 +145,8 @@ export default function Table(
(props: { + columns: TableColumn[]; +}) { + const { columns } = props; + + const filterColumns = columns.filter( + (column) => column.getCanFilter() && column.columnDef.meta?.filter, + ); const [menuVisible, setMenuVisible] = useState(false); const [expandedColumnNames, setExpandedColumnNames] = useState([]); @@ -66,7 +73,9 @@ export default function TableFilter(props: { headers: any[] }) { const onSave = () => { for (const [columnId, value] of Object.entries(unsavedFilterValues)) { - headers.find((header) => header.id === columnId)!.setFilter(value); + filterColumns + .find((column) => column.id === columnId)! + .setFilterValue(value); } setMenuVisible(false); }; @@ -74,18 +83,25 @@ export default function TableFilter(props: { headers: any[] }) { const onSetUnsavedFilterValue = (columnId: string, value: any) => { setUnsavedFilterValues({ ...unsavedFilterValues, - [columnId]: value, + [columnId]: + typeof value === 'function' + ? value(unsavedFilterValues[columnId]) + : value, }); }; const removeFilter = (columnId: string) => { setUnsavedFilterValues(omit(unsavedFilterValues, columnId)); - headers.find((header) => header.id === columnId)!.setFilter(undefined); + filterColumns + .find((column) => column.id === columnId)! + .setFilterValue(undefined); }; - const activeFilters = headers.filter((header) => header.filterValue); + const activeFilters = filterColumns.filter((column) => + column.getFilterValue(), + ); - return headers.some((header) => header.filter) ? ( + return filterColumns.length > 0 ? (
))} @@ -133,11 +149,13 @@ export default function TableFilter(props: { headers: any[] }) {
- {headers.map((column, index) => { - if (!column || !column.Header.length || !column.Filter) { + {filterColumns.map((column, index) => { + const label = String(column.columnDef.header); + if (!label.length || !column.columnDef.meta?.filter) { return null; } - const expanded = expandedColumnNames.includes(column.Header); + const expanded = expandedColumnNames.includes(label); + const Renderer = column.columnDef.meta.filter; return (
toggleColumn(column.Header)} + onClick={(_) => toggleColumn(label)} key={`${index}_column_cell`} >
- {column.Header} + {label}
{expanded ? ( @@ -165,13 +183,14 @@ export default function TableFilter(props: { headers: any[] }) { className="flex flex-col px-4 pt-0 pb-4" key={`${index}_content`} > - {column.Filter({ - ...column, - setUnsavedFilterValue: (value: any) => - onSetUnsavedFilterValue(column.id, value), - unsavedFilterValue: unsavedFilterValues[column.id], - onSave, - })} + + onSetUnsavedFilterValue(column.id, value) + } + unsavedFilterValue={unsavedFilterValues[column.id]} + onSave={onSave} + />
)}
diff --git a/client/src/webpages/dashboard/components/table/filters.tsx b/client/src/webpages/dashboard/components/table/filters.tsx index 09da9f3fb..55369ff04 100644 --- a/client/src/webpages/dashboard/components/table/filters.tsx +++ b/client/src/webpages/dashboard/components/table/filters.tsx @@ -2,101 +2,93 @@ import { DatePicker, Input, Select } from 'antd'; import intersection from 'lodash/intersection'; import uniq from 'lodash/uniq'; import { MouseEvent } from 'react'; -import { Row, UseFiltersColumnProps } from 'react-table'; + +import type { + FacetedRow, + FilterRendererProps, + TableData, +} from './tableFeatures'; const { Option } = Select; const { RangePicker } = DatePicker; +type RawRow = { values: Record }; -export type ColumnProps = UseFiltersColumnProps & { - setUnsavedFilterValue: (value: any) => void; - unsavedFilterValue: any; - onSave: () => void; -}; - +export type ColumnProps = + FilterRendererProps; export type FilterProps = { columnProps: ColumnProps; accessor: string; placeholder?: string; }; +const raw = (row: FacetedRow, id: string) => + (row.original as RawRow).values[id]; function onClickFilter(event: MouseEvent) { - // This ensures that clicking the filter doesn't trigger a - // 'sort column' event + // Prevent clicks inside filter controls from sorting the column. event.stopPropagation(); } export function getFilterTypes() { return { - // Override the default text filter to use "startWith" - text: (rows: Row[], id: string, filterValue: any) => { + // Match case-insensitive substrings against the raw column value. + text: (row: FacetedRow, id: string, filterValue: any) => { if (filterValue == null || filterValue.length === 0) { - return rows; + return true; + } + const rowValue = raw(row, id); + if (rowValue == null) { + return false; } - return rows.filter((row) => { - const rowValue = row.original.values[id[0]]; - return rowValue != null - ? String(rowValue) - .toLowerCase() - .includes(String(filterValue).toLowerCase()) - : false; - }); + return String(rowValue) + .toLowerCase() + .includes(String(filterValue).toLowerCase()); }, - // Allow for filtering on options in a predetermined list - includes: (rows: Row[], id: string, filterValue: any) => { + // Allow filtering on options in a predetermined list. + includes: (row: FacetedRow, id: string, filterValue: any) => { if ( filterValue == null || (Array.isArray(filterValue) && filterValue.length === 0) ) { - return rows; + return true; } - return rows.filter((row) => { - const rowValue = row.original.values[id[0]]; - if (rowValue == null) { - return false; - } - if (Array.isArray(rowValue)) { - return intersection(filterValue, rowValue).length > 0; - } - return filterValue.includes(rowValue); - }); + const rowValue = raw(row, id); + if (rowValue == null) { + return false; + } + if (Array.isArray(rowValue)) { + return intersection(filterValue, rowValue).length > 0; + } + return filterValue.includes(rowValue); }, - range: (rows: Row[], id: string, filterValue: any) => { + range: (row: FacetedRow, id: string, filterValue: any) => { if (filterValue == null) { - return rows; + return true; } const start = filterValue[0]; const end = filterValue[1]; - return rows.filter((row) => { - if (start && start > row.original.values[id[0]]) { - return false; - } - if (end && end < row.original.values[id[0]]) { - return false; - } - return true; - }); + const rowValue = raw(row, id); + if (start && start > rowValue) { + return false; + } + if (end && end < rowValue) { + return false; + } + return true; }, - dateRange: (rows: Row[], id: string, filterValue: any) => { + dateRange: (row: FacetedRow, id: string, filterValue: any) => { if (filterValue == null) { - return rows; + return true; } - let start = filterValue[0]; - let end = filterValue[1]; - if (start) { - start = start.format('YYYY-MM-DD'); + const start = filterValue[0]?.format('YYYY-MM-DD'); + const end = filterValue[1]?.format('YYYY-MM-DD'); + const rowValue = raw(row, id); + if (start && start > rowValue) { + return false; } - if (end) { - end = end.format('YYYY-MM-DD'); + if (end && end < rowValue) { + return false; } - return rows.filter((row) => { - if (start && start > row.original.values[id[0]]) { - return false; - } - if (end && end < row.original.values[id[0]]) { - return false; - } - return true; - }); + return true; }, }; } @@ -130,7 +122,7 @@ export function SelectColumnFilter(props: FilterProps) { // using the preFilteredRows const options: (string[] | string)[] = []; preFilteredRows.forEach((row) => { - options.push((row.original as any).values[accessor]); + options.push((row.original as RawRow).values[accessor]); }); const uniqueOptions = uniq(options.flat()); @@ -165,14 +157,14 @@ export function NumberRangeColumnFilter(props: FilterProps) { className="!w-14" onChange={(e) => { if (!e.target.value) { - setUnsavedFilterValue((old = []) => { + setUnsavedFilterValue((old: any[] = []) => { return [undefined, old[1]]; }); return; } const val = parseFloat(e.target.value); if (!isNaN(val)) { - setUnsavedFilterValue((old = []) => { + setUnsavedFilterValue((old: any[] = []) => { return [val, old[1]]; }); } @@ -185,14 +177,14 @@ export function NumberRangeColumnFilter(props: FilterProps) { className="!w-14" onChange={(e) => { if (!e.target.value) { - setUnsavedFilterValue((old = []) => { + setUnsavedFilterValue((old: any[] = []) => { return [old[0], undefined]; }); return; } const val = parseFloat(e.target.value); if (!isNaN(val)) { - setUnsavedFilterValue((old = []) => { + setUnsavedFilterValue((old: any[] = []) => { return [old[0], val]; }); } @@ -208,7 +200,7 @@ export function DateRangeColumnFilter(props: FilterProps) { const { columnProps } = props; const { unsavedFilterValue, setUnsavedFilterValue } = columnProps; - // We wrap this in a div because RangePicker's onClick doesn't work + // RangePicker does not forward onClick, so intercept it on a wrapper. return (
= TableRow; -export function stringSort( - rowA: Row, - rowB: Row, - columnId: IdType, - _desc?: boolean, +export function stringSort( + rowA: Row, + rowB: Row, + columnId: string, ) { const s1 = rowA.original.values[columnId]; const s2 = rowB.original.values[columnId]; @@ -33,11 +33,10 @@ export function stringSort( return 0; } -export function integerSort( - rowA: Row, - rowB: Row, - columnId: IdType, - _desc?: boolean, +export function integerSort( + rowA: Row, + rowB: Row, + columnId: string, ) { // the values come formatted with commas, so we remove all // comma characters before doing any parsing or comparison @@ -46,11 +45,10 @@ export function integerSort( return s1 > s2 ? 1 : s2 > s1 ? -1 : 0; } -export function boolSort( - rowA: Row, - rowB: Row, - columnId: IdType, - _desc?: boolean, +export function boolSort( + rowA: Row, + rowB: Row, + columnId: string, ) { const s1 = rowA.original.values[columnId]; const s2 = rowB.original.values[columnId]; @@ -81,11 +79,11 @@ export function boolSort( * @param columnId - the ID (aka the accessor prop) of the column we're sorting * @returns - -1, 0, or 1 corresponding to the standard sorting return value */ -function enumSort( +function enumSort( precedence: any[], - rowA: Row, - rowB: Row, - columnId: IdType, + rowA: Row, + rowB: Row, + columnId: string, ) { const s1 = rowA.original.values[columnId]; const s2 = rowB.original.values[columnId]; @@ -96,11 +94,10 @@ function enumSort( return precedence.indexOf(s1) > precedence.indexOf(s2) ? 1 : -1; } -export function ruleStatusSort( - rowA: Row, - rowB: Row, - columnId: IdType, - __?: boolean, +export function ruleStatusSort( + rowA: Row, + rowB: Row, + columnId: string, ) { return enumSort( [ @@ -115,11 +112,10 @@ export function ruleStatusSort( ); } -export function reportingRuleStatusSort( - rowA: Row, - rowB: Row, - columnId: IdType, - __?: boolean, +export function reportingRuleStatusSort( + rowA: Row, + rowB: Row, + columnId: string, ) { return enumSort( [ @@ -134,11 +130,10 @@ export function reportingRuleStatusSort( ); } -export function userRoleSort( - rowA: Row, - rowB: Row, - columnId: IdType, - __?: boolean, +export function userRoleSort( + rowA: Row, + rowB: Row, + columnId: string, ) { return enumSort( [GQLUserRole.Admin, GQLUserRole.RulesManager, GQLUserRole.Analyst], @@ -148,11 +143,10 @@ export function userRoleSort( ); } -export function conditionOutcomeSort( - rowA: Row, - rowB: Row, - columnId: IdType, - __?: boolean, +export function conditionOutcomeSort( + rowA: Row, + rowB: Row, + columnId: string, ) { return enumSort( [ @@ -167,11 +161,10 @@ export function conditionOutcomeSort( ); } -export function userPenaltySeveritySort( - rowA: Row, - rowB: Row, - columnId: IdType, - __?: boolean, +export function userPenaltySeveritySort( + rowA: Row, + rowB: Row, + columnId: string, ) { return enumSort( [ @@ -193,11 +186,10 @@ export function userPenaltySeveritySort( * @returns a sort function compatible with react-table */ export function dateSort(dateKey: string) { - return ( - rowA: Row, - rowB: Row, - _columnId: IdType, - _desc?: boolean, + return ( + rowA: Row, + rowB: Row, + _columnId: string, ) => { const a = (rowA.original as unknown as Record)[dateKey]; const b = (rowB.original as unknown as Record)[dateKey]; diff --git a/client/src/webpages/dashboard/components/table/tableFeatures.ts b/client/src/webpages/dashboard/components/table/tableFeatures.ts new file mode 100644 index 000000000..f77e79d02 --- /dev/null +++ b/client/src/webpages/dashboard/components/table/tableFeatures.ts @@ -0,0 +1,61 @@ +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedRowModel, + createFilteredRowModel, + createSortedRowModel, + metaHelper, + rowSortingFeature, + tableFeatures, + type Column, + type ColumnDef, + type Row, +} from '@tanstack/react-table'; +import type { ReactNode } from 'react'; + +import { getFilterTypes } from './filters'; + +export type TableData = Record; +export type FacetedRow = { + original: TData; +}; +export type FilterRendererProps = { + preFilteredRows: readonly FacetedRow[]; + setUnsavedFilterValue: (value: any) => void; + unsavedFilterValue: any; + onSave: () => void; +}; +type TableColumnMeta = { + // Use TypeScript's bivariance pattern to accept filter renderers + // typed for a table's specific row shape. + // https://stackoverflow.com/a/52668133 + filter?: { + bivarianceHack(props: FilterRendererProps): ReactNode; + }['bivarianceHack']; + valueType?: unknown; +}; + +export const features = tableFeatures({ + columnFilteringFeature, + columnFacetingFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + facetedRowModel: createFacetedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: getFilterTypes(), + columnMeta: metaHelper(), +}); + +export type TableFeatures = typeof features; +export type TableRow = Row< + TableFeatures, + TData +>; +export type TableColumnDef< + TData extends TableData = TableData, + TValue = unknown, +> = ColumnDef; +export type TableColumn< + TData extends TableData = TableData, + TValue = unknown, +> = Column; diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx index 9a6cdb919..e0263ae08 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx @@ -4,7 +4,6 @@ import capitalize from 'lodash/capitalize'; import lowerCase from 'lodash/lowerCase'; import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Row } from 'react-table'; import ComponentLoading from '../../../components/common/ComponentLoading'; import CoopBadge from '../components/CoopBadge'; @@ -20,7 +19,7 @@ import { ruleStatusSort, stringSort, } from '../components/table/sort'; -import Table from '../components/table/Table'; +import Table, { TableRow } from '../components/table/Table'; import { GQLConditionOutcome, @@ -85,76 +84,88 @@ export default function ItemInvestigationRuleResults(props: { const columns = useMemo( () => [ { - Header: 'Rule', - accessor: 'rule', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'rule', - }), - filter: 'text', - sortType: stringSort, + header: 'Rule', + accessorKey: 'rule', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'rule', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Result', - accessor: 'result', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'result', - }), - filter: 'includes', - sortType: conditionOutcomeSort, + header: 'Result', + accessorKey: 'result', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'result', + }), + }, + filterFn: 'includes' as const, + sortFn: conditionOutcomeSort, }, { - Header: 'Status', - accessor: 'status', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'status', - }), - filter: 'includes', - sortType: ruleStatusSort, + header: 'Status', + accessorKey: 'status', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'status', + }), + }, + filterFn: 'includes' as const, + sortFn: ruleStatusSort, }, { - Header: 'Policies', - accessor: 'policies', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'policies', - }), - filter: 'includes', - canSort: false, + header: 'Policies', + accessorKey: 'policies', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'policies', + }), + }, + filterFn: 'includes' as const, + enableSorting: false, }, { - Header: 'Tags', - accessor: 'tags', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'tags', - }), - filter: 'includes', - canSort: false, + header: 'Tags', + accessorKey: 'tags', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'tags', + }), + }, + filterFn: 'includes' as const, + enableSorting: false, }, { - Header: 'Actions', - accessor: 'actions', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'actions', - placeholder: 'Filter by action', - }), - filter: 'includes', - sortType: stringSort, + header: 'Actions', + accessorKey: 'actions', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'actions', + placeholder: 'Filter by action', + }), + }, + filterFn: 'includes' as const, + sortFn: stringSort, }, { - Header: '', - accessor: 'edit', - canSort: false, + header: '', + accessorKey: 'edit', + enableSorting: false, }, ], [], @@ -227,6 +238,7 @@ export default function ItemInvestigationRuleResults(props: {
), ruleExecutionResult: ruleResult.result, + values: ruleResult, }; }), [ruleExecutionsHistory, navigate, rules], @@ -273,7 +285,7 @@ export default function ItemInvestigationRuleResults(props: { ); - const onSelectRow = (rowData: Row) => { + const onSelectRow = (rowData: TableRow<(typeof tableData)[number]>) => { const executionResult = ruleExecutionsHistory[rowData.index]; if (executionResult == null) { return; diff --git a/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx b/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx index cb1913528..7595f5940 100644 --- a/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx +++ b/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx @@ -262,42 +262,48 @@ export default function ItemTypesDashboard() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'ID', - accessor: 'id', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'id', - }), - filter: 'text', - canSort: false, + header: 'ID', + accessorKey: 'id', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'id', + }), + }, + filterFn: 'text' as const, + enableSorting: false, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/items/ItemActionHistory.tsx b/client/src/webpages/dashboard/items/ItemActionHistory.tsx index 6d31e5c32..6253e555e 100644 --- a/client/src/webpages/dashboard/items/ItemActionHistory.tsx +++ b/client/src/webpages/dashboard/items/ItemActionHistory.tsx @@ -179,30 +179,30 @@ export default function ItemActionHistory(props: { const columns = useMemo( () => [ { - Header: 'Actions', - accessor: 'actions', - canSort: false, + header: 'Actions', + accessorKey: 'actions', + enableSorting: false, }, { - Header: 'Policies', - accessor: 'policies', - canSort: false, + header: 'Policies', + accessorKey: 'policies', + enableSorting: false, }, { - Header: 'Decision Time', - accessor: 'ts', + header: 'Decision Time', + accessorKey: 'ts', sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, { - Header: 'Actor', - accessor: 'actor', - canSort: false, + header: 'Actor', + accessorKey: 'actor', + enableSorting: false, }, { - Header: 'Source(s)', - accessor: 'source', - canSort: false, + header: 'Source(s)', + accessorKey: 'source', + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx b/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx index 5b436656e..6e6e0e5e5 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx @@ -6,9 +6,8 @@ import { assertUnreachable } from '@/utils/misc'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { gql } from '@apollo/client/core'; import { useState } from 'react'; -import type { Column } from 'react-table'; -import Table from '../components/table/Table'; +import Table, { TableColumnDef } from '../components/table/Table'; import FullScreenLoading from '@/components/common/FullScreenLoading'; import type { TimeWindow } from '../rules/dashboard/visualization/RulesDashboardInsights'; @@ -150,52 +149,58 @@ export default function ManualReviewDecisionsTable(props: { ); const columns = data?.getDecisionsTable - .reduce[]>( + .reduce< + (TableColumnDef<{ + [key: string]: string | number; + groupedByKey: string; + name: string; + }> & { accessorKey: string; header: string })[] + >( (acc, { action_id, type }) => { if (action_id !== null && action_id !== undefined) { const action = data?.myOrg?.actions.find((a) => a.id === action_id); if ( action === undefined || - acc.find((it) => it.Header === action.name) + acc.find((it) => it.header === action.name) ) { return acc; } - acc.push({ accessor: action_id, Header: action.name }); + acc.push({ accessorKey: action_id, header: action.name }); } // These should be caught in the previous if with the action if (type === 'CUSTOM_ACTION' || type === 'RELATED_ACTION') { return acc; } const title = getReadableNameFromDecisionType(type); - if (!acc.find((it) => it.Header === title)) { + if (!acc.find((it) => it.header === title)) { acc.push({ - accessor: type, - Header: getReadableNameFromDecisionType(type), + accessorKey: type, + header: getReadableNameFromDecisionType(type), }); } return acc; }, - [{ accessor: 'name', Header: 'Name' }], + [{ accessorKey: 'name', header: 'Name' }], ) .sort((a, b) => { - if (typeof a.Header !== 'string') { + if (typeof a.header !== 'string') { return -1; } - if (typeof b.Header !== 'string') { + if (typeof b.header !== 'string') { return 1; } - return a.Header === 'Name' + return a.header === 'Name' ? -1 - : b.Header === 'Name' + : b.header === 'Name' ? 1 - : a.Header.localeCompare(b.Header); + : a.header.localeCompare(b.header); }); const filledInData = columns ? groupedByKey?.map((it) => { const obj = { ...it }; columns.forEach((col) => { - const accessor = col.accessor as string; + const accessor = col.accessorKey; if (obj[accessor] === undefined) { obj[accessor] = 0; } diff --git a/client/src/webpages/dashboard/mrt/ManualReviewQueueJobsPreview.tsx b/client/src/webpages/dashboard/mrt/ManualReviewQueueJobsPreview.tsx index 0e96d44df..e420efc99 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewQueueJobsPreview.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewQueueJobsPreview.tsx @@ -2,7 +2,6 @@ import { safeFormat } from '@/utils/time'; import { gql } from '@apollo/client'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { Row } from 'react-table'; import ComponentLoading from '../../../components/common/ComponentLoading'; import DashboardHeader from '../components/DashboardHeader'; @@ -12,7 +11,7 @@ import { SelectColumnFilter, } from '../components/table/filters'; import { stringSort } from '../components/table/sort'; -import Table from '../components/table/Table'; +import Table, { TableRow } from '../components/table/Table'; import { useGQLManualReviewQueueJobsPreviewQuery } from '../../../graphql/generated'; import { filterNullOrUndefined } from '../../../utils/collections'; @@ -116,33 +115,37 @@ export default function ManualReviewQueueJobsPreview() { const columns = useMemo( () => [ { - Header: 'Preview', - accessor: 'preview', - canSort: false, + header: 'Preview', + accessorKey: 'preview', + enableSorting: false, }, { - Header: 'Policies', - accessor: 'policies', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'policies', - }), - filter: 'includes', - canSort: false, + header: 'Policies', + accessorKey: 'policies', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'policies', + }), + }, + filterFn: 'includes' as const, + enableSorting: false, }, { - Header: 'Created At', - accessor: 'createdAt', - Filter: (props: ColumnProps) => - DateRangeColumnFilter({ - columnProps: props, - accessor: 'createdAt', - placeholder: '', - }), - filter: 'dateRange', + header: 'Created At', + accessorKey: 'createdAt', + meta: { + filter: (props: ColumnProps) => + DateRangeColumnFilter({ + columnProps: props, + accessor: 'createdAt', + placeholder: '', + }), + }, + filterFn: 'dateRange' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, ], [], @@ -218,7 +221,7 @@ export default function ManualReviewQueueJobsPreview() { throw Error(`Queue not found for ID ${queueId}`); } - const rowLinkTo = (row: Row) => { + const rowLinkTo = (row: TableRow<(typeof tableData)[number]>) => { // I don't know why but the jobs do not ever render unless you put a fake lock token // at the end of the URL, so the `/1` is actually necessary here return `/dashboard/manual_review/queues/review/${queueId}/${row.original.jobId}/1`; diff --git a/client/src/webpages/dashboard/mrt/ManualReviewQueuesDashboard.tsx b/client/src/webpages/dashboard/mrt/ManualReviewQueuesDashboard.tsx index 8ac60d345..16d88a6ef 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewQueuesDashboard.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewQueuesDashboard.tsx @@ -538,87 +538,93 @@ export default function ManualReviewQueuesDashboard() { filterNullOrUndefined([ columnVisibility.favoriteQueues ? { - Header: '', - accessor: 'favoriteQueues', - canSort: false, + header: '', + accessorKey: 'favoriteQueues', + enableSorting: false, } : undefined, columnVisibility.id ? { - Header: 'ID', - accessor: 'id', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'id', - placeholder: 'Queue ID', - }), - filter: 'text', - sortType: stringSort, + header: 'ID', + accessorKey: 'id', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'id', + placeholder: 'Queue ID', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, } : undefined, columnVisibility.name ? { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - placeholder: 'My Queue', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + placeholder: 'My Queue', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, } : undefined, columnVisibility.description ? { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - sortType: stringSort, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, } : undefined, columnVisibility.oldestTaskAge ? { - Header: 'Oldest Task Age', - accessor: 'oldestTaskAge', - sortType: dateSort('oldestJobCreatedAt'), + header: 'Oldest Task Age', + accessorKey: 'oldestTaskAge', + sortFn: dateSort('oldestJobCreatedAt'), } : undefined, columnVisibility.pendingJobCount ? { - Header: 'Pending Jobs', - accessor: 'pendingJobCount', - sortType: integerSort, + header: 'Pending Jobs', + accessorKey: 'pendingJobCount', + sortFn: integerSort, } : undefined, columnVisibility.startReviewing ? { - Header: '', - accessor: 'startReviewing', - canSort: false, + header: '', + accessorKey: 'startReviewing', + enableSorting: false, } : undefined, columnVisibility.mutations ? { - Header: '', - accessor: 'mutations', - canSort: false, + header: '', + accessorKey: 'mutations', + enableSorting: false, } : undefined, userHasPermissions(data?.me?.permissions, [ GQLUserPermission.ManageOrg, ]) && columnVisibility.deleteJobs ? { - Header: '', - accessor: 'deleteJobs', - canSort: false, + header: '', + accessorKey: 'deleteJobs', + enableSorting: false, } : undefined, previewJobsViewEnabled && @@ -627,9 +633,9 @@ export default function ManualReviewQueuesDashboard() { ]) && columnVisibility.previewJobs ? { - Header: '', - accessor: 'previewJobs', - canSort: false, + header: '', + accessorKey: 'previewJobs', + enableSorting: false, } : undefined, ]), diff --git a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx index 6d2a1695e..666cf60bc 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx @@ -365,45 +365,45 @@ export default function ManualReviewRecentDecisions() { filterNullOrUndefined([ columnVisibility.decisionTime ? { - Header: 'Decision Time', - accessor: 'decisionTime', + header: 'Decision Time', + accessorKey: 'decisionTime', sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, } : undefined, columnVisibility.decisions ? { - Header: 'Decisions', - accessor: 'decisions', - canSort: false, + header: 'Decisions', + accessorKey: 'decisions', + enableSorting: false, } : undefined, columnVisibility.decisionReason ? { - Header: 'Decision Reason', - accessor: 'decisionReason', - canSort: false, + header: 'Decision Reason', + accessorKey: 'decisionReason', + enableSorting: false, } : undefined, columnVisibility.policies ? { - Header: 'Policies', - accessor: 'policies', - canSort: false, + header: 'Policies', + accessorKey: 'policies', + enableSorting: false, } : undefined, columnVisibility.reviewer ? { - Header: 'Reviewer', - accessor: 'reviewer', - canSort: false, + header: 'Reviewer', + accessorKey: 'reviewer', + enableSorting: false, } : undefined, columnVisibility.queue ? { - Header: 'Queue', - accessor: 'queue', - canSort: true, + header: 'Queue', + accessorKey: 'queue', + enableSorting: true, } : undefined, ]), @@ -1013,13 +1013,13 @@ export default function ManualReviewRecentDecisions() {
setSelectedDecision( - rowData.original.values.originalDecisionData, + rowData.original.values + .originalDecisionData as GQLManualReviewDecision, ) } topLeftComponent={selectedDecision ? null : tableControls} @@ -1028,7 +1028,10 @@ export default function ManualReviewRecentDecisions() { collapsedColumnTitle="Decisions" renderCollapsedCell={(row) => { const values = row.original.values as { - decisionColorNamePairs: { name: string; colors: string }[]; + decisionColorNamePairs: { + name: string; + colorVariant: BadgeColorVariant; + }[]; reviewerId: string; createdAt: string | Date; }; @@ -1037,13 +1040,13 @@ export default function ManualReviewRecentDecisions() {
{values.decisionColorNamePairs.map( - ({ name, colors }, index) => ( -
( + - {name} -
+ colorVariant={colorVariant} + label={name} + shapeVariant="pill" + /> ), )}
diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx index 926b511a9..c8d61e9d2 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx @@ -103,10 +103,10 @@ export default function MergedReportsComponent(props: { const columns = useMemo( () => [ - { Header: 'Reported By', accessor: 'reportedBy' }, - { Header: 'Reported For', accessor: 'reportedFor' }, - { Header: 'Reason', accessor: 'reason' }, - { Header: 'Report Time', accessor: 'reportTime' }, + { header: 'Reported By', accessorKey: 'reportedBy' }, + { header: 'Reported For', accessorKey: 'reportedFor' }, + { header: 'Reason', accessorKey: 'reason' }, + { header: 'Report Time', accessorKey: 'reportTime' }, ], [], ); diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/user/ManualReviewJobCurrentJobsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/user/ManualReviewJobCurrentJobsComponent.tsx index 9e8b93cf4..1d306aeb9 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/user/ManualReviewJobCurrentJobsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/user/ManualReviewJobCurrentJobsComponent.tsx @@ -47,15 +47,15 @@ export default function ManualReviewCurrentJobsComponent(props: { const columns = useMemo( () => [ { - Header: 'Queue', - accessor: 'queue', - canSort: true, + header: 'Queue', + accessorKey: 'queue', + enableSorting: true, }, { - Header: 'Created At', - accessor: 'createdAt', + header: 'Created At', + accessorKey: 'createdAt', sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, ], [], diff --git a/client/src/webpages/dashboard/ncmec/NcmecReportsDashboard.tsx b/client/src/webpages/dashboard/ncmec/NcmecReportsDashboard.tsx index 1283f0a5d..ec2e3f960 100644 --- a/client/src/webpages/dashboard/ncmec/NcmecReportsDashboard.tsx +++ b/client/src/webpages/dashboard/ncmec/NcmecReportsDashboard.tsx @@ -326,106 +326,118 @@ export default function NcmecReportsDashboard() { filterNullOrUndefined([ columnVisibility.date ? { - Header: 'Date', - accessor: 'date', - sortType: stringSort, + header: 'Date', + accessorKey: 'date', + sortFn: stringSort, sortDescFirst: true, - Filter: (props: ColumnProps) => - DateRangeColumnFilter({ - columnProps: props, - accessor: 'date', - placeholder: '', - }), - filter: 'dateRange', + meta: { + filter: (props: ColumnProps) => + DateRangeColumnFilter({ + columnProps: props, + accessor: 'date', + placeholder: '', + }), + }, + filterFn: 'dateRange' as const, } : undefined, columnVisibility.reviewer ? { - Header: 'Reviewer', - accessor: 'reviewer', - filter: 'includes', - sortType: stringSort, - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'reviewer', - }), + header: 'Reviewer', + accessorKey: 'reviewer', + filterFn: 'includes' as const, + sortFn: stringSort, + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'reviewer', + }), + }, } : undefined, columnVisibility.status ? { // Cell renders the colored Tag from row.status; the filter // reads the plain string from row.original.values.status. - Header: 'Status', - accessor: 'status', - filter: 'includes', - sortType: stringSort, - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'status', - }), + header: 'Status', + accessorKey: 'status', + filterFn: 'includes' as const, + sortFn: stringSort, + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'status', + }), + }, } : undefined, columnVisibility.reportId ? { - Header: 'Report ID', - accessor: 'reportId', - filter: 'text', - canSort: false, - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'reportId', - placeholder: 'Report ID', - }), + header: 'Report ID', + accessorKey: 'reportId', + filterFn: 'text' as const, + enableSorting: false, + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'reportId', + placeholder: 'Report ID', + }), + }, } : undefined, columnVisibility.userId ? { - Header: 'User ID', - accessor: 'userId', - filter: 'text', - canSort: false, - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'userId', - placeholder: 'User ID', - }), + header: 'User ID', + accessorKey: 'userId', + filterFn: 'text' as const, + enableSorting: false, + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'userId', + placeholder: 'User ID', + }), + }, } : undefined, columnVisibility.userItemType ? { - Header: 'User Item Type', - accessor: 'userItemType', - filter: 'text', - canSort: false, - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'userItemType', - placeholder: 'User Type', - }), + header: 'User Item Type', + accessorKey: 'userItemType', + filterFn: 'text' as const, + enableSorting: false, + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'userItemType', + placeholder: 'User Type', + }), + }, } : undefined, columnVisibility.reportedMedia - ? { Header: 'Reported Media', accessor: 'reportedMedia' } + ? { header: 'Reported Media', accessorKey: 'reportedMedia' } : undefined, columnVisibility.additionalFiles - ? { Header: 'Additional Files', accessor: 'additionalFiles' } + ? { header: 'Additional Files', accessorKey: 'additionalFiles' } : undefined, columnVisibility.reportedMessages - ? { Header: 'Reported Messages', accessor: 'reportedMessages' } + ? { header: 'Reported Messages', accessorKey: 'reportedMessages' } : undefined, columnVisibility.isTest - ? { Header: 'Test Report', accessor: 'isTest' } + ? { header: 'Test Report', accessorKey: 'isTest' } : undefined, columnVisibility.lastError - ? { Header: 'Last Error', accessor: 'lastError' } + ? { header: 'Last Error', accessorKey: 'lastError' } : undefined, columnVisibility.action - ? { Header: 'Action', accessor: 'action' } + ? { header: 'Action', accessorKey: 'action' } : undefined, ]), [columnVisibility], diff --git a/client/src/webpages/dashboard/rules/dashboard/ReportingRulesDashboard.tsx b/client/src/webpages/dashboard/rules/dashboard/ReportingRulesDashboard.tsx index 86b7296f4..a92ed9c27 100644 --- a/client/src/webpages/dashboard/rules/dashboard/ReportingRulesDashboard.tsx +++ b/client/src/webpages/dashboard/rules/dashboard/ReportingRulesDashboard.tsx @@ -5,7 +5,6 @@ import lowerCase from 'lodash/lowerCase'; import { MouseEvent, useCallback, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useNavigate } from 'react-router-dom'; -import { Column, Row } from 'react-table'; import FullScreenLoading from '../../../../components/common/FullScreenLoading'; import CoopButton from '../../components/CoopButton'; @@ -25,7 +24,7 @@ import { reportingRuleStatusSort, stringSort, } from '../../components/table/sort'; -import Table from '../../components/table/Table'; +import Table, { TableRow } from '../../components/table/Table'; import TruncatedListTableCell from '../../components/table/TruncatedListTableCell'; import TruncatedTextTableCell from '../../components/table/TruncatedTextTableCell'; import UserWithAvatar from '../../components/UserWithAvatar'; @@ -144,10 +143,6 @@ export default function ReportingRulesDashboard() { const navigate = useNavigate(); - const rowLinkTo = (row: Row) => { - return `info/${row.original.values.id}`; - }; - const permissions = rulesQueryParams.data?.me?.permissions; const canEditLiveRules = userHasPermissions(permissions, [ GQLUserPermission.MutateLiveRules, @@ -209,69 +204,78 @@ export default function ReportingRulesDashboard() { ); const columns = useMemo( - () => - [ - { - Header: 'Rule', - accessor: 'name', - Filter: (props: ColumnProps) => + () => [ + { + header: 'Rule', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => DefaultColumnFilter({ columnProps: props, accessor: 'name', }), - filter: 'text', - sortType: stringSort, }, - { - Header: 'Owner', - accessor: 'owner', - Filter: (props: ColumnProps) => + filterFn: 'text' as const, + sortFn: stringSort, + }, + { + header: 'Owner', + accessorKey: 'owner', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'owner', }), - filter: 'includes', - canSort: false, }, - { - Header: 'Status', - accessor: 'status', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: 'Status', + accessorKey: 'status', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'status', }), - filter: 'includes', - sortType: reportingRuleStatusSort, }, - { - Header: 'Policies', - accessor: 'policies', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + sortFn: reportingRuleStatusSort, + }, + { + header: 'Policies', + accessorKey: 'policies', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'policies', }), - filter: 'includes', - canSort: false, }, - { - Header: 'Item Types', - accessor: 'itemTypes', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: 'Item Types', + accessorKey: 'itemTypes', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'itemTypes', }), - filter: 'includes', - canSort: false, - }, - { - Header: '', - accessor: 'mutations', - canSort: false, }, - ] as (Column & { canSort?: boolean })[], + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: '', + accessorKey: 'mutations', + enableSorting: false, + }, + ], [], ); @@ -330,6 +334,10 @@ export default function ReportingRulesDashboard() { [mutations, dataValues], ); + const rowLinkTo = (row: TableRow<(typeof tableData)[number]>) => { + return `info/${row.original.values.id}`; + }; + if (rulesQueryParams.error) { throw rulesQueryParams.error; } diff --git a/client/src/webpages/dashboard/rules/dashboard/RulesDashboard.tsx b/client/src/webpages/dashboard/rules/dashboard/RulesDashboard.tsx index 5e42988c2..efb5aa29e 100644 --- a/client/src/webpages/dashboard/rules/dashboard/RulesDashboard.tsx +++ b/client/src/webpages/dashboard/rules/dashboard/RulesDashboard.tsx @@ -14,7 +14,6 @@ import lowerCase from 'lodash/lowerCase'; import { MouseEvent, useCallback, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useNavigate } from 'react-router-dom'; -import { Column, Row } from 'react-table'; import FullScreenLoading from '../../../../components/common/FullScreenLoading'; import CoopButton from '../../components/CoopButton'; @@ -33,7 +32,7 @@ import { SelectColumnFilter, } from '../../components/table/filters'; import { ruleStatusSort, stringSort } from '../../components/table/sort'; -import Table from '../../components/table/Table'; +import Table, { TableRow } from '../../components/table/Table'; import TruncatedListTableCell from '../../components/table/TruncatedListTableCell'; import TruncatedTextTableCell from '../../components/table/TruncatedTextTableCell'; import UserWithAvatar from '../../components/UserWithAvatar'; @@ -191,10 +190,6 @@ export default function RulesDashboard() { const navigate = useNavigate(); const [timeDivision, setTimeDivision] = useState('DAY'); - const rowLinkTo = (row: Row) => { - return `info/${row.original.values.id}`; - }; - const permissions = data?.me?.permissions; const favoritedRules = data?.me?.favoriteRules.map((rule) => rule?.id); const canEditLiveRules = userHasPermissions(permissions, [ @@ -281,86 +276,97 @@ export default function RulesDashboard() { ); const columns = useMemo( - () => - [ - { - Header: '', - accessor: 'favoriteRules', - canSort: false, - }, - { - Header: 'Created', - accessor: 'dateCreated', - Filter: (props: ColumnProps) => + () => [ + { + header: '', + accessorKey: 'favoriteRules', + enableSorting: false, + }, + { + header: 'Created', + accessorKey: 'dateCreated', + meta: { + filter: (props: ColumnProps) => DateRangeColumnFilter({ columnProps: props, accessor: 'dateCreated', }), - filter: 'dateRange', - sortDescFirst: true, - sortType: stringSort, }, - { - Header: 'Rule', - accessor: 'name', - Filter: (props: ColumnProps) => + filterFn: 'dateRange' as const, + sortDescFirst: true, + sortFn: stringSort, + }, + { + header: 'Rule', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => DefaultColumnFilter({ columnProps: props, accessor: 'name', }), - filter: 'text', - sortType: stringSort, }, - { - Header: 'Owner', - accessor: 'owner', - Filter: (props: ColumnProps) => + filterFn: 'text' as const, + sortFn: stringSort, + }, + { + header: 'Owner', + accessorKey: 'owner', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'owner', }), - filter: 'includes', - canSort: false, }, - { - Header: 'Status', - accessor: 'status', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: 'Status', + accessorKey: 'status', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'status', }), - filter: 'includes', - sortType: ruleStatusSort, }, - { - Header: 'Policies', - accessor: 'policies', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + sortFn: ruleStatusSort, + }, + { + header: 'Policies', + accessorKey: 'policies', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'policies', }), - filter: 'includes', - canSort: false, }, - { - Header: 'Item Types', - accessor: 'itemTypes', - Filter: (props: ColumnProps) => + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: 'Item Types', + accessorKey: 'itemTypes', + meta: { + filter: (props: ColumnProps) => SelectColumnFilter({ columnProps: props, accessor: 'itemTypes', }), - filter: 'includes', - canSort: false, - }, - { - Header: '', - accessor: 'mutations', - canSort: false, }, - ] as (Column & { canSort?: boolean })[], + filterFn: 'includes' as const, + enableSorting: false, + }, + { + header: '', + accessorKey: 'mutations', + enableSorting: false, + }, + ], [], ); @@ -469,6 +475,10 @@ export default function RulesDashboard() { [mutations, dataValues, onAddFavoriteRule, onRemoveFavoriteRule], ); + const rowLinkTo = (row: TableRow<(typeof tableData)[number]>) => { + return `info/${row.original.values.id}`; + }; + if (error) { throw error; } diff --git a/client/src/webpages/dashboard/rules/info/insights/ReportingRuleInsightsSamplesTable.tsx b/client/src/webpages/dashboard/rules/info/insights/ReportingRuleInsightsSamplesTable.tsx index 0707ace07..846da6671 100644 --- a/client/src/webpages/dashboard/rules/info/insights/ReportingRuleInsightsSamplesTable.tsx +++ b/client/src/webpages/dashboard/rules/info/insights/ReportingRuleInsightsSamplesTable.tsx @@ -11,7 +11,6 @@ import uniq from 'lodash/uniq'; import { useMemo, useState } from 'react'; import { CSVLink } from 'react-csv'; import { Link } from 'react-router-dom'; -import { Row } from 'react-table'; import ComponentLoading from '../../../../../components/common/ComponentLoading'; import CopyTextComponent from '../../../../../components/common/CopyTextComponent'; @@ -23,7 +22,7 @@ import { SelectColumnFilter, } from '../../../components/table/filters'; import { ruleStatusSort, stringSort } from '../../../components/table/sort'; -import Table from '../../../components/table/Table'; +import Table, { TableRow } from '../../../components/table/Table'; import { GQLFieldType, @@ -235,17 +234,19 @@ export default function ReportingRuleInsightsSamplesTable(props: { ); return distinctSignalNames.map((signalName) => ({ - Header: signalName, - accessor: signalName, - Filter: (props: ColumnProps) => - NumberRangeColumnFilter({ - columnProps: props, - accessor: signalName, - placeholder: '', - }), - filter: 'between', + header: signalName, + accessorKey: signalName, + meta: { + filter: (props: ColumnProps) => + NumberRangeColumnFilter({ + columnProps: props, + accessor: signalName, + placeholder: '', + }), + }, + filterFn: 'range' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, })); }, [allSignals, samples]); @@ -289,65 +290,71 @@ export default function ReportingRuleInsightsSamplesTable(props: { const columns = useMemo(() => { return [ { - Header: 'Timestamp', - accessor: 'time', - Filter: (props: ColumnProps) => - DateRangeColumnFilter({ - columnProps: props, - accessor: 'date', - placeholder: '', - }), - filter: 'dateRange', + header: 'Timestamp', + accessorKey: 'time', + meta: { + filter: (props: ColumnProps) => + DateRangeColumnFilter({ + columnProps: props, + accessor: 'date', + placeholder: '', + }), + }, + filterFn: 'dateRange' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, { - Header: 'Status', - accessor: 'status', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'status', - placeholder: 'Live', - }), - filter: 'includes', - sortType: ruleStatusSort, + header: 'Status', + accessorKey: 'status', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'status', + placeholder: 'Live', + }), + }, + filterFn: 'includes' as const, + sortFn: ruleStatusSort, }, { - Header: 'Item', - accessor: 'item', - canSort: false, + header: 'Item', + accessorKey: 'item', + enableSorting: false, }, { - Header: 'Item Type', - accessor: 'itemTypeName', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'itemTypeName', - }), - filter: 'includes', - sortType: stringSort, + header: 'Item Type', + accessorKey: 'itemTypeName', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'itemTypeName', + }), + }, + filterFn: 'includes' as const, + sortFn: stringSort, }, { - Header: 'ID', - accessor: 'id', // accessor is the "key" in the data - canSort: false, + header: 'ID', + accessorKey: 'id', // accessor is the "key" in the data + enableSorting: false, }, { - Header: 'Creator ID', - accessor: 'creatorId', - canSort: false, + header: 'Creator ID', + accessorKey: 'creatorId', + enableSorting: false, }, ...extraColumns, ]; }, [extraColumns]); const tableData = useMemo( () => - (dataValues ?? []).map((values) => { + (dataValues ?? []).flatMap((values) => { const parsedItem = JSON.parse(values.itemData); if (itemTypeFields == null) { - return ; + return []; } const fields = itemTypeFields[values.itemTypeName]; if (!fields || fields.length === 0) { @@ -394,56 +401,60 @@ export default function ReportingRuleInsightsSamplesTable(props: { }), ); - return { - id: ( - {values.id}} - /> - ), - itemTypeName: , - creatorId: ( - event.stopPropagation()} - target="_blank" - > - {values.creatorId} - - ), - item: ( - it.length > 0).join('\n')} - displayValue={ -
{item}
- } - footerItems={videoUrls.map((videoUrl) => ( - { - setVideoPlayerUrl(videoUrl); - }} - /> - ))} - /> - ), - time:
{values.time}
, - status: ( -
- {values.id}
} + /> + ), + itemTypeName: , + creatorId: ( + event.stopPropagation()} + target="_blank" + > + {values.creatorId} + + ), + item: ( + it.length > 0).join('\n')} + displayValue={ +
{item}
+ } + footerItems={videoUrls.map((videoUrl) => ( + { + setVideoPlayerUrl(videoUrl); + }} + /> + ))} /> - - ), - values, - ...Object.fromEntries( - extraColumns.map((it) => [ - it.accessor, - (values as { [key: string]: any })[it.accessor], - ]), - ), - }; + ), + time:
{values.time}
, + status: ( +
+ +
+ ), + values, + ...Object.fromEntries( + extraColumns.map((it) => [ + it.accessorKey, + Object.entries(values).find( + ([key]) => key === it.accessorKey, + )?.[1], + ]), + ), + }, + ]; }), [dataValues, itemTypeFields, extraColumns], ); @@ -452,12 +463,12 @@ export default function ReportingRuleInsightsSamplesTable(props: { throw error ?? priorRuleVersionError ?? signalsError!; } - const onSelectRow = (row: Row) => { + const onSelectRow = (row: TableRow<(typeof tableData)[number]>) => { dataValues.length > 0 && setDetailViewData({ visible: true, item: (() => { - const rowData = dataValues[row.index]; + const rowData = row.original.values; return { identifier: { id: rowData.id, typeId: rowData.itemTypeId }, date: rowData.time, @@ -535,7 +546,6 @@ export default function ReportingRuleInsightsSamplesTable(props: {
({ - Header: signalName, - accessor: signalName, - Filter: (props: ColumnProps) => - NumberRangeColumnFilter({ - columnProps: props, - accessor: signalName, - placeholder: '', - }), - filter: 'between', + header: signalName, + accessorKey: signalName, + meta: { + filter: (props: ColumnProps) => + NumberRangeColumnFilter({ + columnProps: props, + accessor: signalName, + placeholder: '', + }), + }, + filterFn: 'range' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, })); }, [allSignals, samples]); @@ -455,65 +456,71 @@ export default function RuleInsightsSamplesTable(props: { ruleId: string }) { const columns = useMemo(() => { return [ { - Header: 'Timestamp', - accessor: 'time', - Filter: (props: ColumnProps) => - DateRangeColumnFilter({ - columnProps: props, - accessor: 'date', - placeholder: '', - }), - filter: 'dateRange', + header: 'Timestamp', + accessorKey: 'time', + meta: { + filter: (props: ColumnProps) => + DateRangeColumnFilter({ + columnProps: props, + accessor: 'date', + placeholder: '', + }), + }, + filterFn: 'dateRange' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, { - Header: 'Status', - accessor: 'status', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'status', - placeholder: 'Live', - }), - filter: 'includes', - sortType: ruleStatusSort, + header: 'Status', + accessorKey: 'status', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'status', + placeholder: 'Live', + }), + }, + filterFn: 'includes' as const, + sortFn: ruleStatusSort, }, { - Header: 'Content', - accessor: 'content', - canSort: false, + header: 'Content', + accessorKey: 'content', + enableSorting: false, }, { - Header: 'Item Type', - accessor: 'itemTypeName', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'itemTypeName', - }), - filter: 'includes', - sortType: stringSort, + header: 'Item Type', + accessorKey: 'itemTypeName', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'itemTypeName', + }), + }, + filterFn: 'includes' as const, + sortFn: stringSort, }, { - Header: 'ID', - accessor: 'id', // accessor is the "key" in the data - canSort: false, + header: 'ID', + accessorKey: 'id', // accessor is the "key" in the data + enableSorting: false, }, { - Header: 'User ID', - accessor: 'userId', - canSort: false, + header: 'User ID', + accessorKey: 'userId', + enableSorting: false, }, ...extraColumns, ]; }, [extraColumns]); const tableData = useMemo( () => - dataValues?.map((values) => { + (dataValues ?? []).flatMap((values) => { const parsedContent = JSON.parse(values.content); if (itemTypeFields == null) { - return ; + return []; } const fields = itemTypeFields[values.itemTypeName]; if (!fields || fields.length === 0) { @@ -563,56 +570,62 @@ export default function RuleInsightsSamplesTable(props: { ruleId: string }) { }), ); - return { - id: ( - {values.id}} - /> - ), - itemTypeName: , - userId: ( - event.stopPropagation()} - target="_blank" - > - {values.userId} - - ), - content: ( - it.length > 0).join('\n')} - displayValue={ -
{content}
- } - footerItems={videoUrls.map((videoUrl) => ( - { - setVideoPlayerUrl(videoUrl); - }} - /> - ))} - /> - ), - time:
{values.time}
, - status: ( -
- {values.id}
} /> - - ), - values, - ...Object.fromEntries( - extraColumns.map((it) => [ - it.accessor, - (values as any)[it.accessor], - ]), - ), - }; + ), + itemTypeName: , + userId: ( + event.stopPropagation()} + target="_blank" + > + {values.userId} + + ), + content: ( + it.length > 0) + .join('\n')} + displayValue={ +
{content}
+ } + footerItems={videoUrls.map((videoUrl) => ( + { + setVideoPlayerUrl(videoUrl); + }} + /> + ))} + /> + ), + time:
{values.time}
, + status: ( +
+ +
+ ), + values, + ...Object.fromEntries( + extraColumns.map((it) => [ + it.accessorKey, + Object.entries(values).find( + ([key]) => key === it.accessorKey, + )?.[1], + ]), + ), + }, + ]; }), [dataValues, itemTypeFields, extraColumns], ); @@ -621,11 +634,11 @@ export default function RuleInsightsSamplesTable(props: { ruleId: string }) { throw error ?? priorRuleVersionError ?? signalsError!; } - const onSelectRow = (row: Row) => { + const onSelectRow = (row: TableRow<(typeof tableData)[number]>) => { setDetailViewData({ visible: true, item: (() => { - const rowData = dataValues![row.index]; + const rowData = row.original.values; return { identifier: { id: rowData.id, typeId: rowData.itemTypeId }, date: rowData.time, @@ -703,9 +716,8 @@ export default function RuleInsightsSamplesTable(props: { ruleId: string }) {
diff --git a/client/src/webpages/dashboard/userStrikes/PolicyScoresTab.tsx b/client/src/webpages/dashboard/userStrikes/PolicyScoresTab.tsx index 2bf54441b..ec287b1d1 100644 --- a/client/src/webpages/dashboard/userStrikes/PolicyScoresTab.tsx +++ b/client/src/webpages/dashboard/userStrikes/PolicyScoresTab.tsx @@ -430,19 +430,19 @@ function ChildPoliciesTable(props: { const columns = useMemo( () => [ { - Header: 'Sub-Policy', - accessor: 'name', - canSort: false, + header: 'Sub-Policy', + accessorKey: 'name', + enableSorting: false, }, { - Header: 'User Strike Score', - accessor: 'userStrikeCount', // accessor is the "key" in the data - canSort: false, + header: 'User Strike Score', + accessorKey: 'userStrikeCount', // accessor is the "key" in the data + enableSorting: false, }, { - Header: 'Apply to sub-policies', - accessor: 'applyUserStrikeCountConfigToChildren', // accessor is the "key" in the data - canSort: false, + header: 'Apply to sub-policies', + accessorKey: 'applyUserStrikeCountConfigToChildren', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/userStrikes/StrikeAnalyticsTab.tsx b/client/src/webpages/dashboard/userStrikes/StrikeAnalyticsTab.tsx index feb3c6f78..3ed7e49df 100644 --- a/client/src/webpages/dashboard/userStrikes/StrikeAnalyticsTab.tsx +++ b/client/src/webpages/dashboard/userStrikes/StrikeAnalyticsTab.tsx @@ -191,19 +191,19 @@ function RecentUserStrikeActionsTable() { const columns = useMemo( () => [ { - Header: 'User', - accessor: 'user', - canSort: false, + header: 'User', + accessorKey: 'user', + enableSorting: false, }, { - Header: 'Action Taken', - accessor: 'action', - canSort: false, + header: 'Action Taken', + accessorKey: 'action', + enableSorting: false, }, { - Header: 'Date', - accessor: 'date', - canSort: false, + header: 'Date', + accessorKey: 'date', + enableSorting: false, }, ], [], diff --git a/client/src/webpages/dashboard/userStrikes/StrikeEnabledActionsTab.tsx b/client/src/webpages/dashboard/userStrikes/StrikeEnabledActionsTab.tsx index d01be4540..0db141ddc 100644 --- a/client/src/webpages/dashboard/userStrikes/StrikeEnabledActionsTab.tsx +++ b/client/src/webpages/dashboard/userStrikes/StrikeEnabledActionsTab.tsx @@ -41,36 +41,40 @@ export default function StrikeEnabledActionsTab() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - }), - filter: 'text', - canSort: false, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + }), + }, + filterFn: 'text' as const, + enableSorting: false, }, { - Header: 'Description', - accessor: 'description', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'description', - }), - filter: 'text', - canSort: false, + header: 'Description', + accessorKey: 'description', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'description', + }), + }, + filterFn: 'text' as const, + enableSorting: false, }, { - Header: 'Enable Strikes', - accessor: 'enableStrikes', // accessor is the "key" in the data - canSort: false, + header: 'Enable Strikes', + accessorKey: 'enableStrikes', // accessor is the "key" in the data + enableSorting: false, }, { - Header: '', - accessor: 'CustomAction', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'customAction', // accessor is the "key" in the data + enableSorting: false, }, ], [], diff --git a/client/src/webpages/settings/ManageUsers.tsx b/client/src/webpages/settings/ManageUsers.tsx index 5ded20f41..3e3d4d351 100644 --- a/client/src/webpages/settings/ManageUsers.tsx +++ b/client/src/webpages/settings/ManageUsers.tsx @@ -314,70 +314,80 @@ export default function ManageUsers() { const columns = useMemo( () => [ { - Header: 'Name', - accessor: 'name', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'name', - placeholder: 'Jane Smith', - }), - filter: 'text', - sortType: stringSort, + header: 'Name', + accessorKey: 'name', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'name', + placeholder: 'Jane Smith', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Email', - accessor: 'email', - Filter: (props: ColumnProps) => - DefaultColumnFilter({ - columnProps: props, - accessor: 'email', - placeholder: 'jane@mywebsite.com', - }), - filter: 'text', - sortType: stringSort, + header: 'Email', + accessorKey: 'email', + meta: { + filter: (props: ColumnProps) => + DefaultColumnFilter({ + columnProps: props, + accessor: 'email', + placeholder: 'jane@mywebsite.com', + }), + }, + filterFn: 'text' as const, + sortFn: stringSort, }, { - Header: 'Role', - accessor: 'role', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'role', - placeholder: 'Filter by role', - }), - filter: 'includes', - sortType: userRoleSort, + header: 'Role', + accessorKey: 'role', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'role', + placeholder: 'Filter by role', + }), + }, + filterFn: 'includes' as const, + sortFn: userRoleSort, }, { - Header: 'Approval Status', - accessor: 'approvalStatus', - Filter: (props: ColumnProps) => - SelectColumnFilter({ - columnProps: props, - accessor: 'approvalStatus', - placeholder: 'Filter by status', - }), - filter: 'includes', - sortType: boolSort, + header: 'Approval Status', + accessorKey: 'approvalStatus', + meta: { + filter: (props: ColumnProps) => + SelectColumnFilter({ + columnProps: props, + accessor: 'approvalStatus', + placeholder: 'Filter by status', + }), + }, + filterFn: 'includes' as const, + sortFn: boolSort, }, { - Header: 'Date Created', - accessor: 'dateCreated', - Filter: (props: ColumnProps) => - DateRangeColumnFilter({ - columnProps: props, - accessor: 'dateCreated', - placeholder: '', - }), - filter: 'dateRange', + header: 'Date Created', + accessorKey: 'dateCreated', + meta: { + filter: (props: ColumnProps) => + DateRangeColumnFilter({ + columnProps: props, + accessor: 'dateCreated', + placeholder: '', + }), + }, + filterFn: 'dateRange' as const, sortDescFirst: true, - sortType: stringSort, + sortFn: stringSort, }, { - Header: '', - accessor: 'mutations', // accessor is the "key" in the data - canSort: false, + header: '', + accessorKey: 'mutations', // accessor is the "key" in the data + enableSorting: false, }, ], [],