'use client'; import { useControlled } from '@base-ui/utils/useControlled'; import { getCoreRowModel, getExpandedRowModel, getSortedRowModel, RowSelectionState, Updater, useReactTable, VisibilityState } from '@tanstack/react-table'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DataViewClearFilters } from './components/clear-filters'; import { DataViewCustom } from './components/custom'; import { DisplayAccess } from './components/display-access'; import { DisplayControls } from './components/display-controls'; import { DataViewEmptyState } from './components/empty-state'; import { Filters } from './components/filters'; import { DataViewList } from './components/list'; import { DataViewSearch } from './components/search'; import { DataViewTimeline } from './components/timeline'; import { Toolbar } from './components/toolbar'; import { DataViewZeroState } from './components/zero-state'; import { DataViewContext } from './context'; import { DataViewContextType, DataViewField, DataViewProps, defaultGroupOption, GroupedData, InternalQuery, TableQueryUpdateFn } from './data-view.types'; import { hasActiveQuery as computeHasActiveQuery, createRowIdResolver, fieldsToColumnDefs, getDefaultTableQuery, getFilteredRowModelWithFlatRows, getInitialColumnVisibility, groupData, hasQueryChanged, isGroupRowData, queryToTableState, transformToDataViewQuery } from './utils'; function DataViewRoot({ data = [], fields, query, mode = 'client', loading = false, totalRowCount, loadingRowCount = 3, defaultSort, children, onTableQueryChange, onLoadMore, onRowClick, onColumnVisibilityChange, onRowSelectionChange, getRowId, views, defaultView, view, onViewChange, groupByResolvers }: DataViewProps) { const defaultTableQuery = useMemo( () => getDefaultTableQuery(defaultSort, query, fields), [defaultSort, query, fields] ); // Active view (controlled / uncontrolled). const [activeView, setActiveViewUnwrapped] = useControlled< string | undefined >({ controlled: view, default: defaultView ?? views?.[0]?.value, name: 'DataView', state: 'view' }); const setActiveView = useCallback( (next: string) => { setActiveViewUnwrapped(next); onViewChange?.(next); }, [onViewChange, setActiveViewUnwrapped] ); // Per-view field overrides registered by mounted renderers. const [fieldsByView, setFieldsByView] = useState< Record[]> >({}); const registerFieldsForView = useCallback( (name: string, override: DataViewField[]) => { setFieldsByView(prev => { if (prev[name] === override) return prev; return { ...prev, [name]: override }; }); return () => { setFieldsByView(prev => { if (!(name in prev)) return prev; const next = { ...prev }; delete next[name]; return next; }); }; }, [] ); const effectiveFields = useMemo(() => { if (activeView && fieldsByView[activeView]) return fieldsByView[activeView]; return fields; }, [activeView, fieldsByView, fields]); const initialColumnVisibility = useMemo( () => getInitialColumnVisibility(fields), [fields] ); const [columnVisibility, setColumnVisibilityState] = useState(initialColumnVisibility); const handleColumnVisibilityChange = useCallback( (value: Updater) => { setColumnVisibilityState(prev => { const newValue = typeof value === 'function' ? value(prev) : value; onColumnVisibilityChange?.(newValue); return newValue; }); }, [onColumnVisibilityChange] ); // Lifted so a selection change invalidates the memoized context value; the // table instance identity is stable and can't do it. const [rowSelection, setRowSelectionState] = useState({}); const handleRowSelectionChange = useCallback( (value: Updater) => { setRowSelectionState(prev => { const newValue = typeof value === 'function' ? value(prev) : value; onRowSelectionChange?.(newValue); return newValue; }); }, [onRowSelectionChange] ); const [tableQuery, setTableQuery] = useState(defaultTableQuery); const oldQueryRef = useRef(null); const reactTableState = useMemo( () => queryToTableState(tableQuery), [tableQuery] ); const onDisplaySettingsReset = useCallback(() => { setTableQuery(prev => ({ ...prev, ...defaultTableQuery, sort: [defaultSort], group_by: [defaultGroupOption.id] })); handleColumnVisibilityChange(initialColumnVisibility); }, [ defaultSort, defaultTableQuery, initialColumnVisibility, handleColumnVisibilityChange ]); const group_by = tableQuery.group_by?.[0]; // Column defs are derived from EFFECTIVE fields so toolbar + headless filter/ // sort/visibility reflect the active view's metadata override. const columnDefs = useMemo( () => fieldsToColumnDefs(effectiveFields, tableQuery.filters), [effectiveFields, tableQuery.filters] ); const groupedData = useMemo( () => groupData(data, group_by, effectiveFields, groupByResolvers), [data, group_by, effectiveFields, groupByResolvers] ); useEffect(() => { if ( tableQuery && onTableQueryChange && hasQueryChanged(oldQueryRef.current, tableQuery) && mode === 'server' ) { onTableQueryChange(transformToDataViewQuery(tableQuery)); oldQueryRef.current = tableQuery; } }, [tableQuery, onTableQueryChange, mode]); const resolveRowId = useMemo(() => createRowIdResolver(getRowId), [getRowId]); const table = useReactTable({ data: groupedData as unknown as TData[], columns: columnDefs, getRowId: resolveRowId, // Group rows render without cells, so they have no checkbox, so keeping them // unselectable keeps `rowSelection` one key per data row. enableRowSelection: row => !isGroupRowData(row.original), getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getSubRows: row => (row as unknown as GroupedData)?.subRows || [], getSortedRowModel: mode === 'server' ? undefined : getSortedRowModel(), getFilteredRowModel: mode === 'server' ? undefined : getFilteredRowModelWithFlatRows(), manualSorting: mode === 'server', manualFiltering: mode === 'server', onColumnVisibilityChange: handleColumnVisibilityChange, onRowSelectionChange: handleRowSelectionChange, globalFilterFn: mode === 'server' ? undefined : 'auto', initialState: { columnVisibility: initialColumnVisibility }, filterFromLeafRows: true, state: { ...reactTableState, columnVisibility, rowSelection, expanded: group_by && group_by !== defaultGroupOption.id ? true : undefined } }); const updateTableQuery = useCallback((fn: TableQueryUpdateFn) => { setTableQuery(prev => fn(prev)); }, []); const loadMoreData = useCallback(() => { if (mode === 'server' && onLoadMore) onLoadMore(); }, [mode, onLoadMore]); const searchQuery = query?.search; useEffect(() => { if (searchQuery) { setTableQuery(prev => ({ ...prev, search: searchQuery })); } }, [searchQuery]); const rowCount = (() => { try { return table.getRowModel().rows.length; } catch { return data.length; } })(); const hasData = rowCount > 0 || loading; const hasActiveQueryFlag = useMemo( () => computeHasActiveQuery(tableQuery, defaultSort), [tableQuery, defaultSort] ); const isZeroState = !hasData && !hasActiveQueryFlag; const isEmptyState = !hasData && hasActiveQueryFlag; // The filter bar shows whenever there's data OR an active query. The pure // zero state (no data + no query) keeps the surface clean. const shouldShowFilters = hasData || hasActiveQueryFlag; const contextValue: DataViewContextType = useMemo( () => ({ table, fields: effectiveFields, rootFields: fields, data, mode, isLoading: loading, loadMoreData, tableQuery, updateTableQuery, onDisplaySettingsReset, defaultSort, totalRowCount, loadingRowCount, onRowClick, shouldShowFilters, columnVisibility, setColumnVisibility: handleColumnVisibilityChange, rowSelection, setRowSelection: handleRowSelectionChange, views, activeView, setActiveView, registerFieldsForView, hasData, hasActiveQuery: hasActiveQueryFlag, isZeroState, isEmptyState }), [ table, effectiveFields, fields, data, mode, loading, loadMoreData, tableQuery, updateTableQuery, onDisplaySettingsReset, defaultSort, totalRowCount, loadingRowCount, onRowClick, shouldShowFilters, columnVisibility, handleColumnVisibilityChange, rowSelection, handleRowSelectionChange, views, activeView, setActiveView, registerFieldsForView, hasData, hasActiveQueryFlag, isZeroState, isEmptyState ] ); return ( } > {children} ); } DataViewRoot.displayName = 'DataView'; // biome-ignore lint/suspicious/noShadowRestrictedNames: public component name intentionally matches the package export export const DataView = Object.assign(DataViewRoot, { List: DataViewList, Timeline: DataViewTimeline, Custom: DataViewCustom, DisplayAccess: DisplayAccess, EmptyState: DataViewEmptyState, ZeroState: DataViewZeroState, ClearFilters: DataViewClearFilters, Toolbar: Toolbar, Search: DataViewSearch, Filters: Filters, DisplayControls: DisplayControls });