"use client" import * as React from "react" import type { DataTableAdvancedFilterField, Filter, FilterOperator, JoinOperator, } from "@/types/table" import { type Table } from "@tanstack/react-table" import { CalendarIcon, Check, ChevronsUpDown, GripVertical, ListFilter, Trash2, } from "lucide-react" import { customAlphabet } from "nanoid" import { getDefaultFilterOperator, getFilterOperators } from "@/lib/data-table" import { cn, formatDate } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { FacetedFilter, FacetedFilterContent, FacetedFilterEmpty, FacetedFilterGroup, FacetedFilterInput, FacetedFilterItem, FacetedFilterList, FacetedFilterTrigger, } from "@/components/ui/faceted-filter" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sortable, SortableDragHandle, SortableItem, } from "@/components/ui/sortable" interface DataTableAdvancedFilterProps { table: Table filterFields: DataTableAdvancedFilterField[] debounceMs?: number } export function ClientDataTableAdvancedFilter({ table, filterFields, debounceMs = 300, }: DataTableAdvancedFilterProps) { const popoverId = React.useId() // 1) local filter state const [filters, setFilters] = React.useState[]>([]) // 2) local joinOperator (and/or) const [joinOperator, setJoinOperator] = React.useState("and") // 3) Sync to table React.useEffect(() => { const newColumnFilters = filters.map((f) => { // 모든 타입에 대해 operator와 value를 함께 전달 return { id: String(f.id), value: { operator: f.operator, value: f.type === "number" ? parseFloat(String(f.value)) : f.value, } } }) if (joinOperator === "and") { table.setColumnFilters(newColumnFilters) table.setGlobalFilter(undefined) // globalFilter 클리어 } else { table.setColumnFilters([]) // columnFilters 클리어 // globalFilterFn이 기대하는 형태로 변환 const globalFilters = filters.map((f) => ({ columnId: String(f.id), filterValue: { operator: f.operator, value: f.type === "number" ? parseFloat(String(f.value)) : f.value, }, type: f.type })) table.setGlobalFilter({ filters: globalFilters, joinOperator }) } }, [filters, joinOperator, table]) function addFilter() { if (!filterFields.length) return const firstField = filterFields[0] setFilters((prev) => [ ...prev, { id: firstField.id, type: firstField.type, operator: getDefaultFilterOperator(firstField.type), value: "", rowId: customAlphabet( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6 )(), }, ]) } function updateFilter(rowId: string, patch: Partial>) { setFilters((prev) => prev.map((f) => (f.rowId === rowId ? { ...f, ...patch } : f)) ) } function removeFilter(rowId: string) { setFilters((prev) => prev.filter((f) => f.rowId !== rowId)) } function moveFilter(activeIndex: number, overIndex: number) { setFilters((prev) => { const arr = [...prev] const [removed] = arr.splice(activeIndex, 1) if (!removed) return prev arr.splice(overIndex, 0, removed) return arr }) } /** * Render the input UI for each filter type (text, select, date, etc.) */ function renderFilterInput(filter: Filter) { const fieldDef = filterFields.find((f) => f.id === filter.id) if (!fieldDef) return null // For "isEmpty"/"isNotEmpty", no input needed if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty") { return (
) } switch (filter.type) { case "text": case "number": return ( updateFilter( filter.rowId, { value: event.target.value }, ) } /> ) case "select": return ( {"No options"} {fieldDef.options?.map((opt) => ( { updateFilter(filter.rowId, { value: val }) }} > {opt.label} ))} ) case "multi-select": const selectedValues = new Set( Array.isArray(filter.value) ? filter.value : [] ) return ( No options found. {fieldDef?.options?.map((option) => ( { const currentValue = Array.isArray(filter.value) ? filter.value : [] const newValue = currentValue.includes(value) ? currentValue.filter((v) => v !== value) : [...currentValue, value] updateFilter( filter.rowId, { value: newValue }, ) }} > {option.icon && ( ))} ) case "date": const dateValue = Array.isArray(filter.value) ? filter.value.filter(Boolean) : [filter.value, filter.value].filter(Boolean) const displayValue = filter.operator === "isBetween" && dateValue.length === 2 ? `${formatDate(dateValue[0] ?? new Date())} - ${formatDate( dateValue[1] ?? new Date() )}` : dateValue[0] ? formatDate(dateValue[0]) : "Pick a date" return ( {filter.operator === "isBetween" ? ( { updateFilter( filter.rowId, { value: date ? [ date.from?.toISOString() ?? "", date.to?.toISOString() ?? "", ] : [], }, ) }} initialFocus numberOfMonths={1} /> ) : ( { updateFilter( filter.rowId, { value: date?.toISOString() ?? "" }, ) setTimeout(() => { document.getElementById(fieldDef.id)?.click() }, 0) }} initialFocus /> )} ) case "boolean": { if (Array.isArray(filter.value)) return null return ( ) } default: return null } } const filterCount = filters.length return ( ({ id: f.rowId }))} onMove={({ activeIndex, overIndex }) => moveFilter(activeIndex, overIndex)} > {filterCount > 0 ? (

{"Filters"}

) : (

{"No filters applied"}

{"Add filters below"}

)} {/* Filter list */}
{filters.map((filter, index) => (
{index === 0 ? ( {"Where"} ) : index === 1 ? ( ) : ( {joinOperator === "and" ? "AND" : "OR"} )}
{/* Field (column) selection */} {"No fields found."} {filterFields.map((ff) => ( { const newField = filterFields.find((x) => x.id === val) if (!newField) return updateFilter(filter.rowId, { id: newField.id, type: newField.type, operator: getDefaultFilterOperator(newField.type), value: "", }) }} > {ff.label} ))} {/* Operator selection */} {/* The actual filter input */}
{renderFilterInput(filter)}
{/* Remove button */} {/* Drag handle */}
))}
{/* Footer: Add filter / Reset */}
{filterCount > 0 && ( )}
) }