Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 99 additions & 3 deletions frontend/src/components/TableView/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import type { ColumnVisibilityGroup } from './TableToolBar'

type TableStateInUrl = 'sorting' | 'columnfilters' | 'pagination'

const TEXT_FILTER_MODE_OPTIONS = ['equals', 'contains', 'startsWith'] as const
type TextFilterModeOption = (typeof TEXT_FILTER_MODE_OPTIONS)[number]

const isEmptyFilterValue = (value: unknown): boolean => {
if (Array.isArray(value)) {
return value.every(item => item === '' || item === null || item === undefined)
Expand All @@ -58,6 +61,91 @@ const sanitizeColumnFilters = (filters: MRT_ColumnFiltersState): MRT_ColumnFilte
return filters.filter(filter => !isEmptyFilterValue(filter.value))
}

const getColumnId = <T extends MRT_RowData>(column: MRT_ColumnDef<T>) => {
if (column.id) return String(column.id)
if (typeof column.accessorKey === 'string') return column.accessorKey
if (column.accessorKey !== undefined) return String(column.accessorKey)
return undefined
}

const isIdLikeColumnId = (columnId: string) => {
const lastSegment = columnId.split('.').pop() ?? columnId
return lastSegment === 'id' || lastSegment === 'rid' || lastSegment === 'lid' || lastSegment.endsWith('_id')
}

const isRangeLikeFilterVariant = (variant: MRT_ColumnDef<MRT_RowData>['filterVariant']) => {
return (
variant === 'range' ||
variant === 'range-slider' ||
variant === 'date-range' ||
variant === 'datetime-range' ||
variant === 'time-range'
)
}

const applyColumnFilterModeDefaults = <T extends MRT_RowData>(
columns: MRT_ColumnDef<T>[],
idFieldName: keyof T
): {
columns: MRT_ColumnDef<T>[]
columnFilterFns: Record<string, TextFilterModeOption>
} => {
const columnFilterFns: Record<string, TextFilterModeOption> = {}

const mapColumn = (column: MRT_ColumnDef<T>): MRT_ColumnDef<T> => {
if (column.columns) {
return {
...column,
columns: column.columns.map(mapColumn),
}
}

const columnId = getColumnId(column)
if (!columnId) return column

const idFieldId = String(idFieldName)
const hasCustomFilterFn = typeof column.filterFn === 'function'
const isRangeFilter = isRangeLikeFilterVariant(column.filterVariant)

if (columnId === idFieldId) {
columnFilterFns[columnId] = 'equals'
return {
...column,
filterFn: typeof column.filterFn === 'function' ? column.filterFn : column.filterFn ?? 'equals',
enableColumnFilterModes: false,
columnFilterModeOptions: ['equals'],
}
}

if (isIdLikeColumnId(columnId)) {
columnFilterFns[columnId] = 'equals'
return {
...column,
filterFn: typeof column.filterFn === 'function' ? column.filterFn : column.filterFn ?? 'equals',
enableColumnFilterModes: false,
columnFilterModeOptions: ['equals'],
}
}

if (hasCustomFilterFn || isRangeFilter) {
return {
...column,
enableColumnFilterModes: false,
columnFilterModeOptions: null,
}
}

columnFilterFns[columnId] = 'equals'
return {
...column,
enableColumnFilterModes: true,
columnFilterModeOptions: [...TEXT_FILTER_MODE_OPTIONS],
}
}

return { columns: columns.map(mapColumn), columnFilterFns }
}

/*
TableView takes in the data and columns of a table, and handles
rendering the actual table and saving & loading its state via url.
Expand Down Expand Up @@ -137,6 +225,13 @@ export const TableView = <T extends MRT_RowData>({
const user = useUser()
const { setIdList } = usePageContext<T>()

const { columns: preparedColumns, columnFilterFns: initialColumnFilterFns } = useMemo(
() => applyColumnFilterModeDefaults(columns, idFieldName),
[columns, idFieldName]
)

const allowColumnFilterModes = (enableColumnFilterModes ?? true) && !serverSidePagination

useEffect(() => {
setSqlLimit(pagination.pageSize)
setSqlOffset(pagination.pageIndex * pagination.pageSize)
Expand Down Expand Up @@ -282,7 +377,7 @@ export const TableView = <T extends MRT_RowData>({
}

const table = useMaterialReactTable({
columns: columns,
columns: preparedColumns,
data: data || [],
muiTableProps: {
sx: {
Expand Down Expand Up @@ -339,6 +434,7 @@ export const TableView = <T extends MRT_RowData>({
},
initialState: {
columnVisibility: visibleColumns,
columnFilterFns: initialColumnFilterFns,
},
onColumnFiltersChange: setColumnFilters,
/**
Expand Down Expand Up @@ -440,8 +536,8 @@ export const TableView = <T extends MRT_RowData>({
},
enableDensityToggle: false,
enableGlobalFilter: false,
enableColumnFilterModes: enableColumnFilterModes && !serverSidePagination,
columnFilterModeOptions: ['fuzzy', 'contains', 'startsWith', 'endsWith', 'equals'],
enableColumnFilterModes: allowColumnFilterModes,
columnFilterModeOptions: [...TEXT_FILTER_MODE_OPTIONS],
enableColumnActions: false,
enableHiding: true,
enableTopToolbar: false,
Expand Down
97 changes: 97 additions & 0 deletions frontend/src/tests/components/TableViewFilterModes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, expect, it } from '@jest/globals'
import '@testing-library/jest-dom'
import { render } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { store } from '@/redux/store'
import { PageContextProvider } from '@/components/Page'
import { TableView } from '@/components/TableView/TableView'
import { getLastMaterialReactTableOptions } from 'material-react-table'
import { NotificationContextProvider } from '@/components/Notification'

type CapturedMrtOptions = {
enableColumnFilterModes?: boolean
columnFilterModeOptions?: string[]
columns: Array<{
accessorKey?: string
enableColumnFilterModes?: boolean
columnFilterModeOptions?: string[] | null
filterVariant?: string
}>
initialState?: {
columnFilterFns?: Record<string, string>
}
}

type Row = {
rid: number
title: string
year: number
}

const renderTable = (columns: React.ComponentProps<typeof TableView<Row>>['columns']) => {
render(
<Provider store={store}>
<MemoryRouter>
<NotificationContextProvider>
<PageContextProvider
editRights={{}}
idFieldName="rid"
viewName="test"
createTitle={() => ''}
createSubtitle={() => ''}
>
<TableView<Row>
title="Test"
idFieldName="rid"
columns={columns}
visibleColumns={{}}
data={[]}
isFetching={false}
selectorFn={() => {}}
/>
</PageContextProvider>
</NotificationContextProvider>
</MemoryRouter>
</Provider>
)
}

describe('TableView filter modes', () => {
it('defaults to equals and exposes allowed filter modes for non-id columns', () => {
renderTable([
{ accessorKey: 'rid', header: 'Id' },
{ accessorKey: 'title', header: 'Title' },
{ accessorKey: 'year', header: 'Year', filterVariant: 'range' },
])

const options = getLastMaterialReactTableOptions<CapturedMrtOptions>()
expect(options).toBeTruthy()
if (!options) return

expect(options.enableColumnFilterModes).toEqual(true)
expect(options.columnFilterModeOptions).toEqual(['equals', 'contains', 'startsWith'])

const ridColumn = options.columns.find(c => c.accessorKey === 'rid')
expect(ridColumn).toBeTruthy()
if (!ridColumn) return
expect(ridColumn.enableColumnFilterModes).toEqual(false)
expect(ridColumn.columnFilterModeOptions).toEqual(['equals'])

const titleColumn = options.columns.find(c => c.accessorKey === 'title')
expect(titleColumn).toBeTruthy()
if (!titleColumn) return
expect(titleColumn.enableColumnFilterModes).toEqual(true)
expect(titleColumn.columnFilterModeOptions).toEqual(['equals', 'contains', 'startsWith'])

const yearColumn = options.columns.find(c => c.accessorKey === 'year')
expect(yearColumn).toBeTruthy()
if (!yearColumn) return
expect(yearColumn.enableColumnFilterModes).toEqual(false)
expect(yearColumn.columnFilterModeOptions).toEqual(null)

expect(options.initialState?.columnFilterFns?.rid).toEqual('equals')
expect(options.initialState?.columnFilterFns?.title).toEqual('equals')
expect(options.initialState?.columnFilterFns?.year).toBeUndefined()
})
})
7 changes: 7 additions & 0 deletions frontend/src/tests/mocks/material-react-table.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'material-react-table'

declare module 'material-react-table' {
export const getLastMaterialReactTableOptions: <T = unknown>() => T | null
}

export {}
8 changes: 7 additions & 1 deletion frontend/src/tests/mocks/materialReactTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ type MockTable = {
getColumn: (id: string) => { columnDef: { header: string } }
}

let lastMaterialReactTableOptions: unknown = null

export const getLastMaterialReactTableOptions = <T,>(): T | null => {
return (lastMaterialReactTableOptions as T | null) ?? null
}

export const useMaterialReactTable = <T extends object>(options: T): T & MockTable => ({
...options,
...(lastMaterialReactTableOptions = options),
getPrePaginationRowModel: () => ({ rows: [] }),
getColumn: (id: string) => ({ columnDef: { header: id } }),
})
Expand Down
Loading