import { memo, useEffect, type ReactNode, type HTMLAttributes, useMemo, type ReactElement, useId, } from 'react'; import cn from 'classnames'; import Loader from './loader'; import useDataCheckboxes from '../hooks/useDataCheckboxes'; import withDataLoader, { type WrapperProps } from './data-loader'; import '../styles/components/data-table.scss'; type BasicDatum = Record; export type CommonColumn = { label?: ReactNode; name: string; render: (datum: Datum) => ReactNode; tooltip?: ReactNode; width?: string; }; // Either a column is sortable export interface SortableColumn extends CommonColumn { sortable?: true; sorted?: 'ascend' | 'descend'; } // Or it's not sortable export interface NonSortableColumn extends CommonColumn { sortable?: false | undefined; sorted?: never; } type SharedProps = { /** * An array of objects which specifies attributes about each column of your * data. Each object has label, name and render attributes. */ columns: Array | NonSortableColumn>; /** * Table fixed layout */ // Note sure why it doesn't detect it as used... fixedLayout?: boolean; }; const BLOCK = 'data-table'; type HeadProps = { /** * Optional event handler called when a sortable column header gets clicked * Make sure that it doesn't change unecessarily by wrapping it in useCallback */ onHeaderClick?: (columnName: SortableColumn['name']) => void; }; const DataTableHead = ({ columns, onHeaderClick, checkbox, }: HeadProps & SharedProps & { checkbox: ReactNode }) => ( {checkbox && ( {/* needs a relative wrapper, because the header is sticky */}
{checkbox}
)} {columns.map(({ sorted, name, label, sortable, width }) => ( onHeaderClick?.(name) : undefined} style={width ? { width } : undefined} data-column-name={name} > {typeof label === 'function' ? (label as () => ReactNode)() : label} ))} ); const MemoizedDataTableHead = memo(DataTableHead) as typeof DataTableHead; type CellProp = { column: CommonColumn; datum: Datum; loading?: boolean; fixedLayout?: boolean; firstColumn: boolean; }; const Cell = ({ column, datum, loading, fixedLayout, firstColumn, }: CellProp) => { let rendered: ReactNode; try { rendered = column.render(datum); } catch (error) { /** * We get here only if the renderer fails. If the renderer returns null of * undefined because of a lack of data, then it will no throw and will not * display the loader at all */ /* istanbul ignore next */ if (!loading) { throw error; } else { rendered = firstColumn && ; } } return ( {rendered} ); }; type RowProps = { /** * The data to be displayed */ datum: Datum; loading?: boolean; id: string; selectable: boolean; }; const DataTableRow = ({ datum, loading, columns, selectable, id, fixedLayout, firstColumn, }: RowProps & SharedProps & { firstColumn: boolean }) => { const labelId = useId(); return ( {selectable && (