1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 07:49:41 +02:00

refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609)

This commit is contained in:
Chaim Lev-Ari 2022-11-22 14:16:34 +02:00 committed by GitHub
parent 0f0513c684
commit fe8e834dbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 1714 additions and 2717 deletions

View file

@ -8,79 +8,95 @@ import {
Row,
TableInstance,
TableState,
TableRowProps,
useExpanded,
} from 'react-table';
import { ReactNode, useEffect } from 'react';
import { ReactNode } from 'react';
import { useRowSelectColumn } from '@lineup-lite/hooks';
import clsx from 'clsx';
import { PaginationControls } from '@@/PaginationControls';
import { IconProps } from '@@/Icon';
import { Table } from './Table';
import { multiple } from './filter-types';
import { SearchBar, useSearchBarState } from './SearchBar';
import { SelectedRowsCount } from './SelectedRowsCount';
import { TableSettingsProvider } from './useZustandTableSettings';
import { useRowSelect } from './useRowSelect';
import { PaginationTableSettings, SortableTableSettings } from './types';
import { BasicTableSettings } from './types';
import { DatatableHeader } from './DatatableHeader';
import { DatatableFooter } from './DatatableFooter';
import { DatatableContent } from './DatatableContent';
import { defaultGetRowId } from './defaultGetRowId';
import { emptyPlugin } from './emptyReactTablePlugin';
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
interface DefaultTableSettings
extends SortableTableSettings,
PaginationTableSettings {}
interface TitleOptionsVisible {
title: string;
icon?: IconProps['icon'];
featherIcon?: IconProps['featherIcon'];
hide?: never;
}
type TitleOptions = TitleOptionsVisible | { hide: true };
interface Props<
D extends Record<string, unknown>,
TSettings extends DefaultTableSettings
> {
export interface Props<D extends Record<string, unknown>> {
dataset: D[];
storageKey: string;
columns: readonly Column<D>[];
renderTableSettings?(instance: TableInstance<D>): ReactNode;
renderTableActions?(selectedRows: D[]): ReactNode;
settingsStore: TSettings;
disableSelect?: boolean;
getRowId?(row: D): string;
isRowSelectable?(row: Row<D>): boolean;
emptyContentLabel?: string;
titleOptions: TitleOptions;
title?: string;
titleIcon?: IconProps['icon'];
initialTableState?: Partial<TableState<D>>;
isLoading?: boolean;
totalCount?: number;
description?: JSX.Element;
initialActiveItem?: string;
description?: ReactNode;
pageCount?: number;
initialSortBy?: BasicTableSettings['sortBy'];
initialPageSize?: BasicTableSettings['pageSize'];
highlightedItemId?: string;
searchValue: string;
onSearchChange(search: string): void;
onSortByChange(colId: string, desc: boolean): void;
onPageSizeChange(pageSize: number): void;
// send state up
onPageChange?(page: number): void;
renderRow?(
row: Row<D>,
rowProps: TableRowProps,
highlightedItemId?: string
): ReactNode;
expandable?: boolean;
noWidget?: boolean;
}
export function Datatable<
D extends Record<string, unknown>,
TSettings extends DefaultTableSettings
>({
export function Datatable<D extends Record<string, unknown>>({
columns,
dataset,
storageKey,
renderTableSettings,
renderTableActions,
settingsStore,
renderTableSettings = () => null,
renderTableActions = () => null,
disableSelect,
getRowId = defaultGetRowId,
isRowSelectable = () => true,
titleOptions,
title,
titleIcon,
emptyContentLabel,
initialTableState = {},
isLoading,
totalCount = dataset.length,
description,
initialActiveItem,
}: Props<D, TSettings>) {
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
pageCount,
initialSortBy,
initialPageSize = 10,
onPageChange = () => {},
onPageSizeChange,
onSortByChange,
searchValue,
onSearchChange,
renderRow = defaultRenderRow,
expandable = false,
highlightedItemId,
noWidget,
}: Props<D>) {
const isServerSidePagination = typeof pageCount !== 'undefined';
const tableInstance = useTable<D>(
{
@ -89,183 +105,104 @@ export function Datatable<
data: dataset,
filterTypes: { multiple },
initialState: {
pageSize: settingsStore.pageSize || 10,
sortBy: [settingsStore.sortBy],
globalFilter: searchBarValue,
pageSize: initialPageSize,
sortBy: initialSortBy ? [initialSortBy] : [],
globalFilter: searchValue,
...initialTableState,
},
isRowSelectable,
autoResetExpanded: false,
autoResetSelectedRows: false,
getRowId,
stateReducer: (newState, action) => {
switch (action.type) {
case 'setGlobalFilter':
setSearchBarValue(action.filterValue);
break;
case 'toggleSortBy':
settingsStore.setSortBy(action.columnId, action.desc);
break;
case 'setPageSize':
settingsStore.setPageSize(action.pageSize);
break;
default:
break;
}
return newState;
},
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
},
useFilters,
useGlobalFilter,
useSortBy,
expandable ? useExpanded : emptyPlugin,
usePagination,
useRowSelect,
!disableSelect ? useRowSelectColumn : emptyPlugin
);
const {
rows,
selectedFlatRows,
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
gotoPage,
setPageSize,
setGlobalFilter,
state: { pageIndex, pageSize },
} = tableInstance;
useGoToHighlightedRow(
isServerSidePagination,
tableInstance.state.pageSize,
tableInstance.rows,
handlePageChange,
highlightedItemId
);
useEffect(() => {
if (initialActiveItem && pageSize !== rows.length) {
const paginatedData = [...Array(Math.ceil(rows.length / pageSize))].map(
(_, i) => rows.slice(pageSize * i, pageSize + pageSize * i)
);
const itemPage = paginatedData.findIndex((sub) =>
sub.some((row) => row.id === initialActiveItem)
);
gotoPage(itemPage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialActiveItem]);
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
const selectedItems = selectedFlatRows.map((row) => row.original);
const selectedItems = tableInstance.selectedFlatRows.map(
(row) => row.original
);
return (
<div className="row">
<div className="col-sm-12">
<TableSettingsProvider settings={settingsStore}>
<Table.Container>
{isTitleVisible(titleOptions) && (
<Table.Title
label={titleOptions.title}
icon={titleOptions.icon}
featherIcon={titleOptions.featherIcon}
description={description}
>
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
{renderTableActions && (
<Table.Actions>
{renderTableActions(selectedItems)}
</Table.Actions>
)}
<Table.TitleActions>
{!!renderTableSettings && renderTableSettings(tableInstance)}
</Table.TitleActions>
</Table.Title>
)}
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<Table.HeaderRow<D>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
<Table.Content<D>
rows={page}
isLoading={isLoading}
prepareRow={prepareRow}
emptyContent={emptyContentLabel}
renderRow={(row, { key, className, role, style }) => (
<Table.Row<D>
cells={row.cells}
key={key}
className={clsx(
className,
initialActiveItem &&
initialActiveItem === row.id &&
'active'
)}
role={role}
style={style}
/>
)}
/>
</tbody>
</Table>
<Table.Footer>
<SelectedRowsCount value={selectedFlatRows.length} />
<PaginationControls
showAll
pageLimit={pageSize}
page={pageIndex + 1}
onPageChange={(p) => gotoPage(p - 1)}
totalCount={totalCount}
onPageLimitChange={setPageSize}
/>
</Table.Footer>
</Table.Container>
</TableSettingsProvider>
</div>
</div>
<Table.Container noWidget={noWidget}>
<DatatableHeader
onSearchChange={handleSearchBarChange}
searchValue={searchValue}
title={title}
titleIcon={titleIcon}
renderTableActions={() => renderTableActions(selectedItems)}
renderTableSettings={() => renderTableSettings(tableInstance)}
description={description}
/>
<DatatableContent<D>
tableInstance={tableInstance}
renderRow={(row, rowProps) =>
renderRow(row, rowProps, highlightedItemId)
}
emptyContentLabel={emptyContentLabel}
isLoading={isLoading}
onSortChange={handleSortChange}
/>
<DatatableFooter
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
page={tableInstance.state.pageIndex}
pageSize={tableInstance.state.pageSize}
totalCount={totalCount}
totalSelected={selectedItems.length}
/>
</Table.Container>
);
function handleSearchBarChange(value: string) {
tableInstance.setGlobalFilter(value);
onSearchChange(value);
}
function handlePageChange(page: number) {
tableInstance.gotoPage(page);
onPageChange(page);
}
function handleSortChange(colId: string, desc: boolean) {
onSortByChange(colId, desc);
}
function handlePageSizeChange(pageSize: number) {
tableInstance.setPageSize(pageSize);
onPageSizeChange(pageSize);
}
}
function defaultRenderRow<D extends Record<string, unknown>>(
row: Row<D>,
rowProps: TableRowProps,
highlightedItemId?: string
) {
return (
<Table.Row<D>
key={rowProps.key}
cells={row.cells}
className={clsx(rowProps.className, {
active: highlightedItemId === row.id,
})}
role={rowProps.role}
style={rowProps.style}
/>
);
}
function isTitleVisible(
titleSettings: TitleOptions
): titleSettings is TitleOptionsVisible {
return !titleSettings.hide;
}
function defaultGetRowId<D extends Record<string, unknown>>(row: D): string {
if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) {
return row.id.toString();
}
if (row.Id && (typeof row.Id === 'string' || typeof row.Id === 'number')) {
return row.Id.toString();
}
if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) {
return row.ID.toString();
}
return '';
}
function emptyPlugin() {}
emptyPlugin.pluginName = 'emptyPlugin';

View file

@ -0,0 +1,62 @@
import { Row, TableInstance, TableRowProps } from 'react-table';
import { Table } from './Table';
interface Props<D extends Record<string, unknown>> {
tableInstance: TableInstance<D>;
renderRow(row: Row<D>, rowProps: TableRowProps): React.ReactNode;
onSortChange?(colId: string, desc: boolean): void;
isLoading?: boolean;
emptyContentLabel?: string;
}
export function DatatableContent<D extends Record<string, unknown>>({
tableInstance,
renderRow,
onSortChange,
isLoading,
emptyContentLabel,
}: Props<D>) {
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
tableInstance;
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<Table.HeaderRow<D>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={onSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
<Table.Content<D>
rows={page}
isLoading={isLoading}
prepareRow={prepareRow}
emptyContent={emptyContentLabel}
renderRow={renderRow}
/>
</tbody>
</Table>
);
}

View file

@ -0,0 +1,36 @@
import { PaginationControls } from '@@/PaginationControls';
import { Table } from './Table';
import { SelectedRowsCount } from './SelectedRowsCount';
interface Props {
totalSelected: number;
pageSize: number;
page: number;
onPageChange(page: number): void;
totalCount: number;
onPageSizeChange(pageSize: number): void;
}
export function DatatableFooter({
totalSelected,
pageSize,
page,
onPageChange,
totalCount,
onPageSizeChange,
}: Props) {
return (
<Table.Footer>
<SelectedRowsCount value={totalSelected} />
<PaginationControls
showAll
pageLimit={pageSize}
page={page + 1}
onPageChange={(page) => onPageChange(page - 1)}
totalCount={totalCount}
onPageLimitChange={onPageSizeChange}
/>
</Table.Footer>
);
}

View file

@ -0,0 +1,42 @@
import { ReactNode } from 'react';
import { IconProps } from '@@/Icon';
import { SearchBar } from './SearchBar';
import { Table } from './Table';
type Props = {
title?: string;
titleIcon?: IconProps['icon'];
searchValue: string;
onSearchChange(value: string): void;
renderTableSettings?(): ReactNode;
renderTableActions?(): ReactNode;
description?: ReactNode;
};
export function DatatableHeader({
onSearchChange,
renderTableActions,
renderTableSettings,
searchValue,
title,
titleIcon,
description,
}: Props) {
if (!title) {
return null;
}
return (
<Table.Title label={title} icon={titleIcon} description={description}>
<SearchBar value={searchValue} onChange={onSearchChange} />
{renderTableActions && (
<Table.Actions>{renderTableActions()}</Table.Actions>
)}
<Table.TitleActions>
{!!renderTableSettings && renderTableSettings()}
</Table.TitleActions>
</Table.Title>
);
}

View file

@ -0,0 +1,33 @@
import { Row } from 'react-table';
import { ReactNode } from 'react';
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
import { Datatable, Props as DatatableProps } from './Datatable';
interface Props<D extends Record<string, unknown>>
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
renderSubRow(row: Row<D>): ReactNode;
}
export function ExpandableDatatable<D extends Record<string, unknown>>({
renderSubRow,
...props
}: Props<D>) {
return (
<Datatable<D>
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
expandable
renderRow={(row, { key, className, role, style }) => (
<ExpandableDatatableTableRow<D>
key={key}
row={row}
className={className}
role={role}
style={style}
renderSubRow={renderSubRow}
/>
)}
/>
);
}

View file

@ -0,0 +1,41 @@
import { CSSProperties, ReactNode } from 'react';
import { Row } from 'react-table';
import { TableRow } from './TableRow';
interface Props<D extends Record<string, unknown>> {
row: Row<D>;
className?: string;
role?: string;
style?: CSSProperties;
disableSelect?: boolean;
renderSubRow(row: Row<D>): ReactNode;
}
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
row,
className,
role,
style,
disableSelect,
renderSubRow,
}: Props<D>) {
return (
<>
<TableRow<D>
cells={row.cells}
className={className}
role={role}
style={style}
/>
{row.isExpanded && (
<tr>
{!disableSelect && <td />}
<td colSpan={disableSelect ? row.cells.length : row.cells.length - 1}>
{renderSubRow(row)}
</td>
</tr>
)}
</>
);
}

