/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; import type { Column } from "@tanstack/react-table"; import { ChevronDownIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { useAsyncData } from "@/hooks/useAsyncData"; import { ErrorBanner } from "@/plugins/impl/common/error-banner"; import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin"; import { cn } from "@/utils/cn"; import { Logger } from "@/utils/Logger"; import { Sets } from "@/utils/sets"; import { smartMatch } from "@/utils/smartMatch"; import { Spinner } from "../icons/spinner"; import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, } from "../ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { SentinelCell } from "./sentinel-cell"; import { detectSentinel, stringifyUnknownValue } from "./utils"; const TOP_K_ROWS = 30; interface Props { column: Column; calculateTopKRows?: CalculateTopKRows; chosenValues: unknown[]; onChange: (values: unknown[]) => void; } export const FilterByValuesPicker = ({ column, calculateTopKRows, chosenValues, onChange, }: Props) => { const [open, setOpen] = useState(false); const chosenValuesSet = useMemo(() => new Set(chosenValues), [chosenValues]); const selectedValuesStr = useMemo(() => { if (chosenValuesSet.size === 0) { return "Select values…"; } const items = [...chosenValuesSet].map((v) => stringifyUnknownValue({ value: v }), ); return `[${items.join(", ")}]`; }, [chosenValuesSet]); return ( ); }; interface FilterByValuesListProps { column: Column; calculateTopKRows?: CalculateTopKRows; chosenValues: Set; onChange: (values: unknown[]) => void; } /** * Search + checkbox list that powers the "filter by values" picker. */ export const FilterByValuesList = ({ column, calculateTopKRows, chosenValues, onChange, }: FilterByValuesListProps) => { const [query, setQuery] = useState(""); const { data, isPending, error } = useAsyncData(async () => { if (!calculateTopKRows) { return null; } const res = await calculateTopKRows({ column: column.id, k: TOP_K_ROWS }); return res.data; }, [calculateTopKRows, column.id]); const filteredData = useMemo(() => { if (!data) { return []; } try { // try to do includes and also smart match for prefixes return data.filter(([value, _count]) => { if (value === undefined) { return false; } const str = String(value); return ( smartMatch(query, str) || str.toLowerCase().includes(query.toLowerCase()) ); }); } catch (error_) { Logger.error("Error filtering data", error_); return []; } }, [data, query]); const handleToggle = (value: unknown) => { onChange([...Sets.toggle(chosenValues, value)]); }; const allVisibleChecked = filteredData.length > 0 && filteredData.every(([value]) => chosenValues.has(value)); const selectAllState: boolean | "indeterminate" = allVisibleChecked ? true : chosenValues.size > 0 ? "indeterminate" : false; const handleToggleAll = () => { if (!data) { return; } const next = new Set(chosenValues); if (allVisibleChecked) { for (const [value] of filteredData) { next.delete(value); } } else { for (const [value] of filteredData) { next.add(value); } } onChange([...next]); }; if (isPending) { return ; } if (error) { return ; } if (!data) { return (
No values available
); } return ( setQuery(value.trim())} /> No results found. {filteredData.length > 0 && ( {column.id} Count )} {filteredData.map(([value, count]) => { const isSelected = chosenValues.has(value); const valueString = stringifyUnknownValue({ value }); const sentinel = detectSentinel( value, column.columnDef.meta?.dataType, ); return ( handleToggle(value)} > {sentinel ? : valueString} {count} ); })} {data.length === TOP_K_ROWS && ( Only showing the top {TOP_K_ROWS} values )} ); };