/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; import type { ColumnFiltersState, Table } from "@tanstack/react-table"; import { MoreHorizontalIcon, XIcon } from "lucide-react"; import { useLayoutEffect, useRef, useState } from "react"; import { useLocale } from "react-aria"; import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin"; import { cn } from "@/utils/cn"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { DraggablePopover } from "../ui/draggable-popover"; import { Popover, PopoverClose, PopoverContent, PopoverTrigger, } from "../ui/popover"; import { Tooltip } from "../ui/tooltip"; import { AddFilterButton } from "./add-filter-button"; import { FilterPillEditor } from "./filter-pill-editor"; import { type ColumnFilterValue, formatValue, type Snapshot } from "./filters"; import { extractTimezone } from "./types"; import { ChipWithComma, CompactChipRow } from "@/components/ui/value-chips"; interface Props { filters: ColumnFiltersState | undefined; table: Table; calculateTopKRows?: CalculateTopKRows; addFilterSnapshot: Snapshot | null; onAddFilterSnapshotChange: (snapshot: Snapshot | null) => void; } const useHasOverflow = ( ref: React.RefObject, signature: string, ): boolean => { const [hasOverflow, setHasOverflow] = useState(false); useLayoutEffect(() => { const el = ref.current; if (!el) { return; } const measure = () => setHasOverflow(el.scrollWidth > el.clientWidth); measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); // biome-ignore lint/correctness/useExhaustiveDependencies: signature triggers re-measure on filter content change }, [ref, signature]); return hasOverflow; }; export const FilterPills = ({ filters, table, calculateTopKRows, addFilterSnapshot, onAddFilterSnapshotChange, }: Props) => { const { locale } = useLocale(); const containerRef = useRef(null); const [overflowOpen, setOverflowOpen] = useState(false); const allFilters = filters ?? []; const signature = allFilters .map((f) => `${f.id}:${JSON.stringify(f.value)}`) .join("|"); const hasOverflow = useHasOverflow(containerRef, signature); if (allFilters.length === 0 && addFilterSnapshot === null) { return null; } const renderPill = (filter: ColumnFiltersState[number], index: number) => ( table.setColumnFilters((current) => current.filter((_, i) => i !== index), ) } /> ); return (
{allFilters.map((filter, index) => renderPill(filter, index))}
{hasOverflow && ( )} {hasOverflow && (
{allFilters.map((filter, index) => renderPill(filter, index))}
)}
); }; interface FilterPillProps { columnId: string; value: ColumnFilterValue; index: number; locale: string; table: Table; calculateTopKRows?: CalculateTopKRows; onRemove: () => void; } const FilterPill = ({ columnId, value, index, locale, table, calculateTopKRows, onRemove, }: FilterPillProps) => { const [open, setOpen] = useState(false); const timezone = extractTimezone( table.getColumn(columnId)?.columnDef.meta?.dtype, ); const formatted = formatValue(value, { locale, timezone }); if (!formatted) { return null; } const twoSegment = formatted.kind === "scalar" && formatted.value === undefined; const handleRemove = (e: React.MouseEvent) => { e.stopPropagation(); onRemove(); }; const renderValue = (truncateValue: boolean) => { if (formatted.kind === "scalar") { return ( {formatted.value} ); } if (truncateValue) { return ; } return ( {formatted.items.map((item, i) => ( ))} ); }; const renderSegments = (truncateValue: boolean) => ( <> {columnId} {formatted.operator} {!twoSegment && ( <> {renderValue(truncateValue)} )} ); const tooltip = ( {renderSegments(false)} ); const segments = renderSegments(true); const removeButton = ( ); return ( {removeButton} e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > setOpen(false)} /> ); }; function Separator() { return ( ); }