1
0
Fork 0
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:
Chaim Lev-Ari 2023-05-02 13:42:16 +07:00 committed by GitHub
parent f20d3e72b9
commit 757461d58b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
140 changed files with 1805 additions and 2872 deletions

View file

@ -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(

View file

@ -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;
}

View file

@ -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}
/>

View file

@ -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}
/>
)}
/>

View file

@ -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>

View file

@ -1,9 +0,0 @@
.expand-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}

View file

@ -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}
</>
);
}

View file

@ -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];
}

View file

@ -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>
);
};
}
}

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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>
))}
</>
);
}

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

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

View file

@ -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,
},
};
}

View file

@ -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);
}

View 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;
}

View file

@ -13,7 +13,7 @@ export function useGoToHighlightedRow<T extends { id: string }>(
handlePageChangeRef.current = goToPage;
});
const highlightedItemIdRef = useRef(highlightedItemId);
const highlightedItemIdRef = useRef<string>();
useEffect(() => {
if (

View file

@ -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;
}

View 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]
);
}