mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
chore(deps): upgrade react-table to v8 [EE-4837] (#8245)
This commit is contained in:
parent
f20d3e72b9
commit
757461d58b
140 changed files with 1805 additions and 2872 deletions
|
@ -1,12 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
||||
import { ColumnInstance } from 'react-table';
|
||||
import { Columns } from 'lucide-react';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
interface Props<D extends object> {
|
||||
columns: ColumnInstance<D>[];
|
||||
columns: Column<D>[];
|
||||
onChange: (value: string[]) => void;
|
||||
value: string[];
|
||||
}
|
||||
|
@ -40,8 +40,12 @@ export function ColumnVisibilityMenu<D extends object>({
|
|||
{columns.map((column) => (
|
||||
<div key={column.id}>
|
||||
<Checkbox
|
||||
checked={column.isVisible}
|
||||
label={column.Header as string}
|
||||
checked={column.getIsVisible()}
|
||||
label={
|
||||
typeof column.columnDef.header === 'string'
|
||||
? column.columnDef.header
|
||||
: ''
|
||||
}
|
||||
id={`visibility_${column.id}`}
|
||||
onChange={(e) =>
|
||||
handleChangeColumnVisibility(
|
||||
|
|
|
@ -1,36 +1,40 @@
|
|||
import {
|
||||
useTable,
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
Column,
|
||||
Row,
|
||||
TableInstance,
|
||||
Table as TableInstance,
|
||||
TableState,
|
||||
TableRowProps,
|
||||
useExpanded,
|
||||
} from 'react-table';
|
||||
import { ReactNode } from 'react';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
useReactTable,
|
||||
Row,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFacetedMinMaxValues,
|
||||
getExpandedRowModel,
|
||||
TableOptions,
|
||||
} from '@tanstack/react-table';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
import { Table } from './Table';
|
||||
import { multiple } from './filter-types';
|
||||
import { useRowSelect } from './useRowSelect';
|
||||
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 { Table } from './Table';
|
||||
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
|
||||
import { BasicTableSettings } from './types';
|
||||
import { DatatableContent } from './DatatableContent';
|
||||
import { createSelectColumn } from './select-column';
|
||||
import { TableRow } from './TableRow';
|
||||
|
||||
export interface Props<D extends Record<string, unknown>> {
|
||||
export interface Props<
|
||||
D extends Record<string, unknown>,
|
||||
TSettings extends BasicTableSettings = BasicTableSettings
|
||||
> {
|
||||
dataset: D[];
|
||||
columns: readonly Column<D>[];
|
||||
columns: TableOptions<D>['columns'];
|
||||
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
||||
renderTableActions?(selectedRows: D[]): ReactNode;
|
||||
disableSelect?: boolean;
|
||||
|
@ -39,29 +43,20 @@ export interface Props<D extends Record<string, unknown>> {
|
|||
emptyContentLabel?: string;
|
||||
title?: string;
|
||||
titleIcon?: IconProps['icon'];
|
||||
initialTableState?: Partial<TableState<D>>;
|
||||
initialTableState?: Partial<TableState>;
|
||||
isLoading?: boolean;
|
||||
totalCount?: number;
|
||||
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;
|
||||
settingsManager: TSettings & {
|
||||
search: string;
|
||||
setSearch: (value: string) => void;
|
||||
};
|
||||
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
|
||||
getRowCanExpand?(row: Row<D>): boolean;
|
||||
noWidget?: boolean;
|
||||
}
|
||||
|
||||
|
@ -81,78 +76,83 @@ export function Datatable<D extends Record<string, unknown>>({
|
|||
totalCount = dataset.length,
|
||||
description,
|
||||
pageCount,
|
||||
|
||||
initialSortBy,
|
||||
initialPageSize = 10,
|
||||
onPageChange = () => {},
|
||||
|
||||
onPageSizeChange,
|
||||
onSortByChange,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
|
||||
onPageChange = () => null,
|
||||
settingsManager: settings,
|
||||
renderRow = defaultRenderRow,
|
||||
expandable = false,
|
||||
highlightedItemId,
|
||||
noWidget,
|
||||
getRowCanExpand,
|
||||
}: Props<D>) {
|
||||
const isServerSidePagination = typeof pageCount !== 'undefined';
|
||||
|
||||
const tableInstance = useTable<D>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: initialPageSize,
|
||||
sortBy: initialSortBy ? [initialSortBy] : [],
|
||||
globalFilter: searchValue,
|
||||
...initialTableState,
|
||||
},
|
||||
isRowSelectable,
|
||||
autoResetExpanded: false,
|
||||
autoResetSelectedRows: false,
|
||||
getRowId,
|
||||
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
expandable ? useExpanded : emptyPlugin,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
!disableSelect ? useRowSelectColumn : emptyPlugin
|
||||
const enableRowSelection = getIsSelectionEnabled(
|
||||
disableSelect,
|
||||
isRowSelectable
|
||||
);
|
||||
|
||||
const allColumns = useMemo(
|
||||
() => _.compact([!disableSelect && createSelectColumn<D>(), ...columns]),
|
||||
[disableSelect, columns]
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<D>({
|
||||
columns: allColumns,
|
||||
data: dataset,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: settings.pageSize,
|
||||
},
|
||||
sorting: settings.sortBy ? [settings.sortBy] : [],
|
||||
globalFilter: settings.search,
|
||||
|
||||
...initialTableState,
|
||||
},
|
||||
defaultColumn: {
|
||||
enableColumnFilter: false,
|
||||
enableHiding: true,
|
||||
},
|
||||
enableRowSelection,
|
||||
autoResetExpanded: false,
|
||||
globalFilterFn,
|
||||
getRowId,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
getRowCanExpand,
|
||||
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
|
||||
});
|
||||
|
||||
const tableState = tableInstance.getState();
|
||||
|
||||
useGoToHighlightedRow(
|
||||
isServerSidePagination,
|
||||
tableInstance.state.pageSize,
|
||||
tableInstance.rows,
|
||||
tableState.pagination.pageSize,
|
||||
tableInstance.getCoreRowModel().rows,
|
||||
handlePageChange,
|
||||
highlightedItemId
|
||||
);
|
||||
|
||||
const selectedItems = tableInstance.selectedFlatRows.map(
|
||||
(row) => row.original
|
||||
);
|
||||
const selectedRowModel = tableInstance.getSelectedRowModel();
|
||||
const selectedItems = selectedRowModel.rows.map((row) => row.original);
|
||||
|
||||
return (
|
||||
<Table.Container noWidget={noWidget}>
|
||||
<DatatableHeader
|
||||
onSearchChange={handleSearchBarChange}
|
||||
searchValue={searchValue}
|
||||
searchValue={settings.search}
|
||||
title={title}
|
||||
titleIcon={titleIcon}
|
||||
description={description}
|
||||
renderTableActions={() => renderTableActions(selectedItems)}
|
||||
renderTableSettings={() => renderTableSettings(tableInstance)}
|
||||
description={description}
|
||||
/>
|
||||
<DatatableContent<D>
|
||||
tableInstance={tableInstance}
|
||||
renderRow={(row, rowProps) =>
|
||||
renderRow(row, rowProps, highlightedItemId)
|
||||
}
|
||||
renderRow={(row) => renderRow(row, highlightedItemId)}
|
||||
emptyContentLabel={emptyContentLabel}
|
||||
isLoading={isLoading}
|
||||
onSortChange={handleSortChange}
|
||||
|
@ -161,8 +161,8 @@ export function Datatable<D extends Record<string, unknown>>({
|
|||
<DatatableFooter
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
page={tableInstance.state.pageIndex}
|
||||
pageSize={tableInstance.state.pageSize}
|
||||
page={tableState.pagination.pageIndex}
|
||||
pageSize={tableState.pagination.pageSize}
|
||||
totalCount={totalCount}
|
||||
totalSelected={selectedItems.length}
|
||||
/>
|
||||
|
@ -171,38 +171,81 @@ export function Datatable<D extends Record<string, unknown>>({
|
|||
|
||||
function handleSearchBarChange(value: string) {
|
||||
tableInstance.setGlobalFilter(value);
|
||||
onSearchChange(value);
|
||||
settings.setSearch(value);
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tableInstance.gotoPage(page);
|
||||
tableInstance.setPageIndex(page);
|
||||
onPageChange(page);
|
||||
}
|
||||
|
||||
function handleSortChange(colId: string, desc: boolean) {
|
||||
onSortByChange(colId, desc);
|
||||
settings.setSortBy(colId, desc);
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
tableInstance.setPageSize(pageSize);
|
||||
onPageSizeChange(pageSize);
|
||||
settings.setPageSize(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, {
|
||||
<TableRow<D>
|
||||
cells={row.getVisibleCells()}
|
||||
className={clsx({
|
||||
active: highlightedItemId === row.id,
|
||||
})}
|
||||
role={rowProps.role}
|
||||
style={rowProps.style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getIsSelectionEnabled<D extends Record<string, unknown>>(
|
||||
disabledSelect?: boolean,
|
||||
isRowSelectable?: Props<D>['isRowSelectable']
|
||||
) {
|
||||
if (disabledSelect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isRowSelectable) {
|
||||
return isRowSelectable;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function globalFilterFn<D>(
|
||||
row: Row<D>,
|
||||
columnId: string,
|
||||
filterValue: null | string
|
||||
): boolean {
|
||||
const value = row.getValue(columnId);
|
||||
|
||||
if (filterValue === null || filterValue === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterValueLower = filterValue.toLowerCase();
|
||||
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
return value.toString().toLowerCase().includes(filterValueLower);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => item.toLowerCase().includes(filterValueLower));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Row, TableInstance, TableRowProps } from 'react-table';
|
||||
import { Row, Table as TableInstance } from '@tanstack/react-table';
|
||||
|
||||
import { Table } from './Table';
|
||||
|
||||
interface Props<D extends Record<string, unknown>> {
|
||||
tableInstance: TableInstance<D>;
|
||||
renderRow(row: Row<D>, rowProps: TableRowProps): React.ReactNode;
|
||||
renderRow(row: Row<D>): React.ReactNode;
|
||||
onSortChange?(colId: string, desc: boolean): void;
|
||||
isLoading?: boolean;
|
||||
emptyContentLabel?: string;
|
||||
|
@ -17,42 +17,24 @@ export function DatatableContent<D extends Record<string, unknown>>({
|
|||
isLoading,
|
||||
emptyContentLabel,
|
||||
}: Props<D>) {
|
||||
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
|
||||
tableInstance;
|
||||
const headerGroups = tableInstance.getHeaderGroups();
|
||||
const pageRowModel = tableInstance.getPaginationRowModel();
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
return (
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<Table>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<Table.HeaderRow<D>
|
||||
key={headerGroup.id}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={onSortChange}
|
||||
/>
|
||||
))}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<tbody>
|
||||
<Table.Content<D>
|
||||
rows={page}
|
||||
rows={pageRowModel.rows}
|
||||
isLoading={isLoading}
|
||||
prepareRow={prepareRow}
|
||||
emptyContent={emptyContentLabel}
|
||||
renderRow={renderRow}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Row } from 'react-table';
|
||||
import { Row } from '@tanstack/react-table';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
|
||||
|
@ -7,25 +7,25 @@ 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;
|
||||
expandOnRowClick?: boolean;
|
||||
}
|
||||
|
||||
export function ExpandableDatatable<D extends Record<string, unknown>>({
|
||||
renderSubRow,
|
||||
getRowCanExpand = () => true,
|
||||
expandOnRowClick,
|
||||
...props
|
||||
}: Props<D>) {
|
||||
return (
|
||||
<Datatable<D>
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
expandable
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
getRowCanExpand={getRowCanExpand}
|
||||
renderRow={(row) => (
|
||||
<ExpandableDatatableTableRow<D>
|
||||
key={key}
|
||||
row={row}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
renderSubRow={renderSubRow}
|
||||
expandOnClick={expandOnRowClick}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,37 +1,33 @@
|
|||
import { CSSProperties, ReactNode } from 'react';
|
||||
import { Row } from 'react-table';
|
||||
import { ReactNode } from 'react';
|
||||
import { Row } from '@tanstack/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;
|
||||
expandOnClick?: boolean;
|
||||
}
|
||||
|
||||
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
|
||||
row,
|
||||
className,
|
||||
role,
|
||||
style,
|
||||
disableSelect,
|
||||
renderSubRow,
|
||||
expandOnClick,
|
||||
}: Props<D>) {
|
||||
const cells = row.getVisibleCells();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow<D>
|
||||
cells={row.cells}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
cells={cells}
|
||||
onClick={expandOnClick ? () => row.toggleExpanded() : undefined}
|
||||
/>
|
||||
{row.isExpanded && (
|
||||
{row.getIsExpanded() && (
|
||||
<tr>
|
||||
{!disableSelect && <td />}
|
||||
<td colSpan={disableSelect ? row.cells.length : row.cells.length - 1}>
|
||||
<td colSpan={disableSelect ? cells.length : cells.length - 1}>
|
||||
{renderSubRow(row)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
.expand-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { Row } from 'react-table';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import styles from './ExpandingCell.module.css';
|
||||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||
row: Row<D>;
|
||||
showExpandArrow: boolean;
|
||||
}
|
||||
|
||||
export function ExpandingCell<
|
||||
D extends Record<string, unknown> = Record<string, unknown>
|
||||
>({ row, showExpandArrow, children }: PropsWithChildren<Props<D>>) {
|
||||
return (
|
||||
<>
|
||||
{showExpandArrow && (
|
||||
<button type="button" className={styles.expandButton}>
|
||||
<Icon
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...row.getToggleRowExpandedProps()}
|
||||
icon={row.isExpanded ? ChevronDown : ChevronRight}
|
||||
className="mr-1"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
|
||||
import { ColumnInstance } from 'react-table';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
import { Check, Filter } from 'lucide-react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
export const DefaultFilter = filterHOC('Filter by state');
|
||||
|
||||
interface MultipleSelectionFilterProps {
|
||||
options: string[];
|
||||
value: string[];
|
||||
|
@ -28,12 +26,12 @@ export function MultipleSelectionFilter({
|
|||
<div>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
className={clsx('table-filter flex items-center', {
|
||||
'filter-active': enabled,
|
||||
})}
|
||||
className={clsx('table-filter', { 'filter-active': enabled })}
|
||||
>
|
||||
Filter
|
||||
<Icon icon={enabled ? Check : Filter} className="!ml-1" />
|
||||
<div className="flex items-center gap-1">
|
||||
Filter
|
||||
<Icon icon={enabled ? Check : Filter} />
|
||||
</div>
|
||||
</MenuButton>
|
||||
<MenuPopover className="dropdown-menu">
|
||||
<div className="tableMenu">
|
||||
|
@ -70,27 +68,54 @@ export function MultipleSelectionFilter({
|
|||
}
|
||||
}
|
||||
|
||||
export function filterHOC(menuTitle: string) {
|
||||
export function filterHOC<TData extends Record<string, unknown>>(
|
||||
menuTitle: string
|
||||
) {
|
||||
return function Filter({
|
||||
column: { filterValue, setFilter, preFilteredRows, id },
|
||||
column: { getFilterValue, setFilterValue, getFacetedRowModel, id },
|
||||
}: {
|
||||
column: ColumnInstance;
|
||||
column: Column<TData>;
|
||||
}) {
|
||||
const { flatRows } = getFacetedRowModel();
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options = new Set<string>();
|
||||
preFilteredRows.forEach((row) => {
|
||||
options.add(row.values[id]);
|
||||
flatRows.forEach(({ getValue }) => {
|
||||
const value = getValue<string>(id);
|
||||
|
||||
options.add(value);
|
||||
});
|
||||
return Array.from(options);
|
||||
}, [id, preFilteredRows]);
|
||||
}, [flatRows, id]);
|
||||
|
||||
const value = getFilterValue();
|
||||
|
||||
const valueAsArray = getValueAsArrayOfStrings(value);
|
||||
|
||||
return (
|
||||
<MultipleSelectionFilter
|
||||
options={options}
|
||||
filterKey={id}
|
||||
value={filterValue}
|
||||
onChange={setFilter}
|
||||
value={valueAsArray}
|
||||
onChange={setFilterValue}
|
||||
menuTitle={menuTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getValueAsArrayOfStrings(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (!value || (typeof value !== 'string' && typeof value !== 'number')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return [value.toString()];
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
|
|
|
@ -1,30 +1,36 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { ColumnDef, CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
export function buildNameColumn<T extends Record<string, unknown>>(
|
||||
nameKey: string,
|
||||
nameKey: keyof T,
|
||||
idKey: string,
|
||||
path: string
|
||||
) {
|
||||
const name: Column<T> = {
|
||||
Header: 'Name',
|
||||
accessor: (row) => row[nameKey],
|
||||
): ColumnDef<T> {
|
||||
const cell = createCell<T>();
|
||||
|
||||
return {
|
||||
header: 'Name',
|
||||
accessorKey: nameKey,
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
cell,
|
||||
enableSorting: true,
|
||||
sortingFn: 'text',
|
||||
};
|
||||
|
||||
return name;
|
||||
function createCell<T extends Record<string, unknown>>() {
|
||||
return function NameCell({ renderValue, row }: CellContext<T, unknown>) {
|
||||
const name = renderValue() || '';
|
||||
|
||||
function NameCell({ value: name, row }: CellProps<T, string>) {
|
||||
return (
|
||||
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
if (typeof name !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
import {
|
||||
useTable,
|
||||
useFilters,
|
||||
useSortBy,
|
||||
Column,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
TableOptions,
|
||||
TableState,
|
||||
usePagination,
|
||||
} from 'react-table';
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { defaultGetRowId } from './defaultGetRowId';
|
||||
import { Table } from './Table';
|
||||
import { multiple } from './filter-types';
|
||||
import { NestedTable } from './NestedTable';
|
||||
import { DatatableContent } from './DatatableContent';
|
||||
import { defaultGetRowId } from './defaultGetRowId';
|
||||
import { BasicTableSettings } from './types';
|
||||
|
||||
interface Props<D extends Record<string, unknown>> {
|
||||
dataset: D[];
|
||||
columns: readonly Column<D>[];
|
||||
columns: TableOptions<D>['columns'];
|
||||
|
||||
getRowId?(row: D): string;
|
||||
emptyContentLabel?: string;
|
||||
initialTableState?: Partial<TableState<D>>;
|
||||
initialTableState?: Partial<TableState>;
|
||||
isLoading?: boolean;
|
||||
defaultSortBy?: string;
|
||||
initialSortBy?: BasicTableSettings['sortBy'];
|
||||
}
|
||||
|
||||
export function NestedDatatable<D extends Record<string, unknown>>({
|
||||
|
@ -31,25 +32,26 @@ export function NestedDatatable<D extends Record<string, unknown>>({
|
|||
emptyContentLabel,
|
||||
initialTableState = {},
|
||||
isLoading,
|
||||
defaultSortBy,
|
||||
initialSortBy,
|
||||
}: Props<D>) {
|
||||
const tableInstance = useTable<D>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
sortBy: defaultSortBy ? [{ id: defaultSortBy, desc: true }] : [],
|
||||
...initialTableState,
|
||||
},
|
||||
autoResetSelectedRows: false,
|
||||
getRowId,
|
||||
const tableInstance = useReactTable<D>({
|
||||
columns,
|
||||
data: dataset,
|
||||
initialState: {
|
||||
sorting: initialSortBy ? [initialSortBy] : [],
|
||||
...initialTableState,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
defaultColumn: {
|
||||
enableColumnFilter: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
getRowId,
|
||||
autoResetExpanded: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<NestedTable>
|
||||
|
@ -58,15 +60,7 @@ export function NestedDatatable<D extends Record<string, unknown>>({
|
|||
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}
|
||||
/>
|
||||
)}
|
||||
renderRow={(row) => <Table.Row<D> cells={row.getVisibleCells()} />}
|
||||
/>
|
||||
</Table.Container>
|
||||
</NestedTable>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.inner-datatable .widget {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.inner-datatable {
|
||||
@apply rounded-md border border-solid border-gray-5 th-dark:border-gray-9;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { TableProps } from 'react-table';
|
||||
|
||||
import { TableContainer } from './TableContainer';
|
||||
import { TableActions } from './TableActions';
|
||||
import { TableFooter } from './TableFooter';
|
||||
import { TableTitleActions } from './TableTitleActions';
|
||||
import { TableContent } from './TableContent';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||
import { TableTitle } from './TableTitle';
|
||||
import { TableContent } from './TableContent';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
import { TableHeaderRow } from './TableHeaderRow';
|
||||
import { TableRow } from './TableRow';
|
||||
import { TableFooter } from './TableFooter';
|
||||
|
||||
function MainComponent({
|
||||
children,
|
||||
className,
|
||||
role,
|
||||
style,
|
||||
}: PropsWithChildren<TableProps>) {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function MainComponent({ children, className }: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table
|
||||
|
@ -26,8 +24,6 @@ function MainComponent({
|
|||
'table-hover table-filters nowrap-cells table',
|
||||
className
|
||||
)}
|
||||
role={role}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { Row, TableRowProps } from 'react-table';
|
||||
import { Fragment, PropsWithChildren } from 'react';
|
||||
import { Row } from '@tanstack/react-table';
|
||||
|
||||
interface Props<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||
isLoading?: boolean;
|
||||
rows: Row<T>[];
|
||||
emptyContent?: string;
|
||||
prepareRow(row: Row<T>): void;
|
||||
renderRow(row: Row<T>, rowProps: TableRowProps): React.ReactNode;
|
||||
renderRow(row: Row<T>): React.ReactNode;
|
||||
}
|
||||
|
||||
export function TableContent<
|
||||
|
@ -15,7 +14,6 @@ export function TableContent<
|
|||
isLoading = false,
|
||||
rows,
|
||||
emptyContent = 'No items available',
|
||||
prepareRow,
|
||||
renderRow,
|
||||
}: Props<T>) {
|
||||
if (isLoading) {
|
||||
|
@ -28,11 +26,9 @@ export function TableContent<
|
|||
|
||||
return (
|
||||
<>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
return renderRow(row, { key, className, role, style });
|
||||
})}
|
||||
{rows.map((row) => (
|
||||
<Fragment key={row.id}>{renderRow(row)}</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { TableHeaderProps } from 'react-table';
|
||||
import { CSSProperties, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
|
||||
import styles from './TableHeaderCell.module.css';
|
||||
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
|
||||
|
||||
interface Props {
|
||||
canFilter: boolean;
|
||||
canSort: boolean;
|
||||
headerProps: TableHeaderProps;
|
||||
isSorted: boolean;
|
||||
isSortedDesc?: boolean;
|
||||
onSortClick: (desc: boolean) => void;
|
||||
render: () => ReactNode;
|
||||
renderFilter: () => ReactNode;
|
||||
renderFilter?: () => ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function TableHeaderCell({
|
||||
headerProps: { className, role, style },
|
||||
canSort,
|
||||
render,
|
||||
onSortClick,
|
||||
isSorted,
|
||||
isSortedDesc = true,
|
||||
canFilter,
|
||||
|
||||
renderFilter,
|
||||
className,
|
||||
style,
|
||||
}: Props) {
|
||||
return (
|
||||
<th role={role} style={style} className={className}>
|
||||
<th style={style} className={className}>
|
||||
<div className="flex h-full flex-row flex-nowrap items-center gap-1">
|
||||
<SortWrapper
|
||||
canSort={canSort}
|
||||
|
@ -37,7 +37,7 @@ export function TableHeaderCell({
|
|||
>
|
||||
{render()}
|
||||
</SortWrapper>
|
||||
{canFilter ? renderFilter() : null}
|
||||
{renderFilter ? renderFilter() : null}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
|
@ -76,7 +76,6 @@ function SortWrapper({
|
|||
<TableHeaderSortIcons
|
||||
sorted={isSorted}
|
||||
descending={isSorted && !!isSortedDesc}
|
||||
className="ml-1"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
@ -1,48 +1,58 @@
|
|||
import { HeaderGroup, TableHeaderProps } from 'react-table';
|
||||
import { Header, flexRender } from '@tanstack/react-table';
|
||||
|
||||
import { filterHOC } from './Filter';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||
headers: HeaderGroup<D>[];
|
||||
headers: Header<D, unknown>[];
|
||||
onSortChange?(colId: string, desc: boolean): void;
|
||||
}
|
||||
|
||||
export function TableHeaderRow<
|
||||
D extends Record<string, unknown> = Record<string, unknown>
|
||||
>({
|
||||
headers,
|
||||
onSortChange,
|
||||
className,
|
||||
role,
|
||||
style,
|
||||
}: Props<D> & TableHeaderProps) {
|
||||
>({ headers, onSortChange }: Props<D>) {
|
||||
return (
|
||||
<tr className={className} role={role} style={style}>
|
||||
{headers.map((column) => (
|
||||
<TableHeaderCell
|
||||
headerProps={{
|
||||
...column.getHeaderProps({
|
||||
className: column.className,
|
||||
style: {
|
||||
width: column.disableResizing ? column.width : '',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
key={column.id}
|
||||
canSort={column.canSort}
|
||||
onSortClick={(desc) => {
|
||||
column.toggleSortBy(desc);
|
||||
if (onSortChange) {
|
||||
onSortChange(column.id, desc);
|
||||
<tr>
|
||||
{headers.map((header) => {
|
||||
const sortDirection = header.column.getIsSorted();
|
||||
const {
|
||||
meta: { className, width } = { className: '', width: undefined },
|
||||
} = header.column.columnDef;
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
className={className}
|
||||
style={{
|
||||
width,
|
||||
}}
|
||||
key={header.id}
|
||||
canSort={header.column.getCanSort()}
|
||||
onSortClick={(desc) => {
|
||||
header.column.toggleSorting(desc);
|
||||
if (onSortChange) {
|
||||
onSortChange(header.id, desc);
|
||||
}
|
||||
}}
|
||||
isSorted={!!sortDirection}
|
||||
isSortedDesc={sortDirection ? sortDirection === 'desc' : false}
|
||||
render={() =>
|
||||
flexRender(header.column.columnDef.header, header.getContext())
|
||||
}
|
||||
}}
|
||||
isSorted={column.isSorted}
|
||||
isSortedDesc={column.isSortedDesc}
|
||||
render={() => column.render('Header')}
|
||||
canFilter={!column.disableFilters}
|
||||
renderFilter={() => column.render('Filter')}
|
||||
/>
|
||||
))}
|
||||
renderFilter={
|
||||
header.column.getCanFilter()
|
||||
? () =>
|
||||
flexRender(
|
||||
header.column.columnDef.meta?.filter ||
|
||||
filterHOC('Filter'),
|
||||
{
|
||||
column: header.column,
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
import { Cell, TableRowProps } from 'react-table';
|
||||
import { Cell, flexRender } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>>
|
||||
extends Omit<TableRowProps, 'key'> {
|
||||
cells: Cell<D>[];
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||
cells: Cell<D, unknown>[];
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function TableRow<
|
||||
D extends Record<string, unknown> = Record<string, unknown>
|
||||
>({ cells, className, role, style }: Props<D>) {
|
||||
>({ cells, className, onClick }: Props<D>) {
|
||||
return (
|
||||
<tr className={className} role={role} style={style}>
|
||||
{cells.map((cell) => {
|
||||
const cellProps = cell.getCellProps({
|
||||
className: cell.className,
|
||||
});
|
||||
|
||||
return (
|
||||
<td
|
||||
className={cellProps.className}
|
||||
role={cellProps.role}
|
||||
style={cellProps.style}
|
||||
key={cellProps.key}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<tr
|
||||
className={clsx(className, { 'cursor-pointer': !!onClick })}
|
||||
onClick={onClick}
|
||||
>
|
||||
{cells.map((cell) => (
|
||||
<td key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export function emptyPlugin() {}
|
||||
|
||||
emptyPlugin.pluginName = 'emptyPlugin';
|
|
@ -1,49 +1,45 @@
|
|||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { CellProps, Column, HeaderProps } from 'react-table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
export function buildExpandColumn<T extends Record<string, unknown>>(
|
||||
isExpandable: (item: T) => boolean
|
||||
): Column<T> {
|
||||
export function buildExpandColumn<
|
||||
T extends Record<string, unknown>
|
||||
>(): ColumnDef<T> {
|
||||
return {
|
||||
id: 'expand',
|
||||
Header: ({
|
||||
filteredFlatRows,
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded,
|
||||
}: HeaderProps<T>) => {
|
||||
const hasExpandableItems = filteredFlatRows.some((item) =>
|
||||
isExpandable(item.original)
|
||||
);
|
||||
header: ({ table }) => {
|
||||
const hasExpandableItems = table.getExpandedRowModel().rows.length > 0;
|
||||
|
||||
return (
|
||||
hasExpandableItems && (
|
||||
<Button
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...getToggleAllRowsExpandedProps()}
|
||||
onClick={table.getToggleAllRowsExpandedHandler()}
|
||||
color="none"
|
||||
icon={isAllRowsExpanded ? ChevronDown : ChevronUp}
|
||||
icon={table.getIsAllRowsExpanded() ? 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,
|
||||
cell: ({ row }) =>
|
||||
row.getCanExpand() && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
row.toggleExpanded();
|
||||
}}
|
||||
color="none"
|
||||
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
|
||||
/>
|
||||
),
|
||||
enableColumnFilter: false,
|
||||
enableGlobalFilter: false,
|
||||
enableHiding: false,
|
||||
|
||||
meta: {
|
||||
width: 40,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { Row } from 'react-table';
|
||||
import { Row } from '@tanstack/react-table';
|
||||
|
||||
export function multiple<
|
||||
D extends Record<string, unknown> = Record<string, unknown>
|
||||
>(rows: Row<D>[], columnIds: string[], filterValue: string[] = []) {
|
||||
if (filterValue.length === 0 || columnIds.length === 0) {
|
||||
return rows;
|
||||
>({ getValue }: Row<D>, columnId: string, filterValue: string[]): boolean {
|
||||
if (filterValue.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return rows.filter((row) => {
|
||||
const value = row.values[columnIds[0]];
|
||||
return filterValue.includes(value);
|
||||
});
|
||||
const value = getValue(columnId) as string;
|
||||
|
||||
return filterValue.includes(value);
|
||||
}
|
||||
|
|
67
app/react/components/datatables/select-column.tsx
Normal file
67
app/react/components/datatables/select-column.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { ColumnDef, Row } from '@tanstack/react-table';
|
||||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
export function createSelectColumn<T>(): ColumnDef<T> {
|
||||
let lastSelectedId = '';
|
||||
|
||||
return {
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
id="select-all"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row, table }) => (
|
||||
<Checkbox
|
||||
id={`select-row-${row.id}`}
|
||||
checked={row.getIsSelected()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey) {
|
||||
const { rows, rowsById } = table.getRowModel();
|
||||
const rowsToToggle = getRowRange(rows, row.id, lastSelectedId);
|
||||
const isLastSelected = rowsById[lastSelectedId].getIsSelected();
|
||||
rowsToToggle.forEach((row) => row.toggleSelected(isLastSelected));
|
||||
}
|
||||
|
||||
lastSelectedId = row.id;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
width: 50,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getRowRange<T>(rows: Array<Row<T>>, idA: string, idB: string) {
|
||||
const range: Array<Row<T>> = [];
|
||||
let foundStart = false;
|
||||
let foundEnd = false;
|
||||
for (let index = 0; index < rows.length; index += 1) {
|
||||
const row = rows[index];
|
||||
if (row.id === idA || row.id === idB) {
|
||||
if (foundStart) {
|
||||
foundEnd = true;
|
||||
}
|
||||
if (!foundStart) {
|
||||
foundStart = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundStart) {
|
||||
range.push(row);
|
||||
}
|
||||
|
||||
if (foundEnd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
|
@ -13,7 +13,7 @@ export function useGoToHighlightedRow<T extends { id: string }>(
|
|||
handlePageChangeRef.current = goToPage;
|
||||
});
|
||||
|
||||
const highlightedItemIdRef = useRef(highlightedItemId);
|
||||
const highlightedItemIdRef = useRef<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
|
@ -1,481 +0,0 @@
|
|||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||
import { ChangeEvent, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
actions,
|
||||
makePropGetter,
|
||||
ensurePluginOrder,
|
||||
useGetLatest,
|
||||
useMountedLayoutEffect,
|
||||
Hooks,
|
||||
TableInstance,
|
||||
TableState,
|
||||
ActionType,
|
||||
ReducerTableState,
|
||||
IdType,
|
||||
Row,
|
||||
PropGetter,
|
||||
TableToggleRowsSelectedProps,
|
||||
TableToggleAllRowsSelectedProps,
|
||||
} from 'react-table';
|
||||
|
||||
type DefaultType = Record<string, unknown>;
|
||||
|
||||
interface UseRowSelectTableInstance<D extends DefaultType = DefaultType>
|
||||
extends TableInstance<D> {
|
||||
isAllRowSelected: boolean;
|
||||
selectSubRows: boolean;
|
||||
getSubRows(row: Row<D>): Row<D>[];
|
||||
isRowSelectable?(row: Row<D>): boolean;
|
||||
}
|
||||
|
||||
const pluginName = 'useRowSelect';
|
||||
|
||||
// Actions
|
||||
actions.resetSelectedRows = 'resetSelectedRows';
|
||||
actions.toggleAllRowsSelected = 'toggleAllRowsSelected';
|
||||
actions.toggleRowSelected = 'toggleRowSelected';
|
||||
actions.toggleAllPageRowsSelected = 'toggleAllPageRowsSelected';
|
||||
|
||||
export function useRowSelect<D extends DefaultType>(hooks: Hooks<D>) {
|
||||
hooks.getToggleRowSelectedProps = [
|
||||
defaultGetToggleRowSelectedProps as PropGetter<
|
||||
D,
|
||||
TableToggleRowsSelectedProps
|
||||
>,
|
||||
];
|
||||
hooks.getToggleAllRowsSelectedProps = [
|
||||
defaultGetToggleAllRowsSelectedProps as PropGetter<
|
||||
D,
|
||||
TableToggleAllRowsSelectedProps
|
||||
>,
|
||||
];
|
||||
hooks.getToggleAllPageRowsSelectedProps = [
|
||||
defaultGetToggleAllPageRowsSelectedProps as PropGetter<
|
||||
D,
|
||||
TableToggleAllRowsSelectedProps
|
||||
>,
|
||||
];
|
||||
hooks.stateReducers.push(
|
||||
reducer as (
|
||||
newState: TableState<D>,
|
||||
action: ActionType,
|
||||
previousState?: TableState<D>,
|
||||
instance?: TableInstance<D>
|
||||
) => ReducerTableState<D> | undefined
|
||||
);
|
||||
hooks.useInstance.push(useInstance as (instance: TableInstance<D>) => void);
|
||||
hooks.prepareRow.push(prepareRow);
|
||||
}
|
||||
|
||||
useRowSelect.pluginName = pluginName;
|
||||
|
||||
function defaultGetToggleRowSelectedProps<D extends DefaultType>(
|
||||
props: D,
|
||||
{ instance, row }: { instance: UseRowSelectTableInstance<D>; row: Row<D> }
|
||||
) {
|
||||
const {
|
||||
manualRowSelectedKey = 'isSelected',
|
||||
isRowSelectable = defaultIsRowSelectable,
|
||||
} = instance;
|
||||
let checked = false;
|
||||
|
||||
if (row.original && row.original[manualRowSelectedKey]) {
|
||||
checked = true;
|
||||
} else {
|
||||
checked = row.isSelected;
|
||||
}
|
||||
|
||||
return [
|
||||
props,
|
||||
{
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
row.toggleRowSelected(e.target.checked);
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
checked,
|
||||
title: 'Toggle Row Selected',
|
||||
indeterminate: row.isSomeSelected,
|
||||
disabled: !isRowSelectable(row),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function defaultGetToggleAllRowsSelectedProps<D extends DefaultType>(
|
||||
props: D,
|
||||
{ instance }: { instance: UseRowSelectTableInstance<D> }
|
||||
) {
|
||||
return [
|
||||
props,
|
||||
{
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
instance.toggleAllRowsSelected(e.target.checked);
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
checked: instance.isAllRowsSelected,
|
||||
title: 'Toggle All Rows Selected',
|
||||
indeterminate: Boolean(
|
||||
!instance.isAllRowsSelected &&
|
||||
Object.keys(instance.state.selectedRowIds).length
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function defaultGetToggleAllPageRowsSelectedProps<D extends DefaultType>(
|
||||
props: D,
|
||||
{ instance }: { instance: UseRowSelectTableInstance<D> }
|
||||
) {
|
||||
return [
|
||||
props,
|
||||
{
|
||||
onChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
instance.toggleAllPageRowsSelected(e.target.checked);
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
checked: instance.isAllPageRowsSelected,
|
||||
title: 'Toggle All Current Page Rows Selected',
|
||||
indeterminate: Boolean(
|
||||
!instance.isAllPageRowsSelected &&
|
||||
instance.page.some(({ id }) => instance.state.selectedRowIds[id])
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function reducer<D extends Record<string, unknown>>(
|
||||
state: TableState<D>,
|
||||
action: ActionType,
|
||||
_previousState?: TableState<D>,
|
||||
instance?: UseRowSelectTableInstance<D>
|
||||
) {
|
||||
if (action.type === actions.init) {
|
||||
return {
|
||||
...state,
|
||||
selectedRowIds: <Record<IdType<D>, boolean>>{},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actions.resetSelectedRows) {
|
||||
return {
|
||||
...state,
|
||||
selectedRowIds: instance?.initialState.selectedRowIds || {},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actions.toggleAllRowsSelected) {
|
||||
const { value: setSelected } = action;
|
||||
|
||||
if (!instance) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const {
|
||||
isAllRowsSelected,
|
||||
rowsById,
|
||||
nonGroupedRowsById = rowsById,
|
||||
isRowSelectable = defaultIsRowSelectable,
|
||||
} = instance;
|
||||
|
||||
const selectAll =
|
||||
typeof setSelected !== 'undefined' ? setSelected : !isAllRowsSelected;
|
||||
|
||||
// Only remove/add the rows that are visible on the screen
|
||||
// Leave all the other rows that are selected alone.
|
||||
const selectedRowIds = { ...state.selectedRowIds };
|
||||
|
||||
Object.keys(nonGroupedRowsById).forEach((rowId: IdType<D>) => {
|
||||
if (selectAll) {
|
||||
const row = rowsById[rowId];
|
||||
if (isRowSelectable(row)) {
|
||||
selectedRowIds[rowId] = true;
|
||||
}
|
||||
} else {
|
||||
delete selectedRowIds[rowId];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedRowIds,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actions.toggleRowSelected) {
|
||||
if (!instance) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { id, value: setSelected } = action;
|
||||
const {
|
||||
rowsById,
|
||||
selectSubRows = true,
|
||||
getSubRows,
|
||||
isRowSelectable = defaultIsRowSelectable,
|
||||
} = instance;
|
||||
|
||||
const isSelected = state.selectedRowIds[id];
|
||||
const shouldExist =
|
||||
typeof setSelected !== 'undefined' ? setSelected : !isSelected;
|
||||
|
||||
if (isSelected === shouldExist) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newSelectedRowIds = { ...state.selectedRowIds };
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function handleRowById(id: IdType<D>) {
|
||||
const row = rowsById[id];
|
||||
|
||||
if (!isRowSelectable(row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row.isGrouped) {
|
||||
if (shouldExist) {
|
||||
newSelectedRowIds[id] = true;
|
||||
} else {
|
||||
delete newSelectedRowIds[id];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectSubRows && getSubRows(row)) {
|
||||
getSubRows(row).forEach((row) => handleRowById(row.id));
|
||||
}
|
||||
}
|
||||
|
||||
handleRowById(id);
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedRowIds: newSelectedRowIds,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === actions.toggleAllPageRowsSelected) {
|
||||
if (!instance) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { value: setSelected } = action;
|
||||
const {
|
||||
page,
|
||||
rowsById,
|
||||
selectSubRows = true,
|
||||
isAllPageRowsSelected,
|
||||
getSubRows,
|
||||
} = instance;
|
||||
|
||||
const selectAll =
|
||||
typeof setSelected !== 'undefined' ? setSelected : !isAllPageRowsSelected;
|
||||
|
||||
const newSelectedRowIds = { ...state.selectedRowIds };
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function handleRowById(id: IdType<D>) {
|
||||
const row = rowsById[id];
|
||||
|
||||
if (!row.isGrouped) {
|
||||
if (selectAll) {
|
||||
newSelectedRowIds[id] = true;
|
||||
} else {
|
||||
delete newSelectedRowIds[id];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectSubRows && getSubRows(row)) {
|
||||
getSubRows(row).forEach((row) => handleRowById(row.id));
|
||||
}
|
||||
}
|
||||
|
||||
page.forEach((row) => handleRowById(row.id));
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedRowIds: newSelectedRowIds,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function useInstance<D extends Record<string, unknown>>(
|
||||
instance: UseRowSelectTableInstance<D>
|
||||
) {
|
||||
const {
|
||||
data,
|
||||
rows,
|
||||
getHooks,
|
||||
plugins,
|
||||
rowsById,
|
||||
nonGroupedRowsById = rowsById,
|
||||
autoResetSelectedRows = true,
|
||||
state: { selectedRowIds },
|
||||
selectSubRows = true,
|
||||
dispatch,
|
||||
page,
|
||||
getSubRows,
|
||||
isRowSelectable = defaultIsRowSelectable,
|
||||
} = instance;
|
||||
|
||||
ensurePluginOrder(
|
||||
plugins,
|
||||
['useFilters', 'useGroupBy', 'useSortBy', 'useExpanded', 'usePagination'],
|
||||
'useRowSelect'
|
||||
);
|
||||
|
||||
const selectedFlatRows = useMemo(() => {
|
||||
const selectedFlatRows = <Array<Row<D>>>[];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const isSelected = selectSubRows
|
||||
? getRowIsSelected(row, selectedRowIds, getSubRows)
|
||||
: !!selectedRowIds[row.id];
|
||||
row.isSelected = !!isSelected;
|
||||
row.isSomeSelected = isSelected === null;
|
||||
|
||||
if (isSelected) {
|
||||
selectedFlatRows.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
return selectedFlatRows;
|
||||
}, [rows, selectSubRows, selectedRowIds, getSubRows]);
|
||||
|
||||
let isAllRowsSelected = Boolean(
|
||||
Object.keys(nonGroupedRowsById).length && Object.keys(selectedRowIds).length
|
||||
);
|
||||
|
||||
let isAllPageRowsSelected = isAllRowsSelected;
|
||||
|
||||
if (isAllRowsSelected) {
|
||||
if (
|
||||
Object.keys(nonGroupedRowsById).some((id) => {
|
||||
const row = rowsById[id];
|
||||
|
||||
return !selectedRowIds[id] && isRowSelectable(row);
|
||||
})
|
||||
) {
|
||||
isAllRowsSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllRowsSelected) {
|
||||
if (
|
||||
page &&
|
||||
page.length &&
|
||||
page.some(({ id }) => {
|
||||
const row = rowsById[id];
|
||||
|
||||
return !selectedRowIds[id] && isRowSelectable(row);
|
||||
})
|
||||
) {
|
||||
isAllPageRowsSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
const getAutoResetSelectedRows = useGetLatest(autoResetSelectedRows);
|
||||
|
||||
useMountedLayoutEffect(() => {
|
||||
if (getAutoResetSelectedRows()) {
|
||||
dispatch({ type: actions.resetSelectedRows });
|
||||
}
|
||||
}, [dispatch, data]);
|
||||
|
||||
const toggleAllRowsSelected = useCallback(
|
||||
(value) => dispatch({ type: actions.toggleAllRowsSelected, value }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const toggleAllPageRowsSelected = useCallback(
|
||||
(value) => dispatch({ type: actions.toggleAllPageRowsSelected, value }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const toggleRowSelected = useCallback(
|
||||
(id, value) => dispatch({ type: actions.toggleRowSelected, id, value }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const getInstance = useGetLatest(instance);
|
||||
|
||||
const getToggleAllRowsSelectedProps = makePropGetter(
|
||||
getHooks().getToggleAllRowsSelectedProps,
|
||||
{ instance: getInstance() }
|
||||
);
|
||||
|
||||
const getToggleAllPageRowsSelectedProps = makePropGetter(
|
||||
getHooks().getToggleAllPageRowsSelectedProps,
|
||||
{ instance: getInstance() }
|
||||
);
|
||||
|
||||
Object.assign(instance, {
|
||||
selectedFlatRows,
|
||||
isAllRowsSelected,
|
||||
isAllPageRowsSelected,
|
||||
toggleRowSelected,
|
||||
toggleAllRowsSelected,
|
||||
getToggleAllRowsSelectedProps,
|
||||
getToggleAllPageRowsSelectedProps,
|
||||
toggleAllPageRowsSelected,
|
||||
});
|
||||
}
|
||||
|
||||
function prepareRow<D extends Record<string, unknown>>(
|
||||
row: Row<D>,
|
||||
{ instance }: { instance: TableInstance<D> }
|
||||
) {
|
||||
row.toggleRowSelected = (set) => instance.toggleRowSelected(row.id, set);
|
||||
|
||||
row.getToggleRowSelectedProps = makePropGetter(
|
||||
instance.getHooks().getToggleRowSelectedProps,
|
||||
{ instance, row }
|
||||
);
|
||||
}
|
||||
|
||||
function getRowIsSelected<D extends Record<string, unknown>>(
|
||||
row: Row<D>,
|
||||
selectedRowIds: Record<IdType<D>, boolean>,
|
||||
getSubRows: (row: Row<D>) => Array<Row<D>>
|
||||
) {
|
||||
if (selectedRowIds[row.id]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subRows = getSubRows(row);
|
||||
|
||||
if (subRows && subRows.length) {
|
||||
let allChildrenSelected = true;
|
||||
let someSelected = false;
|
||||
|
||||
subRows.forEach((subRow) => {
|
||||
// Bail out early if we know both of these
|
||||
if (someSelected && !allChildrenSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getRowIsSelected(subRow, selectedRowIds, getSubRows)) {
|
||||
someSelected = true;
|
||||
} else {
|
||||
allChildrenSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allChildrenSelected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return someSelected ? null : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function defaultIsRowSelectable<D extends DefaultType>(row: Row<D>) {
|
||||
return !row.original.disabled;
|
||||
}
|
29
app/react/components/datatables/useTableState.ts
Normal file
29
app/react/components/datatables/useTableState.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { useSearchBarState } from './SearchBar';
|
||||
import { BasicTableSettings, createPersistedStore } from './types';
|
||||
|
||||
/** this class is just a dummy class to get return type of createPersistedStore
|
||||
* can be fixed after upgrade to ts 4.7+
|
||||
* https://stackoverflow.com/a/64919133
|
||||
*/
|
||||
class Wrapper<T extends BasicTableSettings> {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
wrapped() {
|
||||
return createPersistedStore<T>('', '');
|
||||
}
|
||||
}
|
||||
|
||||
export function useTableState<
|
||||
TSettings extends BasicTableSettings = BasicTableSettings
|
||||
>(store: ReturnType<Wrapper<TSettings>['wrapped']>, storageKey: string) {
|
||||
const settings = useStore(store);
|
||||
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
return useMemo(
|
||||
() => ({ ...settings, setSearch, search }),
|
||||
[settings, search, setSearch]
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue