2023-12-13 14:32:57 +03:00
|
|
|
'use client';
|
|
|
|
|
2023-11-26 02:24:16 +03:00
|
|
|
import {
|
|
|
|
Cell, ColumnSort,
|
|
|
|
createColumnHelper, flexRender, getCoreRowModel,
|
2023-12-05 01:22:44 +03:00
|
|
|
getPaginationRowModel, getSortedRowModel, Header, HeaderGroup,
|
2023-11-26 02:24:16 +03:00
|
|
|
PaginationState, Row, RowData, type RowSelectionState,
|
|
|
|
SortingState, TableOptions, useReactTable, type VisibilityState
|
|
|
|
} from '@tanstack/react-table';
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
|
|
|
import DefaultNoData from './DefaultNoData';
|
|
|
|
import PaginationTools from './PaginationTools';
|
|
|
|
import SelectAll from './SelectAll';
|
|
|
|
import SelectRow from './SelectRow';
|
|
|
|
import SortingIcon from './SortingIcon';
|
|
|
|
|
|
|
|
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
|
|
|
|
|
|
|
|
export interface IConditionalStyle<TData> {
|
|
|
|
when: (rowData: TData) => boolean
|
|
|
|
style: React.CSSProperties
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DataTableProps<TData extends RowData>
|
|
|
|
extends Pick<TableOptions<TData>,
|
|
|
|
'data' | 'columns' |
|
|
|
|
'onRowSelectionChange' | 'onColumnVisibilityChange'
|
|
|
|
> {
|
|
|
|
dense?: boolean
|
|
|
|
headPosition?: string
|
|
|
|
noHeader?: boolean
|
|
|
|
noFooter?: boolean
|
|
|
|
conditionalRowStyles?: IConditionalStyle<TData>[]
|
|
|
|
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
|
|
|
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
|
|
|
noDataComponent?: React.ReactNode
|
|
|
|
|
|
|
|
enableRowSelection?: boolean
|
|
|
|
rowSelection?: RowSelectionState
|
|
|
|
|
|
|
|
enableHiding?: boolean
|
|
|
|
columnVisibility?: VisibilityState
|
|
|
|
|
|
|
|
enablePagination?: boolean
|
|
|
|
paginationPerPage?: number
|
|
|
|
paginationOptions?: number[]
|
|
|
|
onChangePaginationOption?: (newValue: number) => void
|
|
|
|
|
|
|
|
enableSorting?: boolean
|
|
|
|
initialSorting?: ColumnSort
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* UI element: data representation as a table.
|
|
|
|
*
|
|
|
|
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
|
|
|
* No sticky header if omitted
|
|
|
|
*/
|
2023-12-13 14:32:57 +03:00
|
|
|
function DataTable<TData extends RowData>({
|
2023-11-26 02:24:16 +03:00
|
|
|
dense, headPosition, conditionalRowStyles, noFooter, noHeader,
|
|
|
|
onRowClicked, onRowDoubleClicked, noDataComponent,
|
|
|
|
|
|
|
|
enableRowSelection,
|
|
|
|
rowSelection,
|
|
|
|
|
|
|
|
enableHiding,
|
|
|
|
columnVisibility,
|
|
|
|
|
|
|
|
enableSorting,
|
|
|
|
initialSorting,
|
|
|
|
|
|
|
|
enablePagination,
|
|
|
|
paginationPerPage=10,
|
|
|
|
paginationOptions=[10, 20, 30, 40, 50],
|
|
|
|
onChangePaginationOption,
|
|
|
|
|
2023-12-05 01:22:44 +03:00
|
|
|
...restProps
|
2023-11-26 02:24:16 +03:00
|
|
|
}: DataTableProps<TData>) {
|
|
|
|
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
|
|
|
|
|
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
|
|
pageIndex: 0,
|
|
|
|
pageSize: paginationPerPage,
|
|
|
|
});
|
|
|
|
|
|
|
|
const tableImpl = useReactTable({
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
|
|
|
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
|
|
|
|
|
|
|
state: {
|
|
|
|
pagination: pagination,
|
|
|
|
sorting: sorting,
|
|
|
|
rowSelection: rowSelection ?? {},
|
|
|
|
columnVisibility: columnVisibility ?? {}
|
|
|
|
},
|
|
|
|
enableHiding: enableHiding,
|
|
|
|
onPaginationChange: enablePagination ? setPagination : undefined,
|
|
|
|
onSortingChange: enableSorting ? setSorting : undefined,
|
|
|
|
enableMultiRowSelection: enableRowSelection,
|
2023-12-05 01:22:44 +03:00
|
|
|
...restProps
|
2023-11-26 02:24:16 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
|
|
|
|
|
|
|
function getRowStyles(row: Row<TData>) {
|
2023-12-05 01:22:44 +03:00
|
|
|
return ({...conditionalRowStyles!
|
2023-11-26 02:24:16 +03:00
|
|
|
.filter(item => item.when(row.original))
|
|
|
|
.reduce((prev, item) => ({...prev, ...item.style}), {})
|
2023-12-05 01:22:44 +03:00
|
|
|
});
|
2023-11-26 02:24:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className='w-full'>
|
|
|
|
<div className='flex flex-col items-stretch'>
|
|
|
|
<table>
|
2023-11-27 11:33:34 +03:00
|
|
|
{!noHeader ?
|
2023-11-26 02:24:16 +03:00
|
|
|
<thead
|
|
|
|
className={`clr-app shadow-border`}
|
|
|
|
style={{
|
|
|
|
top: headPosition,
|
|
|
|
position: 'sticky'
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{tableImpl.getHeaderGroups().map(
|
|
|
|
(headerGroup: HeaderGroup<TData>) => (
|
|
|
|
<tr key={headerGroup.id}>
|
2023-11-27 11:33:34 +03:00
|
|
|
{enableRowSelection ?
|
2023-11-26 02:24:16 +03:00
|
|
|
<th className='pl-3 pr-1'>
|
|
|
|
<SelectAll table={tableImpl} />
|
2023-11-27 11:33:34 +03:00
|
|
|
</th> : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
{headerGroup.headers.map(
|
|
|
|
(header: Header<TData, unknown>) => (
|
|
|
|
<th key={header.id}
|
|
|
|
colSpan={header.colSpan}
|
|
|
|
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
|
|
|
|
style={{
|
|
|
|
textAlign: header.getSize() > 100 ? 'left': 'center',
|
|
|
|
width: header.getSize(),
|
|
|
|
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto',
|
|
|
|
}}
|
|
|
|
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
|
|
|
>
|
2023-11-27 11:33:34 +03:00
|
|
|
{!header.isPlaceholder ? (
|
2023-11-26 02:24:16 +03:00
|
|
|
<div className='flex gap-1'>
|
|
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
2023-11-27 11:33:34 +03:00
|
|
|
{(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null}
|
|
|
|
</div>) : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
</th>
|
|
|
|
))}
|
|
|
|
</tr>
|
|
|
|
))}
|
2023-11-27 11:33:34 +03:00
|
|
|
</thead> : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
|
|
|
|
<tbody>
|
|
|
|
{tableImpl.getRowModel().rows.map(
|
|
|
|
(row: Row<TData>, index) => (
|
|
|
|
<tr
|
|
|
|
key={row.id}
|
|
|
|
className={
|
|
|
|
row.getIsSelected() ? 'clr-selected clr-hover' :
|
|
|
|
index % 2 === 0 ? 'clr-controls clr-hover' : 'clr-app clr-hover'
|
|
|
|
}
|
|
|
|
style={conditionalRowStyles && getRowStyles(row)}
|
|
|
|
>
|
2023-11-27 11:33:34 +03:00
|
|
|
{enableRowSelection ?
|
2023-11-26 02:24:16 +03:00
|
|
|
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
|
|
|
<SelectRow row={row} />
|
2023-11-27 11:33:34 +03:00
|
|
|
</td> : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
{row.getVisibleCells().map(
|
|
|
|
(cell: Cell<TData, unknown>) => (
|
|
|
|
<td
|
|
|
|
key={cell.id}
|
|
|
|
className='px-2 border-y'
|
|
|
|
style={{
|
|
|
|
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto',
|
|
|
|
paddingBottom: dense ? '0.25rem': '0.5rem',
|
|
|
|
paddingTop: dense ? '0.25rem': '0.5rem'
|
|
|
|
}}
|
2023-11-27 11:33:34 +03:00
|
|
|
onClick={event => onRowClicked ? onRowClicked(row.original, event) : undefined}
|
|
|
|
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
|
2023-11-26 02:24:16 +03:00
|
|
|
>
|
|
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
|
|
</td>
|
|
|
|
))}
|
|
|
|
</tr>
|
|
|
|
))}
|
|
|
|
</tbody>
|
|
|
|
|
2023-11-27 11:33:34 +03:00
|
|
|
{!noFooter ?
|
2023-11-26 02:24:16 +03:00
|
|
|
<tfoot>
|
|
|
|
{tableImpl.getFooterGroups().map(
|
|
|
|
(footerGroup: HeaderGroup<TData>) => (
|
|
|
|
<tr key={footerGroup.id}>
|
|
|
|
{footerGroup.headers.map(
|
|
|
|
(header: Header<TData, unknown>) => (
|
|
|
|
<th key={header.id}>
|
2023-11-27 11:33:34 +03:00
|
|
|
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
</th>
|
|
|
|
))}
|
|
|
|
</tr>
|
|
|
|
))}
|
2023-11-27 11:33:34 +03:00
|
|
|
</tfoot> : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
</table>
|
|
|
|
|
2023-11-27 11:33:34 +03:00
|
|
|
{(enablePagination && !isEmpty) ?
|
2023-11-26 02:24:16 +03:00
|
|
|
<PaginationTools
|
|
|
|
table={tableImpl}
|
|
|
|
paginationOptions={paginationOptions}
|
|
|
|
onChangePaginationOption={onChangePaginationOption}
|
2023-11-27 11:33:34 +03:00
|
|
|
/> : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
</div>
|
2023-11-27 11:33:34 +03:00
|
|
|
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
|
2023-11-26 02:24:16 +03:00
|
|
|
</div>);
|
|
|
|
}
|
2023-12-13 14:32:57 +03:00
|
|
|
|
|
|
|
export default DataTable;
|