@@ -12,29 +12,36 @@ import {
1212 type SortingState ,
1313} from "@tanstack/react-table" ;
1414
15- // Extend ColumnMeta to include noDivider and sortParam
15+ // Extend ColumnMeta to include noDivider
1616declare module "@tanstack/react-table" {
1717 interface ColumnMeta < TData , TValue > {
1818 noDivider ?: boolean ;
19- sortParam ?: string ; // API field name for sorting (e.g., "user__first_name")
2019 }
2120}
2221import { memo , useState , useMemo , useCallback } from "react" ;
2322import { cn } from "../../utils/utils" ;
2423import { useColumnSizing , useDataColumns } from "../../hooks/data-table" ;
2524import { Checkbox } from "../checkbox/checkbox" ;
2625import { Typography } from "../typography/typography" ;
27- import { IconSortUp , IconSortDown , IconSearch } from "@humansignal/icons" ;
26+ import { Tooltip } from "../Tooltip/Tooltip" ;
27+ import { IconSortUp , IconSortDown , IconSearch , IconInfoOutline } from "@humansignal/icons" ;
2828import { EmptyState } from "../empty-state/empty-state" ;
2929import { Skeleton } from "../skeleton/skeleton" ;
3030import styles from "./data-table.module.scss" ;
3131
3232export type DataShape = Record < string , any > [ ] ;
3333
34+ /**
35+ * Extended ColumnDef type that includes custom properties for generic DataTable
36+ */
37+ export type ExtendedDataTableColumnDef < T > = ColumnDef < T > & {
38+ help ?: string ; // Optional help text to display in a tooltip with info icon
39+ } ;
40+
3441export type DataTableProps < T extends DataShape > = {
3542 data : T ;
3643 meta ?: TableMeta < any > ;
37- columns ?: ColumnDef < T [ number ] > [ ] ;
44+ columns ?: ExtendedDataTableColumnDef < T [ number ] > [ ] ;
3845 extraColumns ?: ColumnDef < any > [ ] ;
3946 includeColumns ?: ( keyof T [ number ] ) [ ] ;
4047 excludeColumns ?: ( keyof T [ number ] ) [ ] ;
@@ -111,6 +118,33 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
111118 const [ internalSorting , setInternalSorting ] = useState < SortingState > ( [ ] ) ;
112119 const [ internalActiveRowId , setInternalActiveRowId ] = useState < string | undefined > ( undefined ) ;
113120
121+ // Restore column sizes from localStorage if storageKey is provided
122+ const restoredColumnSizing = useMemo ( ( ) => {
123+ if ( ! props . cellSizesStorageKey ) return { } ;
124+
125+ try {
126+ const stored = localStorage . getItem ( props . cellSizesStorageKey ) ;
127+ if ( ! stored ) return { } ;
128+
129+ const cellSizes = JSON . parse ( stored ) as Record < string , { size : number } > ;
130+ const columnSizing : Record < string , number > = { } ;
131+
132+ // Convert stored format { [columnId]: { size: number } } to TanStack format { [columnId]: number }
133+ for ( const [ columnId , sizeData ] of Object . entries ( cellSizes ) ) {
134+ if ( sizeData ?. size && typeof sizeData . size === "number" ) {
135+ columnSizing [ columnId ] = sizeData . size ;
136+ }
137+ }
138+
139+ return columnSizing ;
140+ } catch ( error ) {
141+ console . warn ( "Failed to restore column sizes from localStorage:" , error ) ;
142+ return { } ;
143+ }
144+ } , [ props . cellSizesStorageKey ] ) ;
145+
146+ const [ internalColumnSizing , setInternalColumnSizing ] = useState < Record < string , number > > ( restoredColumnSizing ) ;
147+
114148 // Use controlled activeRowId if onRowClick is provided (parent controls state via clicks)
115149 // OR if activeRowId is explicitly provided (not undefined)
116150 // When onRowClick is provided, activeRowId is read-only for display purposes
@@ -131,7 +165,10 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
131165 const columnsWithHeaders = useMemo ( ( ) => {
132166 return baseColumns . map ( ( col ) => {
133167 // TanStack Table uses accessorKey as id if id is not explicitly set
134- const columnId = col . id || ( col as any ) . accessorKey ;
168+ const extendedCol = col as ExtendedDataTableColumnDef < T [ number ] > ;
169+ const columnId =
170+ extendedCol . id ||
171+ ( "accessorKey" in extendedCol && extendedCol . accessorKey ? String ( extendedCol . accessorKey ) : undefined ) ;
135172
136173 // Get current sort state for this column
137174 const currentSort = sorting . length > 0 ? sorting [ 0 ] : null ;
@@ -155,6 +192,7 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
155192 isDesc = { isDesc }
156193 enableSorting = { columnSortingEnabled }
157194 originalHeader = { originalHeader }
195+ help = { extendedCol . help }
158196 />
159197 ) ,
160198 } ;
@@ -277,6 +315,13 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
277315 columnVisibility : props . columnVisibility ,
278316 rowSelection,
279317 sorting,
318+ columnSizing : internalColumnSizing ,
319+ } ,
320+ onColumnSizingChange : ( updater ) => {
321+ setInternalColumnSizing ( ( old ) => {
322+ const newState = typeof updater === "function" ? updater ( old ) : updater ;
323+ return newState ;
324+ } ) ;
280325 } ,
281326 onSortingChange : ( updater ) => {
282327 if ( isSortingControlled && controlledOnSortingChange ) {
@@ -308,7 +353,9 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
308353 : undefined ,
309354 getRowId : ( row , index ) => {
310355 // Use id if available, otherwise fall back to index
311- return ( row as any ) ?. id ?. toString ( ) ?? index . toString ( ) ;
356+ // Note: 'row' parameter is the row data object itself, not a Row object
357+ const rowId = ( row as any ) ?. id ;
358+ return rowId !== undefined ? String ( rowId ) : String ( index ) ;
312359 } ,
313360 columnResizeMode : "onChange" ,
314361 enableSorting : enableSorting ,
@@ -655,6 +702,7 @@ export type HeaderProps<T> = {
655702 isDesc ?: boolean ;
656703 enableSorting ?: boolean ;
657704 originalHeader ?: string | React . ReactNode ;
705+ help ?: string ; // Optional help text to display in a tooltip with info icon
658706} ;
659707
660708export const Header = < T , > ( {
@@ -663,6 +711,7 @@ export const Header = <T,>({
663711 isDesc = false ,
664712 enableSorting = false ,
665713 originalHeader,
714+ help,
666715} : HeaderProps < T > ) => {
667716 // Get header label - use originalHeader if provided, otherwise try to extract from columnDef
668717 let headerLabel : string | React . ReactNode = undefined ;
@@ -680,24 +729,29 @@ export const Header = <T,>({
680729 return null ;
681730 }
682731
732+ const headerContent = (
733+ < div className = { cn ( styles . headerContent , help && "gap-tighter" ) } >
734+ < div className = "flex items-center gap-2" >
735+ < Typography variant = "label" size = "small" className = { cn ( isSorted && styles . headerTextSorted ) } >
736+ { headerLabel }
737+ </ Typography >
738+ { help && (
739+ < Tooltip title = { help } alignment = "top-center" >
740+ < IconInfoOutline width = { 18 } height = { 18 } className = "text-neutral-content-subtler cursor-help shrink-0" />
741+ </ Tooltip >
742+ ) }
743+ </ div >
744+ { enableSorting && (
745+ < div className = { cn ( styles . headerIcon , isSorted === true && styles . headerIconVisible ) } >
746+ { isSorted ? isDesc ? < IconSortUp /> : < IconSortDown /> : < IconSortDown /> }
747+ </ div >
748+ ) }
749+ </ div >
750+ ) ;
751+
683752 if ( ! enableSorting ) {
684- return (
685- < Typography variant = "label" size = "small" >
686- { headerLabel }
687- </ Typography >
688- ) ;
753+ return headerContent ;
689754 }
690755
691- // Determine icon: when sorted, show current direction; when hovering unsorted, show next direction (asc)
692- const sortIcon = isSorted ? isDesc ? < IconSortUp /> : < IconSortDown /> : < IconSortDown /> ;
693-
694- return (
695- < div className = { styles . headerContent } >
696- < Typography variant = "label" size = "small" className = { cn ( isSorted && styles . headerTextSorted ) } >
697- { headerLabel }
698- </ Typography >
699- { /* Always render icon container for sortable columns - CSS handles visibility */ }
700- < div className = { cn ( styles . headerIcon , isSorted === true && styles . headerIconVisible ) } > { sortIcon } </ div >
701- </ div >
702- ) ;
756+ return headerContent ;
703757} ;
0 commit comments