import { useMemo, useState } from 'react'; import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { faker } from '@faker-js/faker'; import type { Meta, StoryObj } from '@storybook/react-vite'; import type { ColumnDef, PaginationState, SortingState } from '@tanstack/table-core'; import { range } from 'lodash-es'; import { ChevronDownIcon } from 'lucide-react'; import { Button } from '../Button/Button.tsx'; import { DropdownMenu } from '../DropdownMenu/DropdownMenu.tsx'; import { DataTable } from './DataTable.tsx'; import { useDataTableHandle } from './hooks.ts'; type PaymentStatus = 'failed' | 'pending' | 'processing' | 'success'; type Payment = { amount: number; date: Date; email: string; id: string; status: PaymentStatus; }; type Story = StoryObj>; const columns: ColumnDef[] = [ { accessorKey: 'status', enableSorting: false, filterFn: (row, id, filter: PaymentStatus[]) => { return filter.includes(row.getValue(id)); }, header: 'Status' }, { accessorKey: 'email', header: 'Email' }, { accessorKey: 'amount', header: 'Amount' } ]; const statuses: readonly PaymentStatus[] = Object.freeze(['failed', 'pending', 'processing', 'success']); const createData = (n: number): Payment[] => { return range(n).map((i) => ({ amount: faker.number.int({ max: 100, min: 0 }), date: faker.date.recent(), email: faker.internet.email(), id: String(i + 1), status: faker.helpers.arrayElement(statuses) })); }; const Toggles = () => { const table = useDataTableHandle('table', true); const columns = table.getAllColumns(); const statusColumn = columns.find((column) => column.id === 'status')!; const filterValue = statusColumn.getFilterValue() as PaymentStatus[]; return ( <> {columns .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility(!!value)} > {column.id} ); })} {statuses.map((option) => ( { statusColumn.setFilterValue((prevValue: PaymentStatus[]) => { if (checked) { return [...prevValue, option]; } return prevValue.filter((item) => item !== option); }); }} > {option} ))} ); }; export default { component: DataTable } as Meta; export const Default: Story = { decorators: [ (Story) => { const [tableData, setTableData] = useState(createData(100)); return (
); } ] }; export const WithActions: Story = { args: { columns: [ ...columns, { accessorKey: 'notes', header: 'Notes' } ], data: createData(100).map((payment) => ({ ...payment, notes: faker.lorem.paragraph() })), onRowDoubleClick(row) { alert(`row with email ${row.email} double clicked`); }, onSearchChange: () => { return; }, rowActions: [ { label: 'Modify', onSelect: () => { alert('Modify'); } }, { destructive: true, label: 'Delete', onSelect: () => { alert('Delete'); } } ], tableName: 'action-table' } }; export const WithToggles: Story = { args: { columns, data: createData(100), initialState: { columnFilters: [ { id: 'status', value: [...statuses] } ] }, onSearchChange: () => { return; }, togglesComponent: Toggles } }; export const Empty: Story = { args: { columns, data: [], onSearchChange: () => { return; } } }; export const Grouped: Story = { args: { columns: [ { accessorFn: (row) => toBasicISOString(row.date), header: 'Date', id: 'date' }, { columns: [ { accessorKey: 'id', header: 'ID' }, { accessorKey: 'status', header: 'Status' } ], enableResizing: false, header: 'Internal', meta: { centered: true } }, { columns: [ { accessorKey: 'email', header: 'Email' }, { accessorKey: 'amount', header: 'Amount' } ], enableResizing: false, header: 'Details', meta: { centered: true } } ], data: createData(100), initialState: { columnPinning: { left: ['date'] } }, onRowDoubleClick(row) { alert(`row with ID ${row.id} double clicked`); }, onSearchChange: () => { return; }, rowActions: [ { label: 'Modify', onSelect: () => { alert('Modify'); } }, { destructive: true, label: 'Delete', onSelect: () => { alert('Delete'); } } ] } }; export const RandomColumns: StoryObj> = { decorators: [ (Story) => { const generate = () => { const columnCount = faker.number.int({ max: 5, min: 3 }); const generators = [ { gen: () => faker.person.fullName(), type: 'name' }, { gen: () => faker.internet.email(), type: 'email' }, { gen: () => faker.location.city(), type: 'city' }, { gen: () => faker.company.name(), type: 'company' }, { gen: () => faker.number.int({ max: 1000, min: 0 }), type: 'amount' }, { gen: () => faker.commerce.product(), type: 'product' }, { gen: () => faker.color.human(), type: 'color' } ]; const picked = faker.helpers.arrayElements(generators, columnCount); const randomColumns: ColumnDef<{ [key: string]: unknown }>[] = picked.map(({ type }) => ({ accessorKey: type, header: type.charAt(0).toUpperCase() + type.slice(1) })); const randomData = range(50).map(() => { const row: { [key: string]: unknown } = {}; for (const { gen, type } of picked) { row[type] = gen(); } return row; }); return { randomColumns, randomData }; }; const [state, setState] = useState(generate); return (
); } ] }; export const Server: Story = { decorators: [ (Story) => { const allServerData = useMemo(() => { return range(100).map((i) => ({ amount: faker.number.int({ max: 100, min: 0 }), date: faker.date.recent(), email: faker.internet.email(), id: String(i + 1), status: faker.helpers.arrayElement(statuses) })); }, []); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); const [sorting, setSorting] = useState([]); const sortedData = useMemo(() => { if (!sorting.length) { return allServerData; } const { desc, id } = sorting[0]!; return [...allServerData].sort((a, b) => { const aVal = a[id as keyof Payment]; const bVal = b[id as keyof Payment]; if (aVal < bVal) { return desc ? 1 : -1; } else if (aVal > bVal) { return desc ? -1 : 1; } return 0; }); }, [allServerData, sorting]); const pageData = sortedData.slice( pagination.pageIndex * pagination.pageSize, (pagination.pageIndex + 1) * pagination.pageSize ); return ( ); } ] };