feat: column filter mode selection

This commit is contained in:
karilint
2026-04-23 11:41:40 +03:00
parent 8d67a67140
commit fabb15913e
4 changed files with 210 additions and 4 deletions
@@ -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)
@@ -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.
@@ -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)
@@ -282,7 +377,7 @@ export const TableView = <T extends MRT_RowData>({
}
const table = useMaterialReactTable({
columns: columns,
columns: preparedColumns,
data: data || [],
muiTableProps: {
sx: {
@@ -339,6 +434,7 @@ export const TableView = <T extends MRT_RowData>({
},
initialState: {
columnVisibility: visibleColumns,
columnFilterFns: initialColumnFilterFns,
},
onColumnFiltersChange: setColumnFilters,
/**
@@ -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,
@@ -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
View File
@@ -0,0 +1,7 @@
import 'material-react-table'
declare module 'material-react-table' {
export const getLastMaterialReactTableOptions: <T = unknown>() => T | null
}
export {}
@@ -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 } }),
})