All checks were successful
continuous-integration/drone/push Build is passing
231 lines
7.0 KiB
TypeScript
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);
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
},
|
|
};
|