/* 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; creatable?: boolean; } export const FilterByValuesPicker = ({ column, calculateTopKRows, chosenValues, onChange, creatable = false, }: 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; creatable?: boolean; } /** * Search + checkbox list that powers the "filter by values" picker. */ export const FilterByValuesList = ({ column, calculateTopKRows, chosenValues, onChange, creatable = false, }: 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]); // Surface chosen values that aren't in the top-K so they stay visible/uncheckable. // Count is undefined for these rows; the cell renders an em-dash. const mergedData = useMemo>(() => { const seen = new Set(filteredData.map(([v]) => v)); const extras: Array<[unknown, number | undefined]> = []; for (const chosen of chosenValues) { if (seen.has(chosen)) { continue; } const str = String(chosen); const matches = query.length === 0 || smartMatch(query, str) || str.toLowerCase().includes(query.toLowerCase()); if (matches) { extras.push([chosen, undefined]); } } return [...filteredData, ...extras]; }, [filteredData, chosenValues, query]); const handleToggle = (value: unknown) => { onChange([...Sets.toggle(chosenValues, value)]); }; const trimmedQuery = query.trim(); const canCreate = creatable && trimmedQuery !== "" && !mergedData.some(([v]) => String(v) === trimmedQuery); const commitCreate = () => { if (!canCreate) { return; } onChange([...chosenValues, trimmedQuery]); setQuery(""); }; const allVisibleChecked = mergedData.length > 0 && mergedData.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 mergedData) { next.delete(value); } } else { for (const [value] of mergedData) { next.add(value); } } onChange([...next]); }; if (isPending) { return ; } if (error) { return ; } if (!data) { return (
No values available
); } return ( { if (e.key === "Enter" && canCreate) { e.preventDefault(); commitCreate(); } }} /> No results found. {mergedData.length > 0 && ( {column.id} Count )} {mergedData.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 === undefined ? "—" : count} ); })} {canCreate && ( + Add "{trimmedQuery}" )} {data.length === TOP_K_ROWS && ( Only showing the top {TOP_K_ROWS} values )} ); };