import * as React from "react"; import { Box, Button, Chip, Paper, TextField, Autocomplete, Typography, } from "@mui/material"; import DoneIcon from "@mui/icons-material/Done"; import FilterListIcon from "@mui/icons-material/FilterList"; import { ResourceField, ResourceMode } from "../types/config"; function FilterAutocomplete({ options, value, label, onChange, }: { options: string[]; value: string[]; label: string; onChange: (val: string[]) => void; }) { const listboxRef = React.useRef(null); const scrollPosRef = React.useRef(0); const [open, setOpen] = React.useState(false); const [frozenValue, setFrozenValue] = React.useState(value); const sortedOptions = React.useMemo(() => { const sel = new Set(frozenValue); const picked: string[] = []; const rest: string[] = []; for (const o of options) { if (sel.has(o)) picked.push(o); else rest.push(o); } return [...picked, ...rest]; }, [options, frozenValue]); return ( { setOpen(true); setFrozenValue(value); }} onClose={() => { setOpen(false); setFrozenValue(value); }} options={sortedOptions} value={value} getOptionKey={(option) => option} onChange={(_, val) => onChange(val.length > 0 ? val : [])} ListboxProps={{ ref: listboxRef, onScroll: (e) => { scrollPosRef.current = (e.target as HTMLUListElement).scrollTop; }, }} renderOption={(props, option, { selected }) => { const { key, ...rest } = props; return (
  • {selected ? : } {option}
  • ); }} renderTags={(tagValue, getTagProps) => { const maxChips = 1; return ( <> {tagValue.slice(0, maxChips).map((tag, index) => { const { key, ...tagProps } = getTagProps({ index }); return setOpen(true)} sx={{ cursor: 'pointer' }} />; })} {tagValue.length > maxChips && ( setOpen(true)} sx={{ cursor: 'pointer' }} /> )} ); }} renderInput={(params) => } sx={{ '& .MuiOutlinedInput-root': { minHeight: '3rem', py: 0.5 } }} /> ); } function extractOptions( fieldName: string, field: ResourceField, data: any[] ): string[] { const values = new Set(); if (field.options) return field.options; if (!data) return []; const pull = (item: any): string | null => { if (item == null) return null; if (typeof item === "string") return item; if (typeof item !== "object") return String(item); const df = field.displayField; if (!df) { debugger; return null; } if (Array.isArray(df)) { const parts = df.map((k) => item[k]).filter((v) => v != null); if (parts.length > 0) return parts.join(" "); } else { const v = item[df]; if (v != null) return String(v); } debugger; return null; }; for (const row of data) { const v = row[fieldName]; if (v == null) continue; if (Array.isArray(v)) { for (const el of v) { const label = pull(el); if (label) values.add(label); } } else { const label = pull(v); if (label) values.add(label); } } // console.log('extracted', fieldName, Array.from(values).sort()) return Array.from(values).sort(); } function renderFilterInput( fieldName: string, field: ResourceField, options: string[], value: any, onChange: (key: string, val: any) => void ) { const filterType = field.filterType; if (filterType === "number-range") { const rangeVal = (value as { min?: string; max?: string }) || {}; return ( onChange("min", e.target.value || undefined)} sx={{ width: 100 }} /> onChange("max", e.target.value || undefined)} sx={{ width: 100 }} /> ); } if (filterType === "date-range") { const rangeVal = (value as { start?: string; end?: string }) || {}; return ( onChange("start", e.target.value || undefined)} InputLabelProps={{ shrink: true }} sx={{ width: 170 }} /> onChange("end", e.target.value || undefined)} InputLabelProps={{ shrink: true }} sx={{ width: 170 }} /> ); } const selected = Array.isArray(value) ? value : []; return ( onChange("value", val.length > 0 ? val : undefined)} /> ); } export interface FilterBarProps { fields: Record; filterableFields: string[]; mode: ResourceMode; data?: any[]; appliedValues: Record; onApply: (values: Record) => void; onClear: () => void; } export default function FilterBar({ fields, filterableFields, data, appliedValues, onApply, onClear, }: FilterBarProps) { const [open, setOpen] = React.useState(false); const [draft, setDraft] = React.useState>(() => ({ ...appliedValues })); React.useEffect(() => { if (!open) setDraft({ ...appliedValues }); }, [appliedValues, open]); if (!filterableFields || filterableFields.length === 0) return null; const activeCount = Object.keys(appliedValues).filter((k) => { const v = appliedValues[k]; if (v == null || v === "") return false; if (typeof v === "object" && Object.values(v).every((x) => x == null || x === "")) return false; return true; }).length; const handleApply = () => onApply({ ...draft }); const handleClear = () => { setDraft({}); onClear(); }; const updateDraft = (fieldName: string, key: string, val: any) => { setDraft((prev) => { if (key === "value") { return { ...prev, [fieldName]: val }; } const existing = prev[fieldName] || {}; return { ...prev, [fieldName]: { ...existing, [key]: val } }; }); }; return ( setOpen((o) => !o)} > {open ? "Hide Filters" : "Show Filters"} {activeCount > 0 && ( {activeCount} active )} {open && ( {filterableFields.map((fieldName) => { const field = fields[fieldName]; if (!field) return null; const needsOptions = !field.filterType || field.filterType === "autocomplete" || field.filterType === "multiselect"; const options = needsOptions ? extractOptions(fieldName, field, data ?? []) : []; const raw = draft[fieldName]; return ( {field.label} {renderFilterInput(fieldName, field, options, raw, (key, val) => updateDraft(fieldName, key, val) )} ); })} )} ); }