import { HTMLAttributes, memo, ReactNode, useMemo } from "react" import { getColumnHeaderLabel, useDataGrid, } from "@/components/reui/data-grid/data-grid" import { Column } from "@tanstack/react-table" import { cn } from "@cfdm/ui/lib/utils" import { Button } from "@cfdm/ui/components/button" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@cfdm/ui/components/dropdown-menu" import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react" interface DataGridColumnHeaderProps< TData, TValue, > extends HTMLAttributes { column: Column /** When omitted, uses `column.columnDef.meta.headerTitle`, then a string `columnDef.header`, then `column.id`. */ title?: string icon?: ReactNode /** Reserved; pin controls are gated by tableLayout.columnsPinnable + column.getCanPin(). */ pinnable?: boolean filter?: ReactNode visibility?: boolean } function DataGridColumnHeaderInner({ column, title, icon, className, filter, visibility = false, }: DataGridColumnHeaderProps) { const { isLoading, table, props, recordCount } = useDataGrid() const resolvedTitle = title ?? getColumnHeaderLabel(column) const columnOrder = table.getState().columnOrder const columnVisibilityKey = props.tableLayout?.columnsVisibility && visibility ? JSON.stringify(table.getState().columnVisibility) : "" const isSorted = column.getIsSorted() const isPinned = column.getIsPinned() const canSort = column.getCanSort() const canPin = column.getCanPin() const canResize = column.getCanResize() const columnIndex = columnOrder.indexOf(column.id) const canMoveLeft = columnIndex > 0 const canMoveRight = columnIndex < columnOrder.length - 1 const handleSort = () => { if (isSorted === "asc") { column.toggleSorting(true) } else if (isSorted === "desc") { column.clearSorting() } else { column.toggleSorting(false) } } const headerLabelClassName = cn( "text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5", className ) const headerButtonClassName = cn( "text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground px-2 font-normal h-6 rounded-lg", className ) const sortIcon = canSort && (isSorted === "desc" ? ( ) : isSorted === "asc" ? ( ) : ( )) const hasControls = props.tableLayout?.columnsMovable || (props.tableLayout?.columnsVisibility && visibility) || (props.tableLayout?.columnsPinnable && canPin) || filter const menuItems = useMemo(() => { const items: ReactNode[] = [] let hasPreviousSection = false // Filter section if (filter) { items.push( {filter} ) hasPreviousSection = true } // Sort section if (canSort) { if (hasPreviousSection) { items.push() } items.push( { if (isSorted === "asc") { column.clearSorting() } else { column.toggleSorting(false) } }} disabled={!canSort} > Asc {isSorted === "asc" && ( )} , { if (isSorted === "desc") { column.clearSorting() } else { column.toggleSorting(true) } }} disabled={!canSort} > Desc {isSorted === "desc" && ( )} ) hasPreviousSection = true } // Pin section if (props.tableLayout?.columnsPinnable && canPin) { if (hasPreviousSection) { items.push() } items.push( column.pin(isPinned === "left" ? false : "left")} > Pin to left {isPinned === "left" && ( )} , column.pin(isPinned === "right" ? false : "right")} > Pin to right {isPinned === "right" && ( )} ) hasPreviousSection = true } // Move section if (props.tableLayout?.columnsMovable) { if (hasPreviousSection) { items.push() } items.push( { if (columnIndex > 0) { const newOrder = [...columnOrder] const [movedColumn] = newOrder.splice(columnIndex, 1) newOrder.splice(columnIndex - 1, 0, movedColumn) table.setColumnOrder(newOrder) } }} disabled={!canMoveLeft || isPinned !== false} > Move to Left , { if (columnIndex < columnOrder.length - 1) { const newOrder = [...columnOrder] const [movedColumn] = newOrder.splice(columnIndex, 1) newOrder.splice(columnIndex + 1, 0, movedColumn) table.setColumnOrder(newOrder) } }} disabled={!canMoveRight || isPinned !== false} > Move to Right ) hasPreviousSection = true } // Visibility section if (props.tableLayout?.columnsVisibility && visibility) { if (hasPreviousSection) { items.push() } items.push( Columns {table .getAllColumns() .filter((col) => col.getCanHide()) .map((col) => ( event.preventDefault()} onCheckedChange={(value) => col.toggleVisibility(!!value)} className="capitalize" > {getColumnHeaderLabel(col)} ))} ) } return items // eslint-disable-next-line react-hooks/exhaustive-deps }, [ filter, canSort, isSorted, column, props.tableLayout?.columnsPinnable, props.tableLayout?.columnsMovable, props.tableLayout?.columnsVisibility, canPin, isPinned, canMoveLeft, canMoveRight, visibility, table, columnIndex, columnOrder, columnVisibilityKey, // Needed to update checkbox states when visibility changes ]) if (hasControls) { return ( {icon && icon} {resolvedTitle} {sortIcon} } /> {menuItems} {props.tableLayout?.columnsPinnable && canPin && isPinned && ( column.pin(false)} aria-label={`Unpin ${resolvedTitle} column`} title={`Unpin ${resolvedTitle} column`} > )} ) } if (canSort || (props.tableLayout?.columnsResizable && canResize)) { return ( {icon && icon} {resolvedTitle} {sortIcon} ) } return ( {icon && icon} {resolvedTitle} ) } const DataGridColumnHeader = memo( DataGridColumnHeaderInner ) as typeof DataGridColumnHeaderInner export { DataGridColumnHeader, type DataGridColumnHeaderProps }