import clsx from 'clsx'; import classes from './MRT_FilterTextInput.module.css'; import { type MouseEvent, useEffect, useMemo, useRef, useState } from 'react'; import { ActionIcon, Autocomplete, Badge, Box, MultiSelect, Select, TextInput, type TextInputProps, } from '@mantine/core'; import { DateInput } from '@mantine/dates'; import { useDebouncedValue } from '@mantine/hooks'; import { localizedFilterOption } from '../../fns/filterFns'; import { type MRT_Header, type MRT_RowData, type MRT_TableInstance, } from '../../types'; import { parseFromValuesOrFunc } from '../../utils/utils'; interface Props extends TextInputProps { header: MRT_Header; rangeFilterIndex?: number; table: MRT_TableInstance; } export const MRT_FilterTextInput = ({ header, rangeFilterIndex, table, ...rest }: Props) => { 'use no memo'; const { options: { columnFilterDisplayMode, columnFilterModeOptions, icons: { IconX }, localization, mantineFilterAutocompleteProps, mantineFilterDateInputProps, mantineFilterMultiSelectProps = { clearable: true, }, mantineFilterSelectProps, mantineFilterTextInputProps, manualFiltering, }, refs: { filterInputRefs }, setColumnFilterFns, } = table; const { column } = header; const { columnDef } = column; const arg = { column, rangeFilterIndex, table }; const textInputProps = { ...parseFromValuesOrFunc(mantineFilterTextInputProps, arg), ...parseFromValuesOrFunc(columnDef.mantineFilterTextInputProps, arg), ...rest, }; const selectProps = { ...parseFromValuesOrFunc(mantineFilterSelectProps, arg), ...parseFromValuesOrFunc(columnDef.mantineFilterSelectProps, arg), }; const multiSelectProps = { ...parseFromValuesOrFunc(mantineFilterMultiSelectProps, arg), ...parseFromValuesOrFunc(columnDef.mantineFilterMultiSelectProps, arg), }; const dateInputProps = { ...parseFromValuesOrFunc(mantineFilterDateInputProps, arg), ...parseFromValuesOrFunc(columnDef.mantineFilterDateInputProps, arg), }; const autoCompleteProps = { ...parseFromValuesOrFunc(mantineFilterAutocompleteProps, arg), ...parseFromValuesOrFunc(columnDef.mantineFilterAutocompleteProps, arg), }; const isRangeFilter = columnDef.filterVariant === 'range' || columnDef.filterVariant === 'date-range' || rangeFilterIndex !== undefined; const isSelectFilter = columnDef.filterVariant === 'select'; const isMultiSelectFilter = columnDef.filterVariant === 'multi-select'; const isDateFilter = columnDef.filterVariant === 'date' || columnDef.filterVariant === 'date-range'; const isAutoCompleteFilter = columnDef.filterVariant === 'autocomplete'; const allowedColumnFilterOptions = columnDef?.columnFilterModeOptions ?? columnFilterModeOptions; const currentFilterOption = columnDef._filterFn; const filterChipLabel = ['empty', 'notEmpty'].includes(currentFilterOption) ? localizedFilterOption(localization, currentFilterOption) : ''; const filterPlaceholder = !isRangeFilter ? (textInputProps?.placeholder ?? localization.filterByColumn?.replace( '{column}', String(columnDef.header), )) : rangeFilterIndex === 0 ? localization.min : rangeFilterIndex === 1 ? localization.max : ''; const facetedUniqueValues = column.getFacetedUniqueValues(); const filterSelectOptions = useMemo( () => ( autoCompleteProps?.data ?? selectProps?.data ?? multiSelectProps?.data ?? ((isAutoCompleteFilter || isSelectFilter || isMultiSelectFilter) && facetedUniqueValues ? Array.from(facetedUniqueValues.keys()) .filter((key) => key !== null) .sort((a, b) => a.localeCompare(b)) : []) ) //@ts-ignore .filter((o: any) => o !== undefined && o !== null), [ autoCompleteProps?.data, facetedUniqueValues, isAutoCompleteFilter, isMultiSelectFilter, isSelectFilter, multiSelectProps?.data, selectProps?.data, ], ); const isMounted = useRef(false); const [filterValue, setFilterValue] = useState(() => isMultiSelectFilter ? (column.getFilterValue() as string[]) || [] : isRangeFilter ? (column.getFilterValue() as [string, string])?.[ rangeFilterIndex as number ] || '' : ((column.getFilterValue() as string) ?? ''), ); const [debouncedFilterValue] = useDebouncedValue( filterValue, manualFiltering ? 400 : 200, ); //send debounced filterValue to table instance useEffect(() => { if (!isMounted.current) return; if (isRangeFilter) { column.setFilterValue((old: [string, string]) => { const newFilterValues = Array.isArray(old) ? old : ['', '']; newFilterValues[rangeFilterIndex as number] = debouncedFilterValue as string; return newFilterValues; }); } else { column.setFilterValue(debouncedFilterValue ?? undefined); } }, [debouncedFilterValue]); //receive table filter value and set it to local state useEffect(() => { if (!isMounted.current) { isMounted.current = true; return; } const tableFilterValue = column.getFilterValue(); if (tableFilterValue === undefined) { handleClear(); } else if (isRangeFilter && rangeFilterIndex !== undefined) { setFilterValue( ((tableFilterValue ?? ['', '']) as [string, string])[rangeFilterIndex], ); } else { setFilterValue(tableFilterValue ?? ''); } }, [column.getFilterValue()]); const handleClear = () => { if (isMultiSelectFilter) { setFilterValue([]); column.setFilterValue([]); } else if (isRangeFilter) { setFilterValue(''); column.setFilterValue((old: [string | undefined, string | undefined]) => { const newFilterValues = Array.isArray(old) ? old : ['', '']; newFilterValues[rangeFilterIndex as number] = undefined; return newFilterValues; }); // This is from Mantine v6 but it also applies for v7 // https://github.com/mantinedev/mantine/issues/4716#issuecomment-1702699688 } else if (isSelectFilter) { setFilterValue(null); column.setFilterValue(null); } else { setFilterValue(''); column.setFilterValue(undefined); } }; const handleClearEmptyFilterChip = () => { if (isMultiSelectFilter) { setFilterValue([]); column.setFilterValue([]); } else { setFilterValue(''); column.setFilterValue(undefined); } setColumnFilterFns((prev) => ({ ...prev, [header.id]: allowedColumnFilterOptions?.[0] ?? 'fuzzy', })); }; const { className, ...commonProps } = { 'aria-label': filterPlaceholder, className: clsx( 'mrt-filter-text-input', classes.root, isDateFilter ? classes['date-filter'] : isRangeFilter ? classes['range-filter'] : !filterChipLabel && classes['not-filter-chip'], ), disabled: !!filterChipLabel, onChange: setFilterValue, onClick: (event: MouseEvent) => event.stopPropagation(), placeholder: filterPlaceholder, style: { ...(isMultiSelectFilter ? multiSelectProps?.style : isSelectFilter ? selectProps?.style : isDateFilter ? dateInputProps?.style : textInputProps?.style), }, title: filterPlaceholder, value: isMultiSelectFilter && !Array.isArray(filterValue) ? [] : filterValue, variant: 'unstyled', } as const; const ClearButton = filterValue ? ( ) : null; if (columnDef.Filter) { return ( <>{columnDef.Filter?.({ column, header, rangeFilterIndex, table })} ); } if (filterChipLabel) { return ( {filterChipLabel} ); } if (isMultiSelectFilter) { return ( setFilterValue(value)} ref={(node) => { if (node) { filterInputRefs.current[`${column.id}-${rangeFilterIndex ?? 0}`] = node; if (multiSelectProps.ref && typeof multiSelectProps.ref !== 'function') { multiSelectProps.ref.current = node; } } }} rightSection={ filterValue?.toString()?.length && multiSelectProps?.clearable ? ClearButton : undefined } style={commonProps.style} /> ); } if (isSelectFilter) { return (