mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
feat: column filter mode selection
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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 } }),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user