/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; import type { ColumnFiltersState, Table } from "@tanstack/react-table"; import { XIcon } from "lucide-react"; import { useState } from "react"; import { type DateFormatter, useDateFormatter } from "react-aria"; import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin"; import { logNever } from "@/utils/assertNever"; import { cn } from "@/utils/cn"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { FilterPillEditor } from "./filter-pill-editor"; import type { ColumnFilterValue } from "./filters"; import { stringifyUnknownValue } from "./utils"; interface Props { filters: ColumnFiltersState | undefined; table: Table; calculateTopKRows?: CalculateTopKRows; } export const FilterPills = ({ filters, table, calculateTopKRows, }: Props) => { const timeFormatter = useDateFormatter({ hour: "2-digit", minute: "2-digit", second: "2-digit", }); if (!filters || filters.length === 0) { return null; } return (
{filters.map((filter) => ( table.setColumnFilters((filters) => filters.filter((f) => f.id !== filter.id), ) } /> ))}
); }; interface FilterPillProps { columnId: string; value: ColumnFilterValue; timeFormatter: DateFormatter; table: Table; calculateTopKRows?: CalculateTopKRows; onRemove: () => void; } const FilterPill = ({ columnId, value, timeFormatter, table, calculateTopKRows, onRemove, }: FilterPillProps) => { const [open, setOpen] = useState(false); const formatted = formatValue(value, timeFormatter); if (!formatted) { return null; } // this is temporary, with more operator & datatype support this goes away const isReadOnly = "type" in value && (value.type === "date" || value.type === "datetime" || value.type === "time"); const twoSegment = formatted.value === undefined; const handleRemove = (e: React.MouseEvent) => { e.stopPropagation(); onRemove(); }; const segments = ( <> {columnId} {formatted.operator} {!twoSegment && ( <> {formatted.value} )} ); const removeButton = ( ); if (isReadOnly) { return ( {segments} {removeButton} ); } return ( {removeButton} e.preventDefault()} > setOpen(false)} /> ); }; function Separator() { return ( ); } interface FormattedFilter { operator: string; value?: string; } function formatValue( value: ColumnFilterValue, timeFormatter: DateFormatter, ): FormattedFilter | undefined { if (!("type" in value)) { return; } if (value.operator === "is_null") { return { operator: "is null" }; } if (value.operator === "is_not_null") { return { operator: "is not null" }; } if (value.type === "number") { return formatMinMax(value.min, value.max); } if (value.type === "date") { return formatMinMax(value.min?.toISOString(), value.max?.toISOString()); } if (value.type === "time") { return formatMinMax( value.min ? timeFormatter.format(value.min) : undefined, value.max ? timeFormatter.format(value.max) : undefined, ); } if (value.type === "datetime") { return formatMinMax(value.min?.toISOString(), value.max?.toISOString()); } if (value.type === "boolean") { return { operator: `is ${value.value ? "True" : "False"}` }; } if (value.type === "select") { const stringifiedOptions = value.options.map((o) => stringifyUnknownValue({ value: o }), ); return { operator: value.operator === "in" ? "is in" : "not in", value: `[${stringifiedOptions.join(", ")}]`, }; } if (value.type === "text") { return { operator: "contains", value: `"${value.text}"` }; } logNever(value); return undefined; } function formatMinMax( min: string | number | undefined, max: string | number | undefined, ): FormattedFilter | undefined { if (min === undefined && max === undefined) { return; } if (min === max) { return { operator: "==", value: String(min) }; } if (min === undefined) { return { operator: "<=", value: String(max) }; } if (max === undefined) { return { operator: ">=", value: String(min) }; } return { operator: "between", value: `${min} - ${max}` }; }