View file

@ -0,0 +1,74 @@
import {
useTable,
useFilters,
useSortBy,
Column,
TableState,
usePagination,
} from 'react-table';
import { Table } from './Table';
import { multiple } from './filter-types';
import { NestedTable } from './NestedTable';
import { DatatableContent } from './DatatableContent';
import { defaultGetRowId } from './defaultGetRowId';
interface Props<D extends Record<string, unknown>> {
dataset: D[];
columns: readonly Column<D>[];
getRowId?(row: D): string;
emptyContentLabel?: string;
initialTableState?: Partial<TableState<D>>;
isLoading?: boolean;
defaultSortBy?: string;
}
export function NestedDatatable<D extends Record<string, unknown>>({
columns,
dataset,
getRowId = defaultGetRowId,
emptyContentLabel,
initialTableState = {},
isLoading,
defaultSortBy,
}: Props<D>) {
const tableInstance = useTable<D>(
{
defaultCanFilter: false,
columns,
data: dataset,
filterTypes: { multiple },
initialState: {
sortBy: defaultSortBy ? [{ id: defaultSortBy, desc: true }] : [],
...initialTableState,
},
autoResetSelectedRows: false,
getRowId,
},
useFilters,
useSortBy,
usePagination
);
return (
<NestedTable>
<Table.Container>
<DatatableContent<D>
tableInstance={tableInstance}
isLoading={isLoading}
emptyContentLabel={emptyContentLabel}
renderRow={(row, { key, className, role, style }) => (
<Table.Row<D>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
)}
/>
</Table.Container>
</NestedTable>
);
}

