refactor(advancedSearch): reorder exports and improve type definitions

refactor(types): reorganize SearchCondition and AdvancedSearchState interfaces
refactor(filterPresets): streamline useFilterPresets hook and localStorage handling
refactor(filtering): clean up ColumnFilterButton and ColumnFilterPopover components
refactor(loading): separate GriddyLoadingOverlay from GriddyLoadingSkeleton
refactor(searchHistory): enhance useSearchHistory hook with persistence
refactor(index): update exports for adapters and core components
refactor(rendering): improve EditableCell and TableCell components for clarity
refactor(rendering): enhance TableHeader and VirtualBody components for better readability
This commit is contained in:
2026-02-15 19:54:33 +02:00
parent 9ec2e73640
commit 7244bd33fc
31 changed files with 3571 additions and 1305 deletions

View File

@@ -15,25 +15,33 @@ import {
type SortingState,
useReactTable,
type VisibilityState,
} from '@tanstack/react-table'
import React, { forwardRef, type Ref, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
} from '@tanstack/react-table';
import React, {
forwardRef,
type Ref,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import type { GriddyProps, GriddyRef } from './types'
import type { GriddyProps, GriddyRef } from './types';
import { AdvancedSearchPanel, advancedSearchGlobalFilterFn } from '../features/advancedSearch'
import { GriddyErrorBoundary } from '../features/errorBoundary'
import { useKeyboardNavigation } from '../features/keyboard/useKeyboardNavigation'
import { GriddyLoadingOverlay, GriddyLoadingSkeleton } from '../features/loading'
import { PaginationControl } from '../features/pagination'
import { SearchOverlay } from '../features/search/SearchOverlay'
import { GridToolbar } from '../features/toolbar'
import { useGridVirtualizer } from '../rendering/hooks/useGridVirtualizer'
import { TableHeader } from '../rendering/TableHeader'
import { VirtualBody } from '../rendering/VirtualBody'
import styles from '../styles/griddy.module.css'
import { mapColumns } from './columnMapper'
import { CSS, DEFAULTS } from './constants'
import { GriddyProvider, useGriddyStore } from './GriddyStore'
import { advancedSearchGlobalFilterFn, AdvancedSearchPanel } from '../features/advancedSearch';
import { GriddyErrorBoundary } from '../features/errorBoundary';
import { useKeyboardNavigation } from '../features/keyboard/useKeyboardNavigation';
import { GriddyLoadingOverlay, GriddyLoadingSkeleton } from '../features/loading';
import { PaginationControl } from '../features/pagination';
import { SearchOverlay } from '../features/search/SearchOverlay';
import { GridToolbar } from '../features/toolbar';
import { useGridVirtualizer } from '../rendering/hooks/useGridVirtualizer';
import { TableHeader } from '../rendering/TableHeader';
import { VirtualBody } from '../rendering/VirtualBody';
import styles from '../styles/griddy.module.css';
import { mapColumns } from './columnMapper';
import { CSS, DEFAULTS } from './constants';
import { GriddyProvider, useGriddyStore } from './GriddyStore';
// ─── Inner Component (lives inside Provider, has store access) ───────────────
@@ -45,119 +53,119 @@ function _Griddy<T>(props: GriddyProps<T>, ref: Ref<GriddyRef<T>>) {
</GriddyErrorBoundary>
{props.children}
</GriddyProvider>
)
);
}
// ─── Main Component with forwardRef ──────────────────────────────────────────
function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
// Read props from synced store
const data = useGriddyStore((s) => s.data)
const userColumns = useGriddyStore((s) => s.columns)
const getRowId = useGriddyStore((s) => s.getRowId)
const selection = useGriddyStore((s) => s.selection)
const search = useGriddyStore((s) => s.search)
const groupingConfig = useGriddyStore((s) => s.grouping)
const paginationConfig = useGriddyStore((s) => s.pagination)
const controlledSorting = useGriddyStore((s) => s.sorting)
const onSortingChange = useGriddyStore((s) => s.onSortingChange)
const controlledFilters = useGriddyStore((s) => s.columnFilters)
const onColumnFiltersChange = useGriddyStore((s) => s.onColumnFiltersChange)
const controlledPinning = useGriddyStore((s) => s.columnPinning)
const onColumnPinningChange = useGriddyStore((s) => s.onColumnPinningChange)
const controlledRowSelection = useGriddyStore((s) => s.rowSelection)
const onRowSelectionChange = useGriddyStore((s) => s.onRowSelectionChange)
const onEditCommit = useGriddyStore((s) => s.onEditCommit)
const rowHeight = useGriddyStore((s) => s.rowHeight)
const overscanProp = useGriddyStore((s) => s.overscan)
const height = useGriddyStore((s) => s.height)
const keyboardNavigation = useGriddyStore((s) => s.keyboardNavigation)
const className = useGriddyStore((s) => s.className)
const showToolbar = useGriddyStore((s) => s.showToolbar)
const exportFilename = useGriddyStore((s) => s.exportFilename)
const isLoading = useGriddyStore((s) => s.isLoading)
const filterPresets = useGriddyStore((s) => s.filterPresets)
const advancedSearch = useGriddyStore((s) => s.advancedSearch)
const persistenceKey = useGriddyStore((s) => s.persistenceKey)
const manualSorting = useGriddyStore((s) => s.manualSorting)
const manualFiltering = useGriddyStore((s) => s.manualFiltering)
const dataCount = useGriddyStore((s) => s.dataCount)
const setTable = useGriddyStore((s) => s.setTable)
const setVirtualizer = useGriddyStore((s) => s.setVirtualizer)
const setScrollRef = useGriddyStore((s) => s.setScrollRef)
const setFocusedRow = useGriddyStore((s) => s.setFocusedRow)
const setFocusedColumn = useGriddyStore((s) => s.setFocusedColumn)
const setEditing = useGriddyStore((s) => s.setEditing)
const setTotalRows = useGriddyStore((s) => s.setTotalRows)
const focusedRowIndex = useGriddyStore((s) => s.focusedRowIndex)
const data = useGriddyStore((s) => s.data);
const userColumns = useGriddyStore((s) => s.columns);
const getRowId = useGriddyStore((s) => s.getRowId);
const selection = useGriddyStore((s) => s.selection);
const search = useGriddyStore((s) => s.search);
const groupingConfig = useGriddyStore((s) => s.grouping);
const paginationConfig = useGriddyStore((s) => s.pagination);
const controlledSorting = useGriddyStore((s) => s.sorting);
const onSortingChange = useGriddyStore((s) => s.onSortingChange);
const controlledFilters = useGriddyStore((s) => s.columnFilters);
const onColumnFiltersChange = useGriddyStore((s) => s.onColumnFiltersChange);
const controlledPinning = useGriddyStore((s) => s.columnPinning);
const onColumnPinningChange = useGriddyStore((s) => s.onColumnPinningChange);
const controlledRowSelection = useGriddyStore((s) => s.rowSelection);
const onRowSelectionChange = useGriddyStore((s) => s.onRowSelectionChange);
const onEditCommit = useGriddyStore((s) => s.onEditCommit);
const rowHeight = useGriddyStore((s) => s.rowHeight);
const overscanProp = useGriddyStore((s) => s.overscan);
const height = useGriddyStore((s) => s.height);
const keyboardNavigation = useGriddyStore((s) => s.keyboardNavigation);
const className = useGriddyStore((s) => s.className);
const showToolbar = useGriddyStore((s) => s.showToolbar);
const exportFilename = useGriddyStore((s) => s.exportFilename);
const isLoading = useGriddyStore((s) => s.isLoading);
const filterPresets = useGriddyStore((s) => s.filterPresets);
const advancedSearch = useGriddyStore((s) => s.advancedSearch);
const persistenceKey = useGriddyStore((s) => s.persistenceKey);
const manualSorting = useGriddyStore((s) => s.manualSorting);
const manualFiltering = useGriddyStore((s) => s.manualFiltering);
const dataCount = useGriddyStore((s) => s.dataCount);
const setTable = useGriddyStore((s) => s.setTable);
const setVirtualizer = useGriddyStore((s) => s.setVirtualizer);
const setScrollRef = useGriddyStore((s) => s.setScrollRef);
const setFocusedRow = useGriddyStore((s) => s.setFocusedRow);
const setFocusedColumn = useGriddyStore((s) => s.setFocusedColumn);
const setEditing = useGriddyStore((s) => s.setEditing);
const setTotalRows = useGriddyStore((s) => s.setTotalRows);
const focusedRowIndex = useGriddyStore((s) => s.focusedRowIndex);
const effectiveRowHeight = rowHeight ?? DEFAULTS.rowHeight
const effectiveOverscan = overscanProp ?? DEFAULTS.overscan
const enableKeyboard = keyboardNavigation !== false
const effectiveRowHeight = rowHeight ?? DEFAULTS.rowHeight;
const effectiveOverscan = overscanProp ?? DEFAULTS.overscan;
const enableKeyboard = keyboardNavigation !== false;
// ─── Column Mapping ───
const columns = useMemo(
() => mapColumns(userColumns ?? [], selection) as ColumnDef<T, any>[],
[userColumns, selection],
)
[userColumns, selection]
);
// ─── Table State (internal/uncontrolled) ───
const [internalSorting, setInternalSorting] = useState<SortingState>([])
const [internalFilters, setInternalFilters] = useState<ColumnFiltersState>([])
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
const [globalFilter, setGlobalFilter] = useState<string | undefined>(undefined)
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])
const [internalSorting, setInternalSorting] = useState<SortingState>([]);
const [internalFilters, setInternalFilters] = useState<ColumnFiltersState>([]);
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({});
const [globalFilter, setGlobalFilter] = useState<string | undefined>(undefined);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([]);
// Build initial column pinning from column definitions
const initialPinning = useMemo(() => {
const left: string[] = []
const right: string[] = []
userColumns?.forEach(col => {
if (col.pinned === 'left') left.push(col.id)
else if (col.pinned === 'right') right.push(col.id)
})
return { left, right }
}, [userColumns])
const left: string[] = [];
const right: string[] = [];
userColumns?.forEach((col) => {
if (col.pinned === 'left') left.push(col.id);
else if (col.pinned === 'right') right.push(col.id);
});
return { left, right };
}, [userColumns]);
const [internalPinning, setInternalPinning] = useState<ColumnPinningState>(initialPinning)
const [grouping, setGrouping] = useState<GroupingState>(groupingConfig?.columns ?? [])
const [expanded, setExpanded] = useState({})
const [internalPinning, setInternalPinning] = useState<ColumnPinningState>(initialPinning);
const [grouping, setGrouping] = useState<GroupingState>(groupingConfig?.columns ?? []);
const [expanded, setExpanded] = useState({});
const [internalPagination, setInternalPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: paginationConfig?.pageSize ?? DEFAULTS.pageSize,
})
});
// Wrap pagination setters to call callbacks
const handlePaginationChange = (updater: any) => {
setInternalPagination(prev => {
const next = typeof updater === 'function' ? updater(prev) : updater
setInternalPagination((prev) => {
const next = typeof updater === 'function' ? updater(prev) : updater;
// Call callbacks if pagination config exists
if (paginationConfig) {
if (next.pageIndex !== prev.pageIndex && paginationConfig.onPageChange) {
paginationConfig.onPageChange(next.pageIndex)
paginationConfig.onPageChange(next.pageIndex);
}
if (next.pageSize !== prev.pageSize && paginationConfig.onPageSizeChange) {
paginationConfig.onPageSizeChange(next.pageSize)
paginationConfig.onPageSizeChange(next.pageSize);
}
}
return next
})
}
return next;
});
};
// Resolve controlled vs uncontrolled
const sorting = controlledSorting ?? internalSorting
const setSorting = onSortingChange ?? setInternalSorting
const columnFilters = controlledFilters ?? internalFilters
const setColumnFilters = onColumnFiltersChange ?? setInternalFilters
const columnPinning = controlledPinning ?? internalPinning
const setColumnPinning = onColumnPinningChange ?? setInternalPinning
const rowSelectionState = controlledRowSelection ?? internalRowSelection
const setRowSelection = onRowSelectionChange ?? setInternalRowSelection
const sorting = controlledSorting ?? internalSorting;
const setSorting = onSortingChange ?? setInternalSorting;
const columnFilters = controlledFilters ?? internalFilters;
const setColumnFilters = onColumnFiltersChange ?? setInternalFilters;
const columnPinning = controlledPinning ?? internalPinning;
const setColumnPinning = onColumnPinningChange ?? setInternalPinning;
const rowSelectionState = controlledRowSelection ?? internalRowSelection;
const setRowSelection = onRowSelectionChange ?? setInternalRowSelection;
// ─── Selection config ───
const enableRowSelection = selection ? selection.mode !== 'none' : false
const enableMultiRowSelection = selection?.mode === 'multi'
const enableRowSelection = selection ? selection.mode !== 'none' : false;
const enableMultiRowSelection = selection?.mode === 'multi';
// ─── TanStack Table Instance ───
const table = useReactTable<T>({
@@ -177,7 +185,7 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
getExpandedRowModel: getExpandedRowModel(),
getFilteredRowModel: manualFiltering ? undefined : getFilteredRowModel(),
getGroupedRowModel: groupingConfig?.enabled ? getGroupedRowModel() : undefined,
getRowId: getRowId as any ?? ((_, index) => String(index)),
getRowId: (getRowId as any) ?? ((_, index) => String(index)),
getSortedRowModel: manualSorting ? undefined : getSortedRowModel(),
manualFiltering: manualFiltering ?? false,
manualSorting: manualSorting ?? false,
@@ -206,10 +214,10 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
},
...(paginationConfig?.enabled ? { getPaginationRowModel: getPaginationRowModel() } : {}),
columnResizeMode: 'onChange',
})
});
// ─── Scroll Container Ref ───
const scrollRef = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(null);
// ─── TanStack Virtual ───
const virtualizer = useGridVirtualizer({
@@ -217,16 +225,22 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
rowHeight: effectiveRowHeight,
scrollRef,
table,
})
});
// ─── Sync table + virtualizer + scrollRef into store ───
useEffect(() => { setTable(table) }, [table, setTable])
useEffect(() => { setVirtualizer(virtualizer) }, [virtualizer, setVirtualizer])
useEffect(() => { setScrollRef(scrollRef.current) }, [setScrollRef])
useEffect(() => {
setTable(table);
}, [table, setTable]);
useEffect(() => {
setVirtualizer(virtualizer);
}, [virtualizer, setVirtualizer]);
useEffect(() => {
setScrollRef(scrollRef.current);
}, [setScrollRef]);
// ─── Keyboard Navigation ───
// Get the full store state for imperative access in keyboard handler
const storeState = useGriddyStore()
const storeState = useGriddyStore();
useKeyboardNavigation({
editingEnabled: !!onEditCommit,
@@ -236,59 +250,64 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
storeState,
table,
virtualizer,
})
});
// ─── Set initial focus when data loads ───
const rowCount = table.getRowModel().rows.length
const rowCount = table.getRowModel().rows.length;
useEffect(() => {
setTotalRows(rowCount)
setTotalRows(rowCount);
if (rowCount > 0 && focusedRowIndex === null) {
setFocusedRow(0)
setFocusedRow(0);
}
}, [rowCount, focusedRowIndex, setTotalRows, setFocusedRow])
}, [rowCount, focusedRowIndex, setTotalRows, setFocusedRow]);
// ─── Imperative Ref ───
useImperativeHandle(tableRef, () => ({
deselectAll: () => table.resetRowSelection(),
focusRow: (index: number) => {
setFocusedRow(index)
virtualizer.scrollToIndex(index, { align: 'auto' })
},
getTable: () => table,
getUIState: () => ({
focusedColumnId: null,
focusedRowIndex,
isEditing: false,
isSearchOpen: false,
isSelecting: false,
totalRows: rowCount,
} as any),
getVirtualizer: () => virtualizer,
scrollToRow: (index: number) => virtualizer.scrollToIndex(index, { align: 'auto' }),
selectRow: (id: string) => {
const row = table.getRowModel().rows.find((r) => r.id === id)
row?.toggleSelected(true)
},
startEditing: (rowId: string, columnId?: string) => {
const rowIndex = table.getRowModel().rows.findIndex((r) => r.id === rowId)
if (rowIndex >= 0) {
setFocusedRow(rowIndex)
if (columnId) setFocusedColumn(columnId)
setEditing(true)
}
},
}), [table, virtualizer, setFocusedRow, setFocusedColumn, setEditing, focusedRowIndex, rowCount])
useImperativeHandle(
tableRef,
() => ({
deselectAll: () => table.resetRowSelection(),
focusRow: (index: number) => {
setFocusedRow(index);
virtualizer.scrollToIndex(index, { align: 'auto' });
},
getTable: () => table,
getUIState: () =>
({
focusedColumnId: null,
focusedRowIndex,
isEditing: false,
isSearchOpen: false,
isSelecting: false,
totalRows: rowCount,
}) as any,
getVirtualizer: () => virtualizer,
scrollToRow: (index: number) => virtualizer.scrollToIndex(index, { align: 'auto' }),
selectRow: (id: string) => {
const row = table.getRowModel().rows.find((r) => r.id === id);
row?.toggleSelected(true);
},
startEditing: (rowId: string, columnId?: string) => {
const rowIndex = table.getRowModel().rows.findIndex((r) => r.id === rowId);
if (rowIndex >= 0) {
setFocusedRow(rowIndex);
if (columnId) setFocusedColumn(columnId);
setEditing(true);
}
},
}),
[table, virtualizer, setFocusedRow, setFocusedColumn, setEditing, focusedRowIndex, rowCount]
);
// ─── Render ───
const containerStyle: React.CSSProperties = {
height: height ?? '100%',
overflow: 'auto',
position: 'relative',
}
};
const focusedRow = focusedRowIndex !== null ? table.getRowModel().rows[focusedRowIndex] : null
const focusedRowId = focusedRow ? `griddy-row-${focusedRow.id}` : undefined
const focusedRow = focusedRowIndex !== null ? table.getRowModel().rows[focusedRowIndex] : null;
const focusedRowId = focusedRow ? `griddy-row-${focusedRow.id}` : undefined;
return (
<div
@@ -325,15 +344,12 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
)}
</div>
{paginationConfig?.enabled && (
<PaginationControl
pageSizeOptions={paginationConfig.pageSizeOptions}
table={table}
/>
<PaginationControl pageSizeOptions={paginationConfig.pageSizeOptions} table={table} />
)}
</div>
)
);
}
export const Griddy = forwardRef(_Griddy) as <T>(
props: GriddyProps<T> & React.RefAttributes<GriddyRef<T>>
) => React.ReactElement
) => React.ReactElement;