Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 11x 11x 11x 6x 11x 11x 11x 11x 11x 11x 11x 11x 11x 3x 3x 3x 3x 3x 11x 20x 13x 13x 20x 7x 7x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x | import { i18n } from '@/helpers/i18n';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { ReactNode, useEffect, useState } from 'react';
/**
* Compact multi-select filter for header columns with discrete values.
* Options come from the values actually present in the data
* (`column.getAllUniqueValues()`). The column header sits directly above, so
* we skip the floating label and use the placeholder (from
* `meta.filterPlaceholder`) for hinting when nothing is selected.
*
* Display rule:
* - No selection → the placeholder text.
* - Selected, default → comma-separated values (truncated with `…` when
* they overflow the narrow header cell).
* - Selected + `meta.renderSelectOption` → the renderer's output for each
* value, space-separated (e.g. flag icons for the country column).
* @component
* @param props - Component props.
* - `column` - A TanStack column with `meta.filterVariant === "select"`.
* @returns A 32px-tall MUI `Select` bound to the column's debounced filter.
* @example
* ```tsx
* // Supplier column — plain text, no custom renderer.
* <SelectColumnFilter column={supplierColumn} />
* // No selection → shows "BVV, HiMedia, etc..." placeholder.
* // Picking "BVV" + "HiMedia" → "BVV, HiMedia" in the field,
* // column.setFilterValueDebounced(["BVV", "HiMedia"]).
*
* // Country column — declared meta.renderSelectOption: code => flag emoji.
* <SelectColumnFilter column={countryColumn} />
* // Picking "US" + "CN" → "🇺🇸 🇨🇳" rendered in the field;
* // dropdown rows also show the flag next to each option.
* ```
* @source
*/
export default function SelectColumnFilter({ column }: FilterVariantInputProps) {
const filterValue = column.getFilterValue() as string[] | undefined;
const [columnFilterValue, setColumnFilterValue] = useState<string[]>(filterValue ?? []);
// Content-based key so the effect only fires when the external filter
// actually changes — not on every render, where `getFilterValue()` may
// return a fresh array reference with the same contents and would otherwise
// clobber in-progress local state.
const filterKey = (filterValue ?? []).join(' ');
useEffect(() => {
setColumnFilterValue(filterValue ?? []);
}, [filterKey]);
const columnFilterOptions = (column.getAllUniqueValues() as (string | number)[]).map(String);
const hasOptions = columnFilterOptions.length > 0;
const columnHeader = column.getHeaderText();
const renderSelectOption = column.columnDef.meta?.renderSelectOption;
const placeholder =
column.columnDef.meta?.filterPlaceholder ??
i18n('filter_by_label', [columnHeader?.toLowerCase() ?? '']);
const handleChange = (event: SelectChangeEvent<string[]>) => {
const { value } = event.target;
const next = typeof value === 'string' ? value.split(',') : value;
setColumnFilterValue(next);
column.setFilterValueDebounced(next);
};
const renderValue = (selected: string[]): ReactNode => {
if (selected.length === 0) {
return <span style={{ color: 'rgba(0, 0, 0, 0.4)' }}>{placeholder}</span>;
}
if (renderSelectOption) {
// Space-separated rendered nodes — e.g. "🇺🇸 🇨🇳 🇩🇪".
return selected.map((value, idx) => (
<span key={value} style={{ marginRight: idx < selected.length - 1 ? 4 : 0 }}>
{renderSelectOption(value)}
</span>
));
}
return selected.join(', ');
};
return (
<FormControl size="small" fullWidth sx={{ m: 0, mr: 0.5 }}>
<Select
multiple
displayEmpty
value={columnFilterValue}
onChange={handleChange}
disabled={!hasOptions}
renderValue={renderValue}
inputProps={{ 'aria-label': columnHeader }}
sx={{
height: 32,
// `fontSize: inherit` bypasses the project-wide MuiSelect/Input
// theme overrides and picks up the TableCell's default, so the
// filter text matches the body rows.
fontSize: 'inherit',
'& .MuiSelect-select': {
py: 0,
fontSize: 'inherit',
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
},
}}
MenuProps={{
slotProps: { paper: { sx: { maxHeight: 300 } } },
}}
>
{columnFilterOptions.map((option) => (
<MenuItem key={option} value={option} dense>
<Checkbox
size="small"
checked={columnFilterValue.includes(option)}
sx={{ p: 0.5, mr: 0.5 }}
/>
<ListItemText primary={renderSelectOption ? renderSelectOption(option) : option} />
</MenuItem>
))}
</Select>
</FormControl>
);
}
|