Files
web-ui/src/components/Table.stories.tsx
Beatrice Dellacà f1c7e245aa
All checks were successful
continuous-integration/drone/push Build is passing
update prettier
2026-02-23 14:23:37 +01:00

231 lines
7.0 KiB
TypeScript

import { useMemo, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import type { SortState } from '../types/sort';
import { Chip } from './Chip';
import { Table, type TableHeader } from './Table';
type UserRow = {
id: string;
name: string;
role: 'ADMIN' | 'EDITOR' | 'AUTHOR';
status: 'Active' | 'Pending';
posts: number;
};
const rows: UserRow[] = [
{ id: '1', name: 'Beatrice Rosa', role: 'ADMIN', status: 'Active', posts: 48 },
{ id: '2', name: 'Luca Valli', role: 'EDITOR', status: 'Active', posts: 26 },
{ id: '3', name: 'Marta Bellini', role: 'AUTHOR', status: 'Pending', posts: 4 },
{ id: '4', name: 'Giulia Fontana', role: 'AUTHOR', status: 'Active', posts: 12 },
{ id: '5', name: 'Andrea Pini', role: 'EDITOR', status: 'Pending', posts: 9 },
{ id: '6', name: 'Sofia Denti', role: 'AUTHOR', status: 'Active', posts: 7 },
{ id: '7', name: 'Marco Serra', role: 'AUTHOR', status: 'Active', posts: 18 },
{ id: '8', name: 'Elena Neri', role: 'EDITOR', status: 'Active', posts: 31 },
];
const headers: TableHeader<UserRow>[] = [
{
id: 'name',
label: 'Name',
value: (row) => row.name,
sortable: true,
sortField: 'name',
cellClassName: 'table-cell-primary',
},
{
id: 'role',
label: 'Role',
value: (row) => row.role,
sortable: true,
sortField: 'role',
},
{
id: 'status',
label: 'Status',
value: (row) => (
<Chip variant="outlined" tone={row.status === 'Active' ? 'indigo-700' : 'cyan-700'}>
{row.status}
</Chip>
),
},
{
id: 'posts',
label: 'Posts',
value: (row) => row.posts,
sortable: true,
sortField: 'posts',
},
];
type UsersTableProps = {
data: UserRow[];
isLoading?: boolean;
emptyMessage?: string;
sorting?: SortState | null;
onSortChange?: (field: string) => void;
pagination?: {
page: number;
pageSize: number;
total: number;
totalPages: number;
onPageChange: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
};
};
function UsersTable(props: Readonly<UsersTableProps>) {
return (
<Table<UserRow>
headers={headers}
data={props.data}
rowKey={(row) => row.id}
isLoading={props.isLoading}
emptyMessage={props.emptyMessage}
sorting={props.sorting}
onSortChange={props.onSortChange}
pagination={props.pagination}
/>
);
}
function sortRows(data: UserRow[], sorting: SortState | null): UserRow[] {
if (!sorting) {
return data;
}
const sorted = [...data];
sorted.sort((a, b) => {
const left = a[sorting.field as keyof UserRow];
const right = b[sorting.field as keyof UserRow];
if (left === right) {
return 0;
}
if (typeof left === 'number' && typeof right === 'number') {
return sorting.direction === 'asc' ? left - right : right - left;
}
return sorting.direction === 'asc'
? String(left).localeCompare(String(right))
: String(right).localeCompare(String(left));
});
return sorted;
}
const meta = {
title: 'Components/Table',
component: UsersTable,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Generic data table with loading/empty states, optional sorting controls, and optional pagination footer.',
},
},
},
argTypes: {
data: {
description: 'Rows rendered in the table body.',
control: 'object',
table: { type: { summary: 'UserRow[]' } },
},
isLoading: {
description: 'When true, shows the loading indicator row.',
control: 'boolean',
table: { type: { summary: 'boolean' } },
},
emptyMessage: {
description: 'Message shown when `data` is empty and `isLoading` is false.',
control: 'text',
table: { type: { summary: 'string' } },
},
sorting: {
description: 'Current sort state object. Use `null` for no active sorting.',
control: 'object',
table: { type: { summary: "{ field: string; direction: 'asc' | 'desc' } | null" } },
},
onSortChange: {
description: 'Callback fired when a sortable header is clicked.',
action: 'sort changed',
table: { type: { summary: '(field: string) => void' } },
},
pagination: {
description: 'Pagination config object. When omitted, pagination footer is hidden.',
control: 'object',
table: {
type: {
summary:
'{ page; pageSize; total; totalPages; onPageChange; onPageSizeChange? }',
},
},
},
},
args: {
data: rows,
},
} satisfies Meta<typeof UsersTable>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithRows: Story = {};
export const Loading: Story = {
args: {
isLoading: true,
},
};
export const Empty: Story = {
args: {
data: [],
emptyMessage: 'No users found',
},
};
export const InteractiveSortingAndPagination: Story = {
render: () => {
const [sorting, setSorting] = useState<SortState | null>({
field: 'name',
direction: 'asc',
});
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(5);
const sorted = useMemo(() => sortRows(rows, sorting), [sorting]);
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
const safePage = Math.min(page, totalPages);
const start = (safePage - 1) * pageSize;
const pagedRows = sorted.slice(start, start + pageSize);
return (
<UsersTable
data={pagedRows}
sorting={sorting}
onSortChange={(field) => {
setPage(1);
setSorting((prev) => {
if (!prev || prev.field !== field) {
return { field, direction: 'asc' };
}
if (prev.direction === 'asc') {
return { field, direction: 'desc' };
}
return null;
});
}}
pagination={{
page: safePage,
pageSize,
total: sorted.length,
totalPages,
onPageChange: setPage,
onPageSizeChange: (next) => {
setPage(1);
setPageSize(next);
},
}}
/>
);
},
};