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; } /** * 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 = { columns: groupColumns.map((col) => mapSingleColumn(col)), header: groupName, id: `group-${groupName}`, }; 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; } /** * 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(); } if (col.renderer) { const renderer = col.renderer; def.cell = (info) => renderer({ column: col, columnIndex: info.cell.column.getIndex(), row: info.row.original, rowIndex: info.row.index, value: info.getValue(), }); } return def; }