ConceptPortal-public/rsconcept/frontend/src/components/data-table/data-table.tsx

245 lines
6.8 KiB
TypeScript
Raw Normal View History

'use client';
'use no memo';
import { useCallback, useMemo, useState } from 'react';
import {
type ColumnSort,
2023-12-28 14:04:44 +03:00
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type PaginationState,
type RowData,
2023-12-28 14:04:44 +03:00
type RowSelectionState,
type SortingState,
type TableOptions,
type Updater,
2023-12-28 14:04:44 +03:00
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
2025-02-22 14:04:01 +03:00
import { type Styling } from '../props';
2025-02-12 21:36:25 +03:00
2025-03-12 11:55:43 +03:00
import { DefaultNoData } from './default-no-data';
import { PaginationTools } from './pagination-tools';
import { TableBody } from './table-body';
import { TableFooter } from './table-footer';
import { TableHeader } from './table-header';
2024-08-06 14:39:00 +03:00
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
/** Style to conditionally apply to rows. */
export interface IConditionalStyle<TData> {
/** Callback to determine if the style should be applied. */
2023-12-28 14:04:44 +03:00
when: (rowData: TData) => boolean;
/** Style to apply. */
2023-12-28 14:04:44 +03:00
style: React.CSSProperties;
}
export interface DataTableProps<TData extends RowData>
extends Styling,
2023-12-28 14:04:44 +03:00
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
/** Id of the component. */
id?: string;
/** Indicates that padding should be minimal. */
2023-12-28 14:04:44 +03:00
dense?: boolean;
/** Number of rows to display. */
rows?: number;
/** Height of the content. */
2024-03-20 15:03:53 +03:00
contentHeight?: string;
/** Top position of sticky header (0 if no other sticky elements are present). */
2023-12-28 14:04:44 +03:00
headPosition?: string;
/** Disable header. */
2023-12-28 14:04:44 +03:00
noHeader?: boolean;
/** Disable footer. */
2023-12-28 14:04:44 +03:00
noFooter?: boolean;
/** List of styles to conditionally apply to rows. */
2023-12-28 14:04:44 +03:00
conditionalRowStyles?: IConditionalStyle<TData>[];
/** Component to display when there is no data. */
2023-12-28 14:04:44 +03:00
noDataComponent?: React.ReactNode;
/** Callback to be called when a row is clicked. */
2025-02-22 14:04:01 +03:00
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
/** Callback to be called when a row is double clicked. */
2025-02-22 14:04:01 +03:00
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
2023-12-28 14:04:44 +03:00
/** Enable row selection. */
2023-12-28 14:04:44 +03:00
enableRowSelection?: boolean;
/** Current row selection. */
2023-12-28 14:04:44 +03:00
rowSelection?: RowSelectionState;
/** Enable hiding of columns. */
2023-12-28 14:04:44 +03:00
enableHiding?: boolean;
/** Current column visibility. */
2023-12-28 14:04:44 +03:00
columnVisibility?: VisibilityState;
/** Enable pagination. */
2023-12-28 14:04:44 +03:00
enablePagination?: boolean;
/** Number of rows per page. */
2023-12-28 14:04:44 +03:00
paginationPerPage?: number;
/** List of options to choose from for pagination. */
2023-12-28 14:04:44 +03:00
paginationOptions?: number[];
/** Callback to be called when the pagination option is changed. */
2023-12-28 14:04:44 +03:00
onChangePaginationOption?: (newValue: number) => void;
/** Enable sorting. */
2023-12-28 14:04:44 +03:00
enableSorting?: boolean;
/** Initial sorting. */
2023-12-28 14:04:44 +03:00
initialSorting?: ColumnSort;
}
/**
* Dta representation as a table.
2023-12-28 14:04:44 +03:00
*
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
* No sticky header if omitted
2023-12-28 14:04:44 +03:00
*/
2025-02-19 23:30:35 +03:00
export function DataTable<TData extends RowData>({
id,
2023-12-28 14:04:44 +03:00
style,
className,
dense,
rows,
2024-03-20 15:03:53 +03:00
contentHeight = '1.1875rem',
2023-12-28 14:04:44 +03:00
headPosition,
conditionalRowStyles,
noFooter,
noHeader,
onRowClicked,
onRowDoubleClicked,
noDataComponent,
enableRowSelection,
rowSelection,
enableHiding,
columnVisibility,
enableSorting,
initialSorting,
enablePagination,
2023-12-28 14:04:44 +03:00
paginationPerPage = 10,
paginationOptions = [10, 20, 30, 40, 50],
onChangePaginationOption,
...restProps
}: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
const [lastSelected, setLastSelected] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
2023-12-28 14:04:44 +03:00
pageSize: paginationPerPage
});
2023-12-28 14:04:44 +03:00
const handleChangePagination = useCallback(
(updater: Updater<PaginationState>) => {
setPagination(prev => {
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
onChangePaginationOption(resolvedValue.pageSize);
}
return resolvedValue;
});
},
[onChangePaginationOption]
);
const tableImpl = useReactTable({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
2023-12-28 14:04:44 +03:00
state: {
pagination: pagination,
sorting: sorting,
rowSelection: rowSelection ?? {},
columnVisibility: columnVisibility ?? {}
},
enableHiding: enableHiding,
onPaginationChange: enablePagination ? handleChangePagination : undefined,
onSortingChange: enableSorting ? setSorting : undefined,
enableMultiRowSelection: enableRowSelection,
...restProps
});
const isEmpty = tableImpl.getRowModel().rows.length === 0;
const fixedSize = useMemo(() => {
if (!rows) {
return undefined;
}
if (dense) {
2024-03-20 15:03:53 +03:00
return `calc(2px + (2px + ${contentHeight} + 0.5rem)*${rows} + ${noHeader ? '0px' : '(2px + 2.1875rem)'})`;
} else {
2024-03-20 15:03:53 +03:00
return `calc(2px + (2px + ${contentHeight} + 1rem)*${rows + (noHeader ? 0 : 1)})`;
}
2024-03-20 15:03:53 +03:00
}, [rows, dense, noHeader, contentHeight]);
const columnSizeVars = useMemo(() => {
const headers = tableImpl.getFlatHeaders();
const colSizes: Record<string, number> = {};
for (const header of headers) {
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
2024-10-23 16:21:08 +03:00
}, [tableImpl]);
return (
2024-05-11 20:53:36 +03:00
<div tabIndex={-1} id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
<table className='w-full' style={{ ...columnSizeVars }}>
2023-12-28 14:04:44 +03:00
{!noHeader ? (
<TableHeader
table={tableImpl}
enableRowSelection={enableRowSelection}
enableSorting={enableSorting}
headPosition={headPosition}
resetLastSelected={() => setLastSelected(null)}
2023-12-28 14:04:44 +03:00
/>
) : null}
<TableBody
table={tableImpl}
dense={dense}
2024-05-02 21:19:23 +03:00
noHeader={noHeader}
2023-12-28 14:04:44 +03:00
conditionalRowStyles={conditionalRowStyles}
enableRowSelection={enableRowSelection}
2024-03-25 23:10:29 +03:00
lastSelected={lastSelected}
onChangeLastSelected={setLastSelected}
2023-12-28 14:04:44 +03:00
onRowClicked={onRowClicked}
onRowDoubleClicked={onRowDoubleClicked}
/>
{!noFooter ? <TableFooter table={tableImpl} /> : null}
</table>
{enablePagination && !isEmpty ? (
<PaginationTools
id={id ? `${id}__pagination` : undefined}
2023-12-28 14:04:44 +03:00
table={tableImpl}
paginationOptions={paginationOptions}
/>
) : null}
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
</div>
);
}