/* 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, } }