"use client" import { createContext, ReactNode, useContext, useMemo } from "react" import { Column, ColumnFiltersState, RowData, SortingState, Table, } from "@tanstack/react-table" import { cn } from "@/lib/utils" declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { headerTitle?: string headerClassName?: string cellClassName?: string skeleton?: ReactNode expandedContent?: (row: TData) => ReactNode } } /** Label for headers / column visibility: `meta.headerTitle`, string `columnDef.header`, or `column.id`. */ export function getColumnHeaderLabel( column: Column ): string { const meta = column.columnDef.meta as { headerTitle?: string } | undefined if (typeof meta?.headerTitle === "string") return meta.headerTitle const defHeader = column.columnDef.header if (typeof defHeader === "string") return defHeader return String(column.id) } export type DataGridApiFetchParams = { pageIndex: number pageSize: number sorting?: SortingState filters?: ColumnFiltersState searchQuery?: string } export type DataGridApiResponse = { data: T[] empty: boolean pagination: { total: number page: number } } export interface DataGridContextProps { props: DataGridProps table: Table recordCount: number isLoading: boolean } export type DataGridRequestParams = { pageIndex: number pageSize: number sorting?: SortingState columnFilters?: ColumnFiltersState } export interface DataGridProps { className?: string table?: Table recordCount: number children?: ReactNode onRowClick?: (row: TData) => void isLoading?: boolean loadingMode?: "skeleton" | "spinner" loadingMessage?: ReactNode | string fetchingMoreMessage?: ReactNode | string allRowsLoadedMessage?: ReactNode | string emptyMessage?: ReactNode | string tableLayout?: { dense?: boolean cellBorder?: boolean rowBorder?: boolean rowRounded?: boolean stripped?: boolean headerBackground?: boolean headerBorder?: boolean headerSticky?: boolean width?: "auto" | "fixed" columnsVisibility?: boolean columnsResizable?: boolean columnsResizeMode?: "onChange" | "onEnd" columnsPinnable?: boolean columnsMovable?: boolean columnsDraggable?: boolean rowsDraggable?: boolean rowsPinnable?: boolean } tableClassNames?: { base?: string header?: string headerRow?: string headerSticky?: string body?: string bodyRow?: string footer?: string edgeCell?: string } } const DataGridContext = createContext< // eslint-disable-next-line @typescript-eslint/no-explicit-any DataGridContextProps | undefined >(undefined) function useDataGrid() { const context = useContext(DataGridContext) if (!context) { throw new Error("useDataGrid must be used within a DataGridProvider") } return context } function DataGridProvider({ children, table, ...props }: DataGridProps & { table: Table }) { const tableState = table.getState() const resolvedColumnsResizeMode = props.tableLayout?.columnsResizeMode ?? "onEnd" // Keep resize mode aligned with the DataGrid contract every render so // consumer-level useReactTable options cannot flip it back between drags. if (props.tableLayout?.columnsResizable) { table.options.columnResizeMode = resolvedColumnsResizeMode } // Memoize context value so consumers don't re-render during column resize. // Column sizing state is intentionally excluded from deps -- CSS variables // on the element handle width updates without React re-renders. const value = useMemo( () => ({ props, table, recordCount: props.recordCount, isLoading: props.isLoading || false, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ table, props.recordCount, props.isLoading, props.loadingMode, props.loadingMessage, props.fetchingMoreMessage, props.allRowsLoadedMessage, props.emptyMessage, props.onRowClick, props.className, // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify(props.tableLayout), // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify(props.tableClassNames), tableState.sorting, tableState.pagination, tableState.columnFilters, tableState.rowSelection, tableState.expanded, tableState.columnVisibility, tableState.columnOrder, tableState.columnPinning, tableState.globalFilter, ] ) return ( {children} ) } function DataGrid({ children, table, ...props }: DataGridProps) { const defaultProps: Partial> = { loadingMode: "skeleton", tableLayout: { dense: false, cellBorder: false, rowBorder: true, rowRounded: false, stripped: false, headerSticky: false, headerBackground: true, headerBorder: true, width: "fixed", columnsVisibility: false, columnsResizable: false, columnsResizeMode: "onEnd", columnsPinnable: false, columnsMovable: false, columnsDraggable: false, rowsDraggable: false, rowsPinnable: false, }, tableClassNames: { base: "", header: "", headerRow: "", headerSticky: "sticky top-0 z-15 bg-background/90 backdrop-blur-xs", body: "", bodyRow: "", footer: "", edgeCell: "", }, } const mergedProps: DataGridProps = { ...defaultProps, ...props, tableLayout: { ...defaultProps.tableLayout, ...(props.tableLayout || {}), }, tableClassNames: { ...defaultProps.tableClassNames, ...(props.tableClassNames || {}), }, } // Ensure table is provided if (!table) { throw new Error('DataGrid requires a "table" prop') } return ( {children} ) } function DataGridContainer({ children, className, border = true, }: { children: ReactNode className?: string border?: boolean }) { return (
{children}
) } export { useDataGrid, DataGridProvider, DataGrid, DataGridContainer }