import * as React from 'react' import { useState, type ReactNode } from 'react' import { type Table } from '@tanstack/react-table' import { ChevronDown, Loader2, X as Cross2Icon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { DataTableFacetedFilter } from './faceted-filter' import { DataTableViewOptions } from './view-options' type FilterDef = { columnId: string title: string options: { label: string value: string icon?: React.ComponentType<{ className?: string }> iconNode?: React.ReactNode count?: number }[] singleSelect?: boolean } export type DataTableToolbarProps = { table: Table /** * Placeholder for the default search input. Defaults to `t('Filter...')`. */ searchPlaceholder?: string /** * Column id to filter on. When provided, the search input filters * a specific column. When omitted, the search input updates the * table's `globalFilter`. */ searchKey?: string /** * Column-level filter chips (faceted multi-select / single-select). */ filters?: FilterDef[] /** * Replaces the default search input entirely. Use when the primary * "search" is something custom — e.g. a date-time range picker. */ customSearch?: ReactNode /** * Extra inputs/selects displayed in the primary row alongside the * search input and filter chips. */ additionalSearch?: ReactNode /** * Whether non-table filters (e.g. `additionalSearch` or `expandable` * inputs) are currently active. Controls Reset button visibility * when no column filters are set. */ hasAdditionalFilters?: boolean /** * Callback invoked when the user clicks Reset. */ onReset?: () => void /** * Additional filter inputs hidden behind an Expand/Collapse toggle. * Inputs flow inline with the primary row when expanded. */ expandable?: ReactNode /** * When `expandable` is collapsed, highlights the toggle if any of * the expandable inputs currently hold a value. */ hasExpandedActiveFilters?: boolean /** * Custom action buttons rendered BEFORE the built-in * Reset / Search / View buttons. */ preActions?: ReactNode /** * Explicit "Search" / "Apply" callback. When provided the toolbar * shows a primary Search button. Filters are committed only on click * (form-mode workflow). */ onSearch?: () => void /** * Loading state for the explicit Search button. */ searchLoading?: boolean /** * Hide the View Options (column visibility) dropdown. */ hideViewOptions?: boolean /** * Content rendered on the LEFT side of the secondary action row. When * provided the toolbar splits into two visual rows: * Row 1: search inputs / filter chips …… Expand * Row 2: expanded filters * Row 3: leftActions …… Reset / Search / ViewOptions */ leftActions?: ReactNode /** * Outer wrapper className override. */ className?: string } /** * Unified data-table filter panel — Ant Design Pro inspired. * * Layout (single flex-wrap row): * - Filters (search input + additional inputs + filter chips + expandable * inputs) flow horizontally and wrap as needed. * - The action cluster (Reset / Search / View / Expand) hugs the right * edge via `ms-auto`. When filters fill a row, the cluster naturally * wraps to the next line — still right-aligned — matching the * collapsed/expanded states from the user's reference design. * * No background panel, no row separators — relies on whitespace and the * adjacent table border for visual hierarchy. */ export function DataTableToolbar(props: DataTableToolbarProps) { const { t } = useTranslation() const [expanded, setExpanded] = useState(false) const filters = props.filters ?? [] const hasExpandable = props.expandable != null const hasSearch = props.onSearch != null const isFiltered = props.table.getState().columnFilters.length > 0 || !!props.table.getState().globalFilter || !!props.hasAdditionalFilters const placeholder = props.searchPlaceholder ?? t('Filter...') const searchInput = props.searchKey ? ( props.table .getColumn(props.searchKey!) ?.setFilterValue(event.target.value) } className='w-full sm:w-[200px] lg:w-[240px]' /> ) : ( props.table.setGlobalFilter(event.target.value)} className='w-full sm:w-[200px] lg:w-[240px]' /> ) const filterChips = filters.map((filter) => { const column = props.table.getColumn(filter.columnId) if (!column) return null return ( ) }) const handleReset = () => { props.table.resetColumnFilters() props.table.setGlobalFilter('') props.onReset?.() } // Reset: outline text-only for form mode (always visible, disabled when // nothing to reset); ghost text + X for filter-as-you-type mode (only // visible when active filters exist). const resetButton = hasSearch ? ( ) : isFiltered ? ( ) : null const searchButton = hasSearch ? ( ) : null const viewOptionsNode = !props.hideViewOptions ? ( ) : null const expandToggle = hasExpandable ? ( ) : null const hasLeftActions = props.leftActions != null if (hasLeftActions) { return (
{props.customSearch !== undefined ? props.customSearch : searchInput} {props.additionalSearch} {filterChips}
{expandToggle}
{expanded && hasExpandable && (
{props.expandable}
)}
{props.leftActions}
{props.preActions} {resetButton} {searchButton} {viewOptionsNode}
) } return (
{props.customSearch !== undefined ? props.customSearch : searchInput} {props.additionalSearch} {filterChips} {expanded && hasExpandable && props.expandable}
{props.preActions} {resetButton} {searchButton} {viewOptionsNode} {expandToggle}
) }