/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import {
type ColumnDef,
type ColumnFiltersState,
type ColumnSizingState,
type ExpandedState,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
type SortingState,
type TableOptions,
type Updater,
type VisibilityState,
getCoreRowModel,
getExpandedRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import * as React from 'react'
type DataTableFeatureOptions = Pick<
TableOptions,
| 'enableRowSelection'
| 'getRowId'
| 'getSubRows'
| 'globalFilterFn'
| 'autoResetPageIndex'
| 'manualFiltering'
| 'manualPagination'
| 'manualSorting'
| 'enableSorting'
| 'enableColumnResizing'
>
type DataTableStateOptions = {
initialSorting?: SortingState
sorting?: SortingState
onSortingChange?: OnChangeFn
initialColumnVisibility?: VisibilityState
columnVisibilityStorageKey?: string | false
columnVisibility?: VisibilityState
onColumnVisibilityChange?: OnChangeFn
initialColumnSizing?: ColumnSizingState
columnSizingStorageKey?: string | false
columnSizing?: ColumnSizingState
onColumnSizingChange?: OnChangeFn
initialRowSelection?: RowSelectionState
rowSelection?: RowSelectionState
onRowSelectionChange?: OnChangeFn
initialExpanded?: ExpandedState
expanded?: ExpandedState
onExpandedChange?: OnChangeFn
columnFilters?: ColumnFiltersState
onColumnFiltersChange?: OnChangeFn
globalFilter?: string
onGlobalFilterChange?: OnChangeFn
initialPagination?: PaginationState
pagination?: PaginationState
onPaginationChange?: OnChangeFn
}
type DataTableRowModelOptions = {
withFilteredRowModel?: boolean
withPaginationRowModel?: boolean
withSortedRowModel?: boolean
withFacetedRowModel?: boolean
withExpandedRowModel?: boolean
}
type UseDataTableOptions = DataTableFeatureOptions &
DataTableStateOptions &
DataTableRowModelOptions & {
data: TData[]
columns: ColumnDef[]
totalCount?: number
pageCount?: number
ensurePageInRange?: (pageCount: number) => void
}
type ColumnSizingBounds = Record<
string,
{
minSize?: number
maxSize?: number
}
>
type ColumnWithSizing = ColumnDef & {
accessorKey?: string | number
columns?: ColumnDef[]
}
const COLUMN_SIZING_PERSIST_DELAY_MS = 250
function resolveUpdater(
updater: Updater,
previous: TValue
): TValue {
return typeof updater === 'function'
? (updater as (old: TValue) => TValue)(previous)
: updater
}
function useControllableTableState(
controlledValue: TValue | undefined,
defaultValue: TValue,
onChange: OnChangeFn | undefined
): [TValue, OnChangeFn] {
const [uncontrolledValue, setUncontrolledValue] =
React.useState(defaultValue)
const value = controlledValue ?? uncontrolledValue
const setValue = React.useCallback>(
(updater) => {
if (controlledValue === undefined) {
setUncontrolledValue((previous) => resolveUpdater(updater, previous))
}
onChange?.(updater)
},
[controlledValue, onChange]
)
return [value, setValue]
}
function readColumnVisibility(storageKey: string | undefined): VisibilityState {
if (!storageKey || typeof window === 'undefined') return {}
try {
const raw = window.localStorage.getItem(storageKey)
if (!raw) return {}
const parsed = JSON.parse(raw) as unknown
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return {}
}
return Object.entries(parsed).reduce(
(visibility, [key, value]) => {
if (typeof value === 'boolean') {
visibility[key] = value
}
return visibility
},
{}
)
} catch {
return {}
}
}
function getColumnId(column: ColumnDef) {
const columnWithSizing = column as ColumnWithSizing
if (typeof columnWithSizing.id === 'string') {
return columnWithSizing.id
}
if (typeof columnWithSizing.accessorKey === 'string') {
return columnWithSizing.accessorKey.replaceAll('.', '_')
}
if (typeof columnWithSizing.accessorKey === 'number') {
return String(columnWithSizing.accessorKey)
}
return undefined
}
function buildColumnSizingBounds(
columns: ColumnDef[]
): ColumnSizingBounds {
return columns.reduce((bounds, column) => {
const columnWithSizing = column as ColumnWithSizing
const columnId = getColumnId(column)
if (columnId) {
const minSize =
typeof columnWithSizing.minSize === 'number' &&
Number.isFinite(columnWithSizing.minSize)
? columnWithSizing.minSize
: undefined
const maxSize =
typeof columnWithSizing.maxSize === 'number' &&
Number.isFinite(columnWithSizing.maxSize)
? columnWithSizing.maxSize
: undefined
if (minSize !== undefined || maxSize !== undefined) {
bounds[columnId] = { minSize, maxSize }
}
}
if (Array.isArray(columnWithSizing.columns)) {
Object.assign(bounds, buildColumnSizingBounds(columnWithSizing.columns))
}
return bounds
}, {})
}
function getBoundedColumnSize(
columnId: string,
value: unknown,
bounds: ColumnSizingBounds
) {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return undefined
}
const columnBounds = bounds[columnId]
let size = value
if (columnBounds?.minSize !== undefined && size < columnBounds.minSize) {
size = columnBounds.minSize
}
if (columnBounds?.maxSize !== undefined && size > columnBounds.maxSize) {
size = columnBounds.maxSize
}
return size > 0 ? size : undefined
}
function readColumnSizing(
storageKey: string | undefined,
bounds: ColumnSizingBounds
): ColumnSizingState {
if (!storageKey || typeof window === 'undefined') return {}
try {
const raw = window.localStorage.getItem(storageKey)
if (!raw) return {}
const parsed = JSON.parse(raw) as unknown
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return {}
}
return Object.entries(parsed).reduce(
(sizing, [key, value]) => {
const boundedSize = getBoundedColumnSize(key, value, bounds)
if (boundedSize !== undefined) {
sizing[key] = boundedSize
}
return sizing
},
{}
)
} catch {
return {}
}
}
export function useDataTable(options: UseDataTableOptions) {
const {
data,
columns,
totalCount,
pageCount: explicitPageCount,
ensurePageInRange,
manualFiltering,
manualPagination,
manualSorting,
initialSorting = [],
initialColumnVisibility = {},
initialColumnSizing = {},
initialRowSelection = {},
initialExpanded = {},
initialPagination = { pageIndex: 0, pageSize: 20 },
withFilteredRowModel = !manualFiltering,
withPaginationRowModel = !manualPagination,
withSortedRowModel = !manualSorting && !manualPagination,
withFacetedRowModel = !manualFiltering,
withExpandedRowModel = false,
} = options
const columnVisibilityStorageKey =
typeof options.columnVisibilityStorageKey === 'string'
? options.columnVisibilityStorageKey
: undefined
const columnSizingStorageKey =
typeof options.columnSizingStorageKey === 'string'
? options.columnSizingStorageKey
: undefined
const resolvedInitialColumnVisibility = React.useMemo(
() => ({
...initialColumnVisibility,
...readColumnVisibility(columnVisibilityStorageKey),
}),
[columnVisibilityStorageKey, initialColumnVisibility]
)
const columnSizingBounds = React.useMemo(
() => buildColumnSizingBounds(columns),
[columns]
)
const resolvedInitialColumnSizing = React.useMemo(
() => ({
...initialColumnSizing,
...readColumnSizing(columnSizingStorageKey, columnSizingBounds),
}),
[columnSizingBounds, columnSizingStorageKey, initialColumnSizing]
)
const [sorting, onSortingChange] = useControllableTableState(
options.sorting,
initialSorting,
options.onSortingChange
)
const [columnVisibility, onColumnVisibilityChange] =
useControllableTableState(
options.columnVisibility,
resolvedInitialColumnVisibility,
options.onColumnVisibilityChange
)
const [columnSizing, onColumnSizingChange] = useControllableTableState(
options.columnSizing,
resolvedInitialColumnSizing,
options.onColumnSizingChange
)
const hydratedColumnVisibilityStorageKeyRef = React.useRef(
columnVisibilityStorageKey
)
const hydratedColumnSizingStorageKeyRef = React.useRef(columnSizingStorageKey)
const skipNextColumnVisibilityPersistRef = React.useRef(false)
const skipNextColumnSizingPersistRef = React.useRef(false)
const columnSizingPersistTimerRef = React.useRef(
undefined
)
const [rowSelection, onRowSelectionChange] = useControllableTableState(
options.rowSelection,
initialRowSelection,
options.onRowSelectionChange
)
const [expanded, onExpandedChange] = useControllableTableState(
options.expanded,
initialExpanded,
options.onExpandedChange
)
const [pagination, onPaginationChange] = useControllableTableState(
options.pagination,
initialPagination,
options.onPaginationChange
)
const resolvedPageCount =
explicitPageCount ??
(totalCount !== undefined
? Math.ceil(totalCount / pagination.pageSize)
: undefined)
const resolvedEnableSorting =
options.enableSorting ??
(!manualPagination ||
Boolean(options.sorting) ||
Boolean(options.onSortingChange))
const table = useReactTable({
data,
columns,
rowCount: totalCount,
pageCount: resolvedPageCount,
state: {
sorting,
columnVisibility,
columnSizing,
rowSelection,
expanded,
columnFilters: options.columnFilters,
globalFilter: options.globalFilter,
pagination,
},
enableRowSelection: options.enableRowSelection,
enableSorting: resolvedEnableSorting,
getRowId: options.getRowId,
getSubRows: options.getSubRows,
globalFilterFn: options.globalFilterFn,
autoResetPageIndex: options.autoResetPageIndex,
manualFiltering,
manualPagination,
manualSorting,
enableColumnResizing: options.enableColumnResizing,
columnResizeMode: 'onChange',
onSortingChange,
onColumnVisibilityChange,
onColumnSizingChange,
onRowSelectionChange,
onExpandedChange,
onColumnFiltersChange: options.onColumnFiltersChange,
onGlobalFilterChange: options.onGlobalFilterChange,
onPaginationChange,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: withFilteredRowModel
? getFilteredRowModel()
: undefined,
getPaginationRowModel: withPaginationRowModel
? getPaginationRowModel()
: undefined,
getSortedRowModel: withSortedRowModel ? getSortedRowModel() : undefined,
getFacetedRowModel: withFacetedRowModel ? getFacetedRowModel() : undefined,
getFacetedUniqueValues: withFacetedRowModel
? getFacetedUniqueValues()
: undefined,
getExpandedRowModel: withExpandedRowModel
? getExpandedRowModel()
: undefined,
})
const actualPageCount = table.getPageCount()
React.useEffect(() => {
ensurePageInRange?.(actualPageCount)
}, [actualPageCount, ensurePageInRange])
React.useEffect(() => {
if (
options.columnVisibility !== undefined ||
columnVisibilityStorageKey ===
hydratedColumnVisibilityStorageKeyRef.current
) {
return
}
hydratedColumnVisibilityStorageKeyRef.current = columnVisibilityStorageKey
skipNextColumnVisibilityPersistRef.current = true
onColumnVisibilityChange(() => resolvedInitialColumnVisibility)
}, [
columnVisibilityStorageKey,
onColumnVisibilityChange,
options.columnVisibility,
resolvedInitialColumnVisibility,
])
React.useEffect(() => {
if (
options.columnSizing !== undefined ||
columnSizingStorageKey === hydratedColumnSizingStorageKeyRef.current
) {
return
}
hydratedColumnSizingStorageKeyRef.current = columnSizingStorageKey
skipNextColumnSizingPersistRef.current = true
onColumnSizingChange(() => resolvedInitialColumnSizing)
}, [
columnSizingStorageKey,
onColumnSizingChange,
options.columnSizing,
resolvedInitialColumnSizing,
])
React.useEffect(() => {
if (!columnVisibilityStorageKey || typeof window === 'undefined') return
if (skipNextColumnVisibilityPersistRef.current) {
skipNextColumnVisibilityPersistRef.current = false
return
}
try {
window.localStorage.setItem(
columnVisibilityStorageKey,
JSON.stringify(columnVisibility)
)
} catch {
// Storage can be unavailable in private mode; table controls still work.
}
}, [columnVisibility, columnVisibilityStorageKey])
React.useEffect(() => {
if (!columnSizingStorageKey || typeof window === 'undefined') return
if (skipNextColumnSizingPersistRef.current) {
skipNextColumnSizingPersistRef.current = false
return
}
if (columnSizingPersistTimerRef.current !== undefined) {
window.clearTimeout(columnSizingPersistTimerRef.current)
}
columnSizingPersistTimerRef.current = window.setTimeout(() => {
try {
window.localStorage.setItem(
columnSizingStorageKey,
JSON.stringify(columnSizing)
)
} catch {
// Storage can be unavailable in private mode; table controls still work.
} finally {
columnSizingPersistTimerRef.current = undefined
}
}, COLUMN_SIZING_PERSIST_DELAY_MS)
return () => {
if (columnSizingPersistTimerRef.current !== undefined) {
window.clearTimeout(columnSizingPersistTimerRef.current)
columnSizingPersistTimerRef.current = undefined
}
}
}, [columnSizing, columnSizingStorageKey])
return {
table,
}
}