diff --git a/src/components/data-table/data-table-boolean-filter.tsx b/src/components/data-table/data-table-boolean-filter.tsx new file mode 100644 index 000000000..e25ed5788 --- /dev/null +++ b/src/components/data-table/data-table-boolean-filter.tsx @@ -0,0 +1,148 @@ +"use client"; + +import * as React from "react"; +import type { Column } from "@tanstack/react-table"; +import { PlusCircle, XCircle, Check } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import type { BooleanOption} from "@/types/data-table"; + +interface DataTableBooleanFilterProps { + column?: Column; + title?: string; + booleanOptions: BooleanOption[]; +} + +export function DataTableBooleanFilter({ + column, + title, + booleanOptions, +}: DataTableBooleanFilterProps) { + const [open, setOpen] = React.useState(false); + + const columnFilterValue = column?.getFilterValue(); + const selectedValue = typeof columnFilterValue === "boolean" ? columnFilterValue : undefined; + + const onItemSelect = React.useCallback( + (option: BooleanOption, isSelected: boolean) => { + if (!column) return; + if (isSelected) { + column.setFilterValue(undefined); + } else { + column.setFilterValue(option.value); + } + setOpen(false); + }, + [column], + ); + + const onReset = React.useCallback( + (event?: React.MouseEvent) => { + event?.stopPropagation(); + column?.setFilterValue(undefined); + }, + [column], + ); + + const activeOption = booleanOptions.find((o) => o.value === selectedValue); + + return ( + + + + + + + + + No results found. + + {booleanOptions.map((option) => { + const isSelected = selectedValue === option.value; + const Icon = option.icon; + return ( + onItemSelect(option, isSelected)} + > +
+ +
+ {Icon && } + {option.label} +
+ ); + })} +
+ {activeOption && ( + <> + + + onReset()} + className="justify-center text-center" + > + Clear filter + + + + )} +
+
+
+
+ ); +} diff --git a/src/components/data-table/data-table-toolbar.tsx b/src/components/data-table/data-table-toolbar.tsx index fd49d8fa5..a3eb4c869 100644 --- a/src/components/data-table/data-table-toolbar.tsx +++ b/src/components/data-table/data-table-toolbar.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { DataTableDateFilter } from "@/components/data-table/data-table-date-filter"; import { DataTableFacetedFilter } from "@/components/data-table/data-table-faceted-filter"; +import { DataTableBooleanFilter } from "@/components/data-table/data-table-boolean-filter"; import { DataTableSliderFilter } from "@/components/data-table/data-table-slider-filter"; import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; import { Button } from "@/components/ui/button"; @@ -128,6 +129,15 @@ function DataTableToolbarFilter({ /> ); + case "boolean": + return ( + + ); + case "select": case "multiSelect": return ( diff --git a/src/hooks/use-data-table.ts b/src/hooks/use-data-table.ts index c702d4bec..6102b4cd4 100644 --- a/src/hooks/use-data-table.ts +++ b/src/hooks/use-data-table.ts @@ -23,6 +23,7 @@ import { parseAsArrayOf, parseAsInteger, parseAsString, + parseAsBoolean, type UseQueryStateOptions, useQueryState, useQueryStates, @@ -176,9 +177,11 @@ export function useDataTable(props: UseDataTableProps) { if (enableAdvancedFilter) return {}; return filterableColumns.reduce< - Record | Parser> + Record | Parser | Parser> >((acc, column) => { - if (column.meta?.options) { + if (column.meta?.booleanOptions) { + acc[column.id ?? ""] = parseAsBoolean.withOptions(queryStateOptions); + } else if (column.meta?.options) { acc[column.id ?? ""] = parseAsArrayOf( parseAsString, ARRAY_SEPARATOR, @@ -206,11 +209,16 @@ export function useDataTable(props: UseDataTableProps) { return Object.entries(filterValues).reduce( (filters, [key, value]) => { if (value !== null) { - const processedValue = Array.isArray(value) - ? value - : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) - ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) - : [value]; + let processedValue: unknown; + if (typeof value === "boolean") { + processedValue = value; + } else if (Array.isArray(value)) { + processedValue = value; + } else if (/[^a-zA-Z0-9]/.test(value)) { + processedValue = value.split(/[^a-zA-Z0-9]+/).filter(Boolean); + } else { + processedValue = [value]; + } filters.push({ id: key, @@ -237,10 +245,10 @@ export function useDataTable(props: UseDataTableProps) { : updaterOrValue; const filterUpdates = next.reduce< - Record + Record >((acc, filter) => { if (filterableColumns.find((column) => column.id === filter.id)) { - acc[filter.id] = filter.value as string | string[]; + acc[filter.id] = filter.value as string | string[] | boolean; } return acc; }, {}); diff --git a/src/types/data-table.ts b/src/types/data-table.ts index af4d0f8b4..a3ca9fbbb 100644 --- a/src/types/data-table.ts +++ b/src/types/data-table.ts @@ -12,6 +12,7 @@ declare module "@tanstack/react-table" { range?: [number, number]; unit?: string; icon?: React.FC>; + booleanOptions?: BooleanOption[]; } } @@ -22,6 +23,12 @@ export interface Option { icon?: React.FC>; } +export interface BooleanOption { + label: string; + value: boolean; + icon?: React.FC>; +} + export type FilterOperator = DataTableConfig["operators"][number]; export type FilterVariant = DataTableConfig["filterVariants"][number]; export type JoinOperator = DataTableConfig["joinOperators"][number];