/*
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 * as React from 'react'
import {
type ColumnDef,
type ColumnFiltersState,
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'
type DataTableFeatureOptions = Pick<
TableOptions,
| 'enableRowSelection'
| 'getRowId'
| 'getSubRows'
| 'globalFilterFn'
| 'autoResetPageIndex'
| 'manualFiltering'
| 'manualPagination'
| 'manualSorting'
>
type DataTableStateOptions = {
initialSorting?: SortingState
sorting?: SortingState
onSortingChange?: OnChangeFn
initialColumnVisibility?: VisibilityState
columnVisibilityStorageKey?: string | false
columnVisibility?: VisibilityState
onColumnVisibilityChange?: 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
}
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 {}
}
}
export function useDataTable(options: UseDataTableOptions) {
const {
data,
columns,
totalCount,
pageCount: explicitPageCount,
ensurePageInRange,
manualFiltering,
manualPagination,
manualSorting,
initialSorting = [],
initialColumnVisibility = {},
initialRowSelection = {},
initialExpanded = {},
initialPagination = { pageIndex: 0, pageSize: 20 },
withFilteredRowModel = !manualFiltering,
withPaginationRowModel = !manualPagination,
withSortedRowModel = !manualSorting,
withFacetedRowModel = !manualFiltering,
withExpandedRowModel = false,
} = options
const columnVisibilityStorageKey =
typeof options.columnVisibilityStorageKey === 'string'
? options.columnVisibilityStorageKey
: undefined
const resolvedInitialColumnVisibility = React.useMemo(
() => ({
...initialColumnVisibility,
...readColumnVisibility(columnVisibilityStorageKey),
}),
[columnVisibilityStorageKey, initialColumnVisibility]
)
const [sorting, onSortingChange] = useControllableTableState(
options.sorting,
initialSorting,
options.onSortingChange
)
const [columnVisibility, onColumnVisibilityChange] =
useControllableTableState(
options.columnVisibility,
resolvedInitialColumnVisibility,
options.onColumnVisibilityChange
)
const hydratedColumnVisibilityStorageKeyRef = React.useRef(
columnVisibilityStorageKey
)
const skipNextColumnVisibilityPersistRef = React.useRef(false)
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 table = useReactTable({
data,
columns,
rowCount: totalCount,
pageCount: resolvedPageCount,
state: {
sorting,
columnVisibility,
rowSelection,
expanded,
columnFilters: options.columnFilters,
globalFilter: options.globalFilter,
pagination,
},
enableRowSelection: options.enableRowSelection,
getRowId: options.getRowId,
getSubRows: options.getSubRows,
globalFilterFn: options.globalFilterFn,
autoResetPageIndex: options.autoResetPageIndex,
manualFiltering,
manualPagination,
manualSorting,
onSortingChange,
onColumnVisibilityChange,
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 (!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])
return {
table,
}
}