View file

@ -1,7 +1,7 @@
import { PropsWithChildren } from 'react';
import './InnerDatatable.css';
import './NestedTable.css';
export function InnerDatatable({ children }: PropsWithChildren<unknown>) {
export function NestedTable({ children }: PropsWithChildren<unknown>) {
return <div className="inner-datatable">{children}</div>;
}

View file

@ -5,7 +5,7 @@ import {
import { Checkbox } from '@@/form-components/Checkbox';
import { useTableSettings } from './useZustandTableSettings';
import { useTableSettings } from './useTableSettings';
export interface Action {
id: QuickAction;
@ -17,7 +17,7 @@ interface Props {
}
export function QuickActionsSettings({ actions }: Props) {
const { settings } =
const settings =
useTableSettings<SettableQuickActionsTableSettings<QuickAction>>();
return (

View file

@ -35,6 +35,8 @@ function MainComponent({
);
}
MainComponent.displayName = 'Table';
interface SubComponents {
Container: typeof TableContainer;
Actions: typeof TableActions;

View file

@ -2,12 +2,28 @@ import { PropsWithChildren } from 'react';
import { Widget, WidgetBody } from '@@/Widget';
export function TableContainer({ children }: PropsWithChildren<unknown>) {
interface Props {
// workaround to remove the widget, ideally we should have a different component to wrap the table with a widget
noWidget?: boolean;
}
export function TableContainer({
children,
noWidget = false,
}: PropsWithChildren<Props>) {
if (noWidget) {
return <div className="datatable">{children}</div>;
}
return (
<div className="datatable">
<Widget>
<WidgetBody className="no-padding">{children}</WidgetBody>
</Widget>
<div className="row">
<div className="col-sm-12">
<div className="datatable">
<Widget>
<WidgetBody className="no-padding">{children}</WidgetBody>
</Widget>
</div>
</div>
</div>
);
}

View file

@ -6,7 +6,7 @@ interface Props {
icon?: ReactNode | ComponentType<unknown>;
featherIcon?: boolean;
label: string;
description?: JSX.Element;
description?: ReactNode;
}
export function TableTitle({
@ -34,7 +34,7 @@ export function TableTitle({
</div>
{children}
</div>
{description && description}
{description}
</div>
);
}

View file

@ -0,0 +1,17 @@
export function defaultGetRowId<D extends Record<string, unknown>>(
row: D
): string {
if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) {
return row.id.toString();
}
if (row.Id && (typeof row.Id === 'string' || typeof row.Id === 'number')) {
return row.Id.toString();
}
if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) {
return row.ID.toString();
}
return '';
}

View file

@ -0,0 +1,3 @@
export function emptyPlugin() {}
emptyPlugin.pluginName = 'emptyPlugin';

View file

@ -0,0 +1,49 @@
import { ChevronDown, ChevronUp } from 'react-feather';
import { CellProps, Column, HeaderProps } from 'react-table';
import { Button } from '@@/buttons';
export function buildExpandColumn<T extends Record<string, unknown>>(
isExpandable: (item: T) => boolean
): Column<T> {
return {
id: 'expand',
Header: ({
filteredFlatRows,
getToggleAllRowsExpandedProps,
isAllRowsExpanded,
}: HeaderProps<T>) => {
const hasExpandableItems = filteredFlatRows.some((item) =>
isExpandable(item.original)
);
return (
hasExpandableItems && (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...getToggleAllRowsExpandedProps()}
color="none"
icon={isAllRowsExpanded ? ChevronDown : ChevronUp}
/>
)
);
},
Cell: ({ row }: CellProps<T>) => (
<div className="vertical-center">
{isExpandable(row.original) && (
<Button
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...row.getToggleRowExpandedProps()}
color="none"
icon={row.isExpanded ? ChevronDown : ChevronUp}
/>
)}
</div>
),
disableFilters: true,
Filter: () => null,
canHide: false,
width: 30,
disableResizing: true,
};
}

View file

@ -1,15 +0,0 @@
export interface PaginationTableSettings {
pageSize: number;
}
export interface SortableTableSettings {
sortBy: { id: string; desc: boolean };
}
export interface SettableColumnsTableSettings {
hiddenColumns: string[];
}
export interface RefreshableTableSettings {
autoRefreshRate: number;
}

View file

@ -1,15 +1,20 @@
import { createStore } from 'zustand';
import { persist } from 'zustand/middleware';
import { keyBuilder } from '@/react/hooks/useLocalStorage';
export interface PaginationTableSettings {
pageSize: number;
setPageSize: (pageSize: number) => void;
}
type Set<T> = (
type ZustandSetFunc<T> = (
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
replace?: boolean | undefined
) => void;
export function paginationSettings(
set: Set<PaginationTableSettings>
set: ZustandSetFunc<PaginationTableSettings>
): PaginationTableSettings {
return {
pageSize: 10,
@ -23,12 +28,14 @@ export interface SortableTableSettings {
}
export function sortableSettings(
set: Set<SortableTableSettings>,
initialSortBy = 'name',
desc = false
set: ZustandSetFunc<SortableTableSettings>,
initialSortBy: string | { id: string; desc: boolean }
): SortableTableSettings {
return {
sortBy: { id: initialSortBy, desc },
sortBy:
typeof initialSortBy === 'string'
? { id: initialSortBy, desc: false }
: initialSortBy,
setSortBy: (id: string, desc: boolean) => set({ sortBy: { id, desc } }),
};
}
@ -39,7 +46,7 @@ export interface SettableColumnsTableSettings {
}
export function hiddenColumnsSettings(
set: Set<SettableColumnsTableSettings>
set: ZustandSetFunc<SettableColumnsTableSettings>
): SettableColumnsTableSettings {
return {
hiddenColumns: [],
@ -53,10 +60,38 @@ export interface RefreshableTableSettings {
}
export function refreshableSettings(
set: Set<RefreshableTableSettings>
set: ZustandSetFunc<RefreshableTableSettings>
): RefreshableTableSettings {
return {
autoRefreshRate: 0,
setAutoRefreshRate: (autoRefreshRate: number) => set({ autoRefreshRate }),
};
}
export interface BasicTableSettings
extends SortableTableSettings,
PaginationTableSettings {}
export function createPersistedStore<T extends BasicTableSettings>(
storageKey: string,
initialSortBy: string | { id: string; desc: boolean } = 'name',
create: (set: ZustandSetFunc<T>) => Omit<T, keyof BasicTableSettings> = () =>
({} as T)
) {
return createStore<T>()(
persist(
(set) =>
({
...sortableSettings(
set as ZustandSetFunc<SortableTableSettings>,
initialSortBy
),
...paginationSettings(set as ZustandSetFunc<PaginationTableSettings>),
...create(set),
} as T),
{
name: keyBuilder(storageKey),
}
)
);
}

View file

@ -0,0 +1,51 @@
import _ from 'lodash';
import { useRef, useLayoutEffect, useEffect } from 'react';
export function useGoToHighlightedRow<T extends { id: string }>(
isServerSidePagination: boolean,
pageSize: number,
rows: Array<T>,
goToPage: (page: number) => void,
highlightedItemId?: string
) {
const handlePageChangeRef = useRef(goToPage);
useLayoutEffect(() => {
handlePageChangeRef.current = goToPage;
});
const highlightedItemIdRef = useRef(highlightedItemId);
useEffect(() => {
if (
!isServerSidePagination &&
highlightedItemId &&
highlightedItemId !== highlightedItemIdRef.current
) {
const page = getRowPage(highlightedItemId, pageSize, rows);
if (page) {
handlePageChangeRef.current(page);
}
highlightedItemIdRef.current = highlightedItemId;
}
}, [highlightedItemId, isServerSidePagination, rows, pageSize]);
}
function getRowPage<T extends { id: string }>(
rowID: string,
pageSize: number,
rows: Array<T>
) {
const totalRows = rows.length;
if (!rowID || pageSize > totalRows) {
return 0;
}
const paginatedData = _.chunk(rows, pageSize);
const itemPage = paginatedData.findIndex((sub) =>
sub.some((row) => row.id === rowID)
);
return itemPage;
}

View file

@ -1,91 +1,34 @@
import {
Context,
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { Context, createContext, ReactNode, useContext } from 'react';
import { StoreApi, useStore } from 'zustand';
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
interface TableSettingsContextInterface<T> {
settings: T;
setTableSettings(partialSettings: Partial<T>): void;
setTableSettings(mutation: (settings: T) => T): void;
}
const TableSettingsContext = createContext<TableSettingsContextInterface<
Record<string, unknown>
> | null>(null);
const TableSettingsContext = createContext<StoreApi<object> | null>(null);
TableSettingsContext.displayName = 'TableSettingsContext';
export function useTableSettings<T>() {
export function useTableSettings<T extends object>() {
const Context = getContextType<T>();
const context = useContext(Context);
if (context === null) {
throw new Error('must be nested under TableSettingsProvider');
}
return context;
return useStore(context);
}
interface ProviderProps<T> {
interface ProviderProps<T extends object> {
children: ReactNode;
defaults?: T;
storageKey: string;
settings: StoreApi<T>;
}
export function TableSettingsProvider<T>({
export function TableSettingsProvider<T extends object>({
children,
defaults,
storageKey,
settings,
}: ProviderProps<T>) {
const Context = getContextType<T>();
const [storage, setStorage] = useLocalStorage<T>(
keyBuilder(storageKey),
defaults as T
);
const [settings, setTableSettings] = useState(storage);
const handleChange = useCallback(
(mutation: Partial<T> | ((settings: T) => T)): void => {
setTableSettings((settings) => {
const newTableSettings =
mutation instanceof Function
? mutation(settings)
: { ...settings, ...mutation };
setStorage(newTableSettings);
return newTableSettings;
});
},
[setStorage]
);
const contextValue = useMemo(
() => ({
settings,
setTableSettings: handleChange,
}),
[settings, handleChange]
);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
function keyBuilder(key: string) {
return `datatable_TableSettings_${key}`;
}
return <Context.Provider value={settings}>{children}</Context.Provider>;
}
function getContextType<T>() {
return TableSettingsContext as unknown as Context<
TableSettingsContextInterface<T>
>;
function getContextType<T extends object>() {
return TableSettingsContext as unknown as Context<StoreApi<T>>;
}

View file

@ -1,49 +0,0 @@
import { Context, createContext, ReactNode, useContext, useMemo } from 'react';
interface TableSettingsContextInterface<T> {
settings: T;
}
const TableSettingsContext = createContext<TableSettingsContextInterface<
Record<string, unknown>
> | null>(null);
TableSettingsContext.displayName = 'TableSettingsContext';
export function useTableSettings<T>() {
const Context = getContextType<T>();
const context = useContext(Context);
if (context === null) {
throw new Error('must be nested under TableSettingsProvider');
}
return context;
}
interface ProviderProps<T> {
children: ReactNode;
settings: T;
}
export function TableSettingsProvider<T>({
children,
settings,
}: ProviderProps<T>) {
const Context = getContextType<T>();
const contextValue = useMemo(
() => ({
settings,
}),
[settings]
);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
function getContextType<T>() {
return TableSettingsContext as unknown as Context<
TableSettingsContextInterface<T>
>;
}