/* 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 type { Option } from "@/components/ui/select-core"; import { useSelectList } from "@/components/ui/select-core"; 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"; import { CompactChipRow } from "@/components/ui/value-chips"; 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(chosenValues.length === 0); const chosenValuesSet = useMemo(() => new Set(chosenValues), [chosenValues]); const displayItems = useMemo( () => [...chosenValuesSet].map((v) => stringifyUnknownValue({ value: v })), [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 { 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 options = useMemo>>(() => { if (!data) { return []; } try { return data .filter(([value]) => value !== undefined) .map(([value, count]) => ({ value, label: String(value), data: { count }, })); } catch (error_) { Logger.error("Error building filter options", error_); return []; } }, [data]); const list = useSelectList({ options, value: [...chosenValues], onChange: (next) => onChange(next as unknown[]), multiple: true, filterFn: (label, q) => smartMatch(q, label) || label.toLowerCase().includes(q.toLowerCase()) ? 1 : 0, }); const query = list.searchQuery; const setQuery = list.setSearchQuery; const filteredData = useMemo>( () => list.visibleOptions.map( (o) => [o.value, (o.data as { count: number }).count] as [ unknown, number | undefined, ], ), [list.visibleOptions], ); // 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, column.columnDef.meta?.dtype, ); return ( handleToggle(value)} > {sentinel ? : valueString} {count === undefined ? "—" : count} ); })} {canCreate && ( + Add "{trimmedQuery}" )} {data.length === TOP_K_ROWS && ( Only showing the top {TOP_K_ROWS} values )} ); };