import { useState } from 'react'; import * as React from 'react'; import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { range } from 'lodash-es'; import { ChevronDownIcon } from 'lucide-react'; import { cn } from '#utils'; import { DropdownMenu } from '../DropdownMenu/DropdownMenu.tsx'; import { Table } from '../Table/Table.tsx'; import { ClientTablePagination } from './ClientTablePagination.tsx'; /** Coerces the value in a cell to a string for consistant display purposes */ function defaultFormatter(value: unknown): string { if (typeof value === 'string') { return value; } else if (typeof value === 'number') { return value.toFixed(2).toString(); } else if (typeof value === 'undefined') { return 'NA'; } if (value instanceof Date) { return toBasicISOString(value); } return JSON.stringify(value); } export type ClientTableEntry = { [key: string]: unknown }; export type ClientFieldFactory = (entry: T) => string; export type ClientTableColumn = { /** How to determine the values for column */ field: ClientFieldFactory | keyof T; /** Override the default formatter for this field */ formatter?: (value: any) => string; /** The label to be displayed on the header */ label: string; }; export type ClientTableDropdownOptions = { icon?: React.ComponentType, 'ref'>>; label: string; onSelection: (column: ClientTableColumn) => void; }[]; export type ClientTableColumnProps = { column: ClientTableColumn; dropdownOptions?: ClientTableDropdownOptions; }; export type ClientTableProps = { [key: `data-${string}`]: unknown; className?: string; columnDropdownOptions?: ClientTableDropdownOptions; columns: ClientTableColumn[]; data: T[]; entriesPerPage?: number; minRows?: number; noWrap?: boolean; onEntryClick?: (entry: T) => void; }; export const ClientTable = ({ className, columnDropdownOptions, columns, data, entriesPerPage = 10, minRows, noWrap, onEntryClick, ...props }: ClientTableProps) => { const [currentPage, setCurrentPage] = useState(1); const pageCount = Math.max(Math.ceil(data.length / entriesPerPage), 1); const firstEntry = data.length === 0 ? 0 : (currentPage - 1) * entriesPerPage + 1; const lastEntry = Math.min(firstEntry + entriesPerPage - 1, data.length); const currentEntries = data.slice(firstEntry - 1, lastEntry); const nRows = Math.max(currentEntries.length, minRows ?? -1); return (
{columns.map((column, i) => ( {columnDropdownOptions ? ( {column.label} {columnDropdownOptions.map((option) => { const Icon = option.icon; return ( { option.onSelection(column); }} > {Icon && } {option.label} ); })} ) : ( column.label )} ))} {range(nRows).map((i) => { const entry = currentEntries[i]; const onClick = onEntryClick && entry ? () => onEntryClick(entry) : undefined; return ( {columns.map(({ field, formatter }, j) => { let value: unknown; if (!entry) { value = 'NA'; } else if (typeof field === 'function') { value = field(entry); } else { value = entry[field]; } const formattedValue = entry && formatter ? formatter(value) : defaultFormatter(value); return ( {formattedValue} ); })} ); })}
); };