import type { ColumnDef } from '@tanstack/react-table' import type { GriddyColumn, SelectionConfig } from './types' import { createOperatorFilter } from '../features/filtering' import { DEFAULTS, SELECTION_COLUMN_ID, SELECTION_COLUMN_SIZE } from './constants' /** * Retrieves the original GriddyColumn from a TanStack column's meta. */ export function getGriddyColumn(column: { columnDef: ColumnDef }): GriddyColumn | undefined { return (column.columnDef.meta as { griddy?: GriddyColumn })?.griddy } /** * Converts a single GriddyColumn to a TanStack ColumnDef */ function mapSingleColumn(col: GriddyColumn): ColumnDef { const isStringAccessor = typeof col.accessor !== 'function' const def: ColumnDef = { id: col.id, // Use accessorKey for string keys (enables TanStack auto-detection of sort/filter), // accessorFn for function accessors ...(isStringAccessor ? { accessorKey: col.accessor as string } : { accessorFn: col.accessor as (row: T) => unknown }), aggregationFn: col.aggregationFn, enableColumnFilter: col.filterable ?? false, enableGrouping: col.groupable ?? false, enableHiding: true, enablePinning: true, enableResizing: true, enableSorting: col.sortable ?? true, header: () => col.header, maxSize: col.maxWidth ?? DEFAULTS.maxColumnWidth, meta: { griddy: col }, minSize: col.minWidth ?? DEFAULTS.minColumnWidth, size: col.width, } // For function accessors, TanStack can't auto-detect the sort type, so provide a default if (col.sortFn) { def.sortingFn = col.sortFn } else if (!isStringAccessor && col.sortable !== false) { // Use alphanumeric sorting for function accessors def.sortingFn = 'alphanumeric' } if (col.filterFn) { def.filterFn = col.filterFn } else if (col.filterable) { def.filterFn = createOperatorFilter() } return def } /** * Maps Griddy's user-facing GriddyColumn definitions to TanStack Table ColumnDef[]. * Supports header grouping and optionally prepends a selection checkbox column. */ export function mapColumns( columns: GriddyColumn[], selection?: SelectionConfig, ): ColumnDef[] { // Group columns by headerGroup const grouped = new Map[]>() const ungrouped: GriddyColumn[] = [] columns.forEach(col => { if (col.headerGroup) { const existing = grouped.get(col.headerGroup) || [] existing.push(col) grouped.set(col.headerGroup, existing) } else { ungrouped.push(col) } }) // Build column definitions const mapped: ColumnDef[] = [] // Add ungrouped columns first ungrouped.forEach(col => { mapped.push(mapSingleColumn(col)) }) // Add grouped columns grouped.forEach((groupColumns, groupName) => { const groupDef: ColumnDef = { header: groupName, id: `group-${groupName}`, columns: groupColumns.map(col => mapSingleColumn(col)), } mapped.push(groupDef) }) // Prepend checkbox column if selection is enabled if (selection && selection.mode !== 'none' && selection.showCheckbox !== false) { const checkboxCol: ColumnDef = { cell: 'select-row', // Rendered by TableCell with actual checkbox enableColumnFilter: false, enableHiding: false, enableResizing: false, enableSorting: false, header: selection.mode === 'multi' ? 'select-all' // Rendered by TableHeader with actual checkbox : '', id: SELECTION_COLUMN_ID, size: SELECTION_COLUMN_SIZE, } mapped.unshift(checkboxCol) } return mapped }