This commit is contained in:
David Arranz 2024-06-13 13:09:26 +02:00
parent e6498b4104
commit f39dbe95cc
16 changed files with 364 additions and 537 deletions

6
.vscode/launch.json vendored
View File

@ -10,6 +10,12 @@
"webRoot": "${workspaceFolder}/client" "webRoot": "${workspaceFolder}/client"
}, },
{
"name": "Launch Chrome localhost",
"type": "pwa-chrome",
"port": 9222
},
{ {
"type": "msedge", "type": "msedge",
"request": "launch", "request": "launch",

View File

@ -1,22 +1,59 @@
import { Card, CardContent } from "@/ui";
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { DataTable } from "@/components"; import { DataTable } from "@/components";
import { useDataTable } from "@/lib/hooks"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts"; import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
import { ColumnDef, Row } from "@tanstack/react-table";
import { t } from "i18next";
import { useMemo } from "react"; import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useCatalogList } from "../hooks";
type CatalogTableViewProps = { export const CatalogDataTable = () => {
data: IListResponse_DTO<IListArticles_Response_DTO>; const navigate = useNavigate();
}; const { pagination, globalFilter, isFiltered } = useDataTableContext();
console.log("pagination PADRE => ", pagination);
export const CatalogDataTable = ({ data }: CatalogTableViewProps) => { const { data, isPending, isError, error } = useCatalogList({
const columns = useMemo( pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
searchTerm: globalFilter,
});
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
() => [ () => [
{ {
id: "description" as const, id: "description" as const,
accessorKey: "description", accessorKey: "description",
size: 400, header: () => <>{t("catalog.list.columns.description")}</>,
enableHiding: false,
enableSorting: false,
enableResizing: false, enableResizing: false,
size: 300,
},
{
id: "points" as const,
accessorKey: "points",
header: () => <div className='text-right'>{t("catalog.list.columns.points")}</div>,
cell: ({ renderValue }: { renderValue: () => any }) => (
<div className='text-right'>{renderValue()}</div>
),
enableResizing: false,
size: 20,
},
{
id: "retail_price" as const,
accessorKey: "retail_price",
header: () => <div className='text-right'>{t("catalog.list.columns.retail_price")}</div>,
cell: ({ row }: { row: Row<any> }) => {
const price = MoneyValue.create(row.original.retail_price).object;
return <div className='text-right'>{price.toFormat()}</div>;
},
enableResizing: false,
size: 20,
}, },
], ],
[] []
@ -28,9 +65,41 @@ export const CatalogDataTable = ({ data }: CatalogTableViewProps) => {
pageCount: data?.total_pages ?? -1, pageCount: data?.total_pages ?? -1,
}); });
if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />;
}
if (isPending) {
return (
<Card>
<CardContent>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
</CardContent>
</Card>
);
}
if (data?.total_items === 0 && !isFiltered) {
return (
<SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo'
buttonText=''
onButtonClick={() => navigate("/catalog/add")}
/>
);
}
return ( return (
<> <>
<DataTable table={table} paginationOptions={{ visible: true }} /> <DataTable table={table} paginationOptions={{ visible: true }}>
<DataTableToolbar table={table} />
</DataTable>
</> </>
); );
}; };

View File

@ -1,6 +1,6 @@
import { Layout, LayoutContent, LayoutHeader } from "@/components"; import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { DataTableProvider } from "@/lib/hooks";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import { Trans } from "react-i18next";
import { CatalogProvider } from "./CatalogContext"; import { CatalogProvider } from "./CatalogContext";
export const CatalogLayout = ({ children }: PropsWithChildren) => { export const CatalogLayout = ({ children }: PropsWithChildren) => {
@ -9,8 +9,14 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
<Layout> <Layout>
<LayoutHeader /> <LayoutHeader />
<LayoutContent> <LayoutContent>
<DataTableProvider>{children}</DataTableProvider> <div className='flex items-center'>
<h1 className='text-lg font-semibold md:text-2xl'>
<Trans i18nKey='catalog.title' />
</h1>
</div>
{children}
</LayoutContent> </LayoutContent>
1
</Layout> </Layout>
</CatalogProvider> </CatalogProvider>
); );

View File

@ -28,108 +28,17 @@ import {
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react"; import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; import { DataTableProvider } from "@/lib/hooks";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { useMemo } from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CatalogDataTable } from "./components"; import { CatalogDataTable } from "./components";
import { useCatalogList } from "./hooks/useCatalogList";
export const CatalogList = () => { export const CatalogList = () => {
const navigate = useNavigate();
const { pagination } = useDataTableContext();
console.log("pagination PADRE => ", pagination);
const { data, isPending, isError, error, refetch } = useCatalogList({
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
});
const columns = useMemo(
() => [
{
id: "description" as const,
accessorKey: "description",
size: 400,
enableHiding: false,
enableSorting: false,
enableResizing: false,
},
],
[]
);
const { table } = useDataTable({
data: data?.items ?? [],
columns: columns,
pageCount: data?.total_pages ?? -1,
});
return <DataTable table={table} paginationOptions={{ visible: true }} />;
if (isError || isPending) {
return <></>;
}
return ( return (
<> <DataTableProvider>
<Button <CatalogDataTable />
onClick={() => { </DataTableProvider>
setPagination({
pageIndex: pagination.pageIndex + 1,
});
}}
>
+ Página
</Button>
{data.items.map((row) => (
<p>{row.description}</p>
))}
<CatalogDataTable data={data} />;
</>
); );
if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />;
}
if (isPending) {
return (
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>
<Trans i18nKey='catalog.title' />
</CardTitle>
<CardDescription>Manage your products and view their sales performance.</CardDescription>
</CardHeader>
<CardContent>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
</CardContent>
</Card>
);
}
if (data?.total_items === 0) {
return (
<SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo'
buttonText=''
onButtonClick={() => navigate("/catalog/add")}
/>
);
}
return ( return (
<> <>
<Tabs defaultValue='all'> <Tabs defaultValue='all'>

View File

@ -9,12 +9,12 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/ui/table"; } from "@/ui/table";
import { ReactNode } from "react"; import { PropsWithChildren, ReactNode } from "react";
import { Card, CardContent, CardFooter, CardHeader } from "@/ui"; import { cn } from "@/lib/utils";
import { Card, CardContent, CardDescription, CardFooter, CardHeader } from "@/ui";
import { DataTableColumnHeader } from "./DataTableColumnHeader"; import { DataTableColumnHeader } from "./DataTableColumnHeader";
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination"; import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
import { DataTableToolbar } from "./DataTableToolbar";
export type DataTableColumnProps<TData, TValue> = ColumnDef<TData, TValue>; export type DataTableColumnProps<TData, TValue> = ColumnDef<TData, TValue>;
@ -23,25 +23,31 @@ export type DataTablePaginationOptionsProps<TData> = Pick<
"visible" "visible"
>; >;
export type DataTableProps<TData> = { export type DataTableProps<TData> = PropsWithChildren<{
table: ReactTable<TData>; table: ReactTable<TData>;
caption?: ReactNode; caption?: ReactNode;
className?: string;
paginationOptions?: DataTablePaginationOptionsProps<TData>; paginationOptions?: DataTablePaginationOptionsProps<TData>;
}; className?: string;
}>;
export function DataTable<TData>({ table, caption, paginationOptions }: DataTableProps<TData>) { export function DataTable<TData>({
table,
caption,
paginationOptions,
children,
className,
...props
}: DataTableProps<TData>) {
return ( return (
<> <>
<DataTableToolbar table={table} />
<Card> <Card>
<CardHeader> <CardHeader className='pb-0'>
<DataTablePagination <CardDescription
className='flex-1' className={cn("w-full space-y-2.5 overflow-auto mt-7", className)}
visible={paginationOptions?.visible} {...props}
table={table} >
/> {children}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className='pt-6'> <CardContent className='pt-6'>
<Table> <Table>

View File

@ -10,15 +10,10 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
Separator, Separator,
} from "@/ui"; } from "@/ui";
import { import { t } from "i18next";
ArrowDownIcon, import { ArrowDownIcon, ArrowDownUpIcon, ArrowUpIcon, EyeOffIcon } from "lucide-react";
ArrowDownUpIcon,
ArrowUpIcon,
EyeOffIcon,
} from "lucide-react";
interface DataTableColumnHeaderProps<TData, TValue> interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
extends React.HTMLAttributes<HTMLDivElement> {
table: Table<TData>; table: Table<TData>;
header: Header<TData, TValue>; header: Header<TData, TValue>;
} }
@ -34,20 +29,21 @@ export function DataTableColumnHeader<TData, TValue>({
<div <div
className={cn( className={cn(
"data-[state=open]:bg-accent font-semiboldw text-muted-foreground uppercase", "data-[state=open]:bg-accent font-semiboldw text-muted-foreground uppercase",
className, className
)} )}
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(header.column.columnDef.header, header.getContext())}
</div> </div>
{header.column.getCanResize() && ( {header.column.getCanResize() && (
<Separator <Separator
orientation="vertical" orientation='vertical'
className={cn( className={cn(
"absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize", "absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize",
table.options.columnResizeDirection, table.options.columnResizeDirection,
header.column.getIsResizing() ? "bg-primary opacity-100" : "", header.column.getIsResizing() ? "bg-primary opacity-100" : ""
)} )}
{...{ {...{
onDoubleClick: () => header.column.resetSize(), onDoubleClick: () => header.column.resetSize(),
@ -55,12 +51,9 @@ export function DataTableColumnHeader<TData, TValue>({
onTouchStart: header.getResizeHandler(), onTouchStart: header.getResizeHandler(),
style: { style: {
transform: transform:
table.options.columnResizeMode === "onEnd" && table.options.columnResizeMode === "onEnd" && header.column.getIsResizing()
header.column.getIsResizing()
? `translateX(${ ? `translateX(${
(table.options.columnResizeDirection === "rtl" (table.options.columnResizeDirection === "rtl" ? -1 : 1) *
? -1
: 1) *
(table.getState().columnSizingInfo.deltaOffset ?? 0) (table.getState().columnSizingInfo.deltaOffset ?? 0)
}px)` }px)`
: "", : "",
@ -79,41 +72,67 @@ export function DataTableColumnHeader<TData, TValue>({
<Button <Button
aria-label={ aria-label={
header.column.getIsSorted() === "desc" header.column.getIsSorted() === "desc"
? "En orden descendente. Click para ordenar ascendentemente." ? t("common.sort_desc_description")
: header.column.getIsSorted() === "asc" : header.column.getIsSorted() === "asc"
? "En order ascendente. Click para ordenar descendentemente." ? t("common.sort_asc_description")
: "Sin orden. Click para ordenar ascendentemente." : t("sort_none_description")
} }
size="sm" size='sm'
variant="ghost" variant='ghost'
className="-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground" className='-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground'
> >
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === "desc" ? ( {header.column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="w-4 h-4 ml-2" /> <ArrowDownIcon className='w-4 h-4 ml-2' aria-hidden='true' />
) : header.column.getIsSorted() === "asc" ? ( ) : header.column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="w-4 h-4 ml-2" /> <ArrowUpIcon className='w-4 h-4 ml-2' aria-hidden='true' />
) : ( ) : (
<ArrowDownUpIcon className="w-4 h-4 ml-2 text-muted-foreground/30" /> <ArrowDownUpIcon
className='w-4 h-4 ml-2 text-muted-foreground/30'
aria-hidden='true'
/>
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start"> <DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => header.column.toggleSorting(false)}> {header.column.getCanSort() && (
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> <>
Ascendente <DropdownMenuItem
</DropdownMenuItem> onClick={() => header.column.toggleSorting(false)}
<DropdownMenuItem onClick={() => header.column.toggleSorting(true)}> aria-label={t("common.sort_asc")}
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> >
Descendente <ArrowUpIcon
</DropdownMenuItem> className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
<DropdownMenuSeparator /> aria-hidden='true'
/>
{t("common.sort_asc")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => header.column.toggleSorting(true)}
aria-label={t("common.sort_desc")}
>
<ArrowDownIcon
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true'
/>
{t("common.sort_desc")}
</DropdownMenuItem>
</>
)}
{header.column.getCanSort() && header.column.getCanHide() && <DropdownMenuSeparator />}
{header.column.getCanHide() && ( {header.column.getCanHide() && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => header.column.toggleVisibility(false)} onClick={() => header.column.toggleVisibility(false)}
aria-label={t("Hide")}
> >
<EyeOffIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> <EyeOffIcon
Ocultar className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true'
/>
{t("Hide")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -2,6 +2,7 @@ import { DEFAULT_PAGE_SIZES, INITIAL_PAGE_INDEX } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui"; import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
import { Table } from "@tanstack/react-table"; import { Table } from "@tanstack/react-table";
import { t } from "i18next";
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
@ -31,12 +32,15 @@ export function DataTablePagination<TData>({
return ( return (
<div className={cn("flex items-center justify-between px-2", className)}> <div className={cn("flex items-center justify-between px-2", className)}>
<div className='flex-1 text-base text-muted-foreground'> <div className='flex-1 text-base text-muted-foreground'>
{table.getFilteredSelectedRowModel().rows.length} de{" "} {t("common.rows_selected", {
{table.getFilteredRowModel().rows.length} filas(s) seleccionadas. count: table.getFilteredSelectedRowModel().rows.length,
total: table.getFilteredRowModel().rows.length,
})}
</div> </div>
<div className='flex items-center space-x-6 lg:space-x-8'> <div className='flex items-center space-x-6 lg:space-x-8'>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<p className='text-base font-medium'>Filas por página</p> <p className='text-base font-medium'>{t("common.rows_per_page")}</p>
<Select <Select
value={`${table.getState().pagination.pageSize}`} value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => { onValueChange={(value) => {
@ -56,7 +60,10 @@ export function DataTablePagination<TData>({
</Select> </Select>
</div> </div>
<div className='flex w-[100px] items-center justify-center text-base font-medium'> <div className='flex w-[100px] items-center justify-center text-base font-medium'>
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()} {t("common.num_page_of_total", {
count: table.getState().pagination.pageIndex + 1,
total: table.getPageCount(),
})}
</div> </div>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Button <Button
@ -65,7 +72,7 @@ export function DataTablePagination<TData>({
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)} onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<span className='sr-only'>Ir a la primera página</span> <span className='sr-only'>{t("common.go_to_first_page")}</span>
<ChevronsLeftIcon className='w-4 h-4' /> <ChevronsLeftIcon className='w-4 h-4' />
</Button> </Button>
<Button <Button
@ -74,7 +81,7 @@ export function DataTablePagination<TData>({
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<span className='sr-only'>Ir a la página anterior</span> <span className='sr-only'>{t("common.go_to_prev_page")}</span>
<ChevronLeftIcon className='w-4 h-4' /> <ChevronLeftIcon className='w-4 h-4' />
</Button> </Button>
<Button <Button
@ -83,7 +90,7 @@ export function DataTablePagination<TData>({
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<span className='sr-only'>Ir a la página siguiente</span> <span className='sr-only'>{t("common.go_to_next_page")}</span>
<ChevronRightIcon className='w-4 h-4' /> <ChevronRightIcon className='w-4 h-4' />
</Button> </Button>
<Button <Button
@ -92,7 +99,7 @@ export function DataTablePagination<TData>({
onClick={() => table.setPageIndex(table.getPageCount() + 1)} onClick={() => table.setPageIndex(table.getPageCount() + 1)}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<span className='sr-only'>Ir a la última página</span> <span className='sr-only'>{t("common.go_to_last_page")}</span>
<ChevronsRightIcon className='w-4 h-4' /> <ChevronsRightIcon className='w-4 h-4' />
</Button> </Button>
</div> </div>

View File

@ -1,12 +1,5 @@
import { import { cn } from "@/lib/utils";
Skeleton, import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/ui";
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/ui";
interface DataTableSkeletonProps { interface DataTableSkeletonProps {
/** /**
@ -53,11 +46,20 @@ interface DataTableSkeletonProps {
cellWidths?: string[]; cellWidths?: string[];
/** /**
* Flag to prevent the table from shrinking to fit the content. * Flag to show the pagination bar.
* @default true
* @type boolean | undefined
*/
withPagination?: boolean;
/**
* Flag to prevent the table cells from shrinking.
* @default false * @default false
* @type boolean | undefined * @type boolean | undefined
*/ */
shrinkZero?: boolean; shrinkZero?: boolean;
className?: string;
} }
export function DataTableSkeleton({ export function DataTableSkeleton({
@ -67,32 +69,33 @@ export function DataTableSkeleton({
filterableColumnCount = 0, filterableColumnCount = 0,
showViewOptions = true, showViewOptions = true,
cellWidths = ["auto"], cellWidths = ["auto"],
withPagination = true,
shrinkZero = false, shrinkZero = false,
className,
...skeletonProps
}: DataTableSkeletonProps) { }: DataTableSkeletonProps) {
return ( return (
<div className="w-full space-y-3 overflow-auto"> <div className={cn("w-full space-y-2.5 overflow-auto", className)} {...skeletonProps}>
<div className="flex items-center justify-between w-full p-1 space-x-2 overflow-auto"> <div className='flex items-center justify-between w-full p-1 space-x-2 overflow-auto'>
<div className="flex items-center flex-1 space-x-2"> <div className='flex items-center flex-1 space-x-2'>
{searchableColumnCount > 0 {searchableColumnCount > 0
? Array.from({ length: searchableColumnCount }).map((_, i) => ( ? Array.from({ length: searchableColumnCount }).map((_, i) => (
<Skeleton key={i} className="w-40 h-7 lg:w-60" /> <Skeleton key={i} className='w-40 h-7 lg:w-60' />
)) ))
: null} : null}
{filterableColumnCount > 0 {filterableColumnCount > 0
? Array.from({ length: filterableColumnCount }).map((_, i) => ( ? Array.from({ length: filterableColumnCount }).map((_, i) => (
<Skeleton key={i} className="h-7 w-[4.5rem] border-dashed" /> <Skeleton key={i} className='h-7 w-[4.5rem] border-dashed' />
)) ))
: null} : null}
</div> </div>
{showViewOptions ? ( {showViewOptions ? <Skeleton className='ml-auto hidden h-7 w-[4.5rem] lg:flex' /> : null}
<Skeleton className="ml-auto hidden h-7 w-[4.5rem] lg:flex" />
) : null}
</div> </div>
<div className="border rounded-md"> <div className='border rounded-md'>
<Table> <Table>
<TableHeader> <TableHeader>
{Array.from({ length: 1 }).map((_, i) => ( {Array.from({ length: 1 }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent"> <TableRow key={i} className='hover:bg-transparent'>
{Array.from({ length: columnCount }).map((_, j) => ( {Array.from({ length: columnCount }).map((_, j) => (
<TableHead <TableHead
key={j} key={j}
@ -101,7 +104,7 @@ export function DataTableSkeleton({
minWidth: shrinkZero ? cellWidths[j] : "auto", minWidth: shrinkZero ? cellWidths[j] : "auto",
}} }}
> >
<Skeleton className="w-full h-6" /> <Skeleton className='w-full h-6' />
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@ -109,7 +112,7 @@ export function DataTableSkeleton({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{Array.from({ length: rowCount }).map((_, i) => ( {Array.from({ length: rowCount }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent"> <TableRow key={i} className='hover:bg-transparent'>
{Array.from({ length: columnCount }).map((_, j) => ( {Array.from({ length: columnCount }).map((_, j) => (
<TableCell <TableCell
key={j} key={j}
@ -118,7 +121,7 @@ export function DataTableSkeleton({
minWidth: shrinkZero ? cellWidths[j] : "auto", minWidth: shrinkZero ? cellWidths[j] : "auto",
}} }}
> >
<Skeleton className="w-full h-6" /> <Skeleton className='w-full h-6' />
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
@ -126,24 +129,26 @@ export function DataTableSkeleton({
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<div className="flex flex-col-reverse items-center justify-between w-full gap-4 px-2 py-1 overflow-auto sm:flex-row sm:gap-8"> {withPagination ? (
<Skeleton className="w-40 h-8" /> <div className='flex items-center justify-between w-full gap-4 p-1 overflow-auto sm:gap-8'>
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8"> <Skeleton className='w-40 h-7 shrink-0' />
<div className="flex items-center space-x-2"> <div className='flex items-center gap-4 sm:gap-6 lg:gap-8'>
<Skeleton className="w-24 h-8" /> <div className='flex items-center space-x-2'>
<Skeleton className="h-8 w-[4.5rem]" /> <Skeleton className='w-24 h-7' />
</div> <Skeleton className='h-7 w-[4.5rem]' />
<div className="flex items-center justify-center text-sm font-medium"> </div>
<Skeleton className="w-20 h-8" /> <div className='flex items-center justify-center text-sm font-medium'>
</div> <Skeleton className='w-20 h-7' />
<div className="flex items-center space-x-2"> </div>
<Skeleton className="hidden size-8 lg:block" /> <div className='flex items-center space-x-2'>
<Skeleton className="size-8" /> <Skeleton className='hidden size-7 lg:block' />
<Skeleton className="size-8" /> <Skeleton className='size-7' />
<Skeleton className="hidden size-8 lg:block" /> <Skeleton className='size-7' />
<Skeleton className='hidden size-7 lg:block' />
</div>
</div> </div>
</div> </div>
</div> ) : null}
</div> </div>
); );
} }

View File

@ -1,138 +1,54 @@
import { Table } from "@tanstack/react-table"; import { Table } from "@tanstack/react-table";
//import { priorities, statuses } from './Data' //import { priorities, statuses } from './Data'
import { DataTableFilterField } from "@/lib/types"; import { DataTableFilterField, useDataTableContext } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button, Input } from "@/ui"; import { Button, Input } from "@/ui";
import { CrossIcon } from "lucide-react"; import { t } from "i18next";
import { useMemo } from "react"; import { XIcon } from "lucide-react";
import { DataTableFacetedFilter } from "../DataTableFacetedFilter";
import { DataTableColumnOptions } from "./DataTableColumnOptions"; import { DataTableColumnOptions } from "./DataTableColumnOptions";
interface DataTableToolbarProps<TData> interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
extends React.HTMLAttributes<HTMLDivElement> {
table: Table<TData>; table: Table<TData>;
filterFields?: DataTableFilterField<TData>[]; filterFields?: DataTableFilterField<TData>[];
} }
export function DataTableToolbar<TData>({ export function DataTableToolbar<TData>({
table, table,
filterFields = [],
children,
className, className,
children,
...props ...props
}: DataTableToolbarProps<TData>) { }: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0; const { globalFilter, isFiltered, setGlobalFilter, resetGlobalFilter } = useDataTableContext();
// Memoize computation of searchableColumns and filterableColumns
const { searchableColumns, filterableColumns } = useMemo(() => {
return {
searchableColumns: filterFields.filter((field) => !field.options),
filterableColumns: filterFields.filter((field) => field.options),
};
}, [filterFields]);
return ( return (
<div <div
className={cn( className={cn(
"flex w-full items-center justify-between space-x-2 overflow-auto p-1", "flex w-full items-center justify-between space-x-2 overflow-auto p-1",
className, className
)} )}
{...props} {...props}
> >
<div className="flex items-center flex-1 space-x-2"> <div className='flex items-center flex-1 space-x-2'>
{searchableColumns.length > 0 && <Input
searchableColumns.map( key='global-filter'
(column) => placeholder={t("catalog.list.global_filter_placeholder")}
table.getColumn(column.value ? String(column.value) : "") && ( value={globalFilter}
<Input onChange={(event) => setGlobalFilter(String(event.target.value))}
key={String(column.value)} className='w-3/12 h-8 lg:w-6/12'
placeholder={column.placeholder} />
value={
(table
.getColumn(String(column.value))
?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table
.getColumn(String(column.value))
?.setFilterValue(event.target.value)
}
className="w-40 h-8 lg:w-64"
/>
),
)}
{filterableColumns.length > 0 &&
filterableColumns.map(
(column) =>
table.getColumn(column.value ? String(column.value) : "") && (
<DataTableFacetedFilter
key={String(column.value)}
column={table.getColumn(
column.value ? String(column.value) : "",
)}
title={column.label}
options={column.options ?? []}
/>
),
)}
{isFiltered && ( {isFiltered && (
<Button <Button variant='ghost' onClick={() => resetGlobalFilter()} className='h-8 px-2 lg:px-3'>
variant="ghost" {t("common.reset_filter")}
onClick={() => table.resetColumnFilters()} <XIcon className='w-4 h-4 ml-2' />
className="h-8 px-2 lg:px-3"
>
Reset filters
<CrossIcon className="w-4 h-4 ml-2" />
</Button> </Button>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className='flex items-center gap-2'>
{children} {children}
{table.options.enableHiding && <DataTableColumnOptions table={table} />} {table.options.enableHiding && <DataTableColumnOptions table={table} />}
</div> </div>
</div> </div>
); );
/*
return (
<div className="flex items-center justify-between">
<div className="flex items-center flex-1 space-x-2">
<Input
placeholder="Filter tasks..."
value={(table.getColumn("customer")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("customer")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
{table.getColumn("status") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statuses}
/>
)}
{table.getColumn("priority") && (
<DataTableFacetedFilter
column={table.getColumn("priority")}
title="Priority"
options={priorities}
/>
)}
{isFiltered && (
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3"
>
Reset
<CrossIcon className="w-4 h-4 ml-2" />
</Button>
)}
</div>
<DataTableViewOptions table={table} />
</div>
)
*/
} }

View File

@ -1,7 +1,7 @@
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
export const Layout = ({ children }: PropsWithChildren) => { export const Layout = ({ children }: PropsWithChildren) => (
return <div className='flex flex-col w-full min-h-screen'>{children}</div>; <div className='flex flex-col w-full min-h-screen'>{children}</div>
}; );
Layout.displayName = "Layout"; Layout.displayName = "Layout";

View File

@ -1,18 +1,44 @@
import { usePaginationParams } from "@/lib/hooks"; import { PaginationState, usePaginationParams } from "@/lib/hooks";
import { PropsWithChildren, createContext } from "react"; import { SortingState } from "@tanstack/react-table";
import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react";
export interface IDataTableContextState {} export interface IDataTableContextState {
pagination: PaginationState;
setPagination: (newPagination: PaginationState) => void;
sorting: [];
setSorting: () => void;
globalFilter: string;
setGlobalFilter: (newGlobalFilter: string) => void;
resetGlobalFilter: () => void;
isFiltered: boolean;
}
export const DataTableContext = createContext<IDataTableContextState | null>(null); export const DataTableContext = createContext<IDataTableContextState | null>(null);
export const DataTableProvider = ({ children }: PropsWithChildren) => { export const DataTableProvider = ({
initialGlobalFilter = "",
children,
}: PropsWithChildren<{
initialGlobalFilter?: string;
}>) => {
const [pagination, setPagination] = usePaginationParams(); const [pagination, setPagination] = usePaginationParams();
const [globalFilter, setGlobalFilter] = useState<string>(initialGlobalFilter);
const [sorting, setSorting] = useState<SortingState>([]);
const isFiltered = useMemo(() => Boolean(globalFilter.length), [globalFilter]);
const resetGlobalFilter = useCallback(() => setGlobalFilter(""), []);
return ( return (
<DataTableContext.Provider <DataTableContext.Provider
value={{ value={{
pagination, pagination,
setPagination, setPagination,
sorting,
setSorting,
globalFilter,
setGlobalFilter,
resetGlobalFilter,
isFiltered,
}} }}
> >
{children} {children}

View File

@ -1,4 +1,5 @@
export * from "./DataTableContext"; export * from "./DataTableContext";
export * from "./types";
export * from "./useDataTable"; export * from "./useDataTable";
export * from "./useDataTableColumns"; export * from "./useDataTableColumns";
export * from "./useDataTableContext"; export * from "./useDataTableContext";

View File

@ -8,14 +8,12 @@ import {
getSortedRowModel, getSortedRowModel,
useReactTable, useReactTable,
type ColumnDef, type ColumnDef,
type ColumnFiltersState,
type SortingState, type SortingState,
type VisibilityState, type VisibilityState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { getDataTableSelectionColumn } from "@/components"; import { getDataTableSelectionColumn } from "@/components";
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback } from "react";
import { useSearchParams } from "react-router-dom";
import { DataTableFilterField } from "./types"; import { DataTableFilterField } from "./types";
import { useDataTableContext } from "./useDataTableContext"; import { useDataTableContext } from "./useDataTableContext";
@ -63,6 +61,22 @@ interface UseDataTableProps<TData, TValue> {
*/ */
enableRowSelection?: boolean; enableRowSelection?: boolean;
/**
* The default number of rows per page.
* @default 10
* @type number | undefined
* @example 20
*/
defaultPerPage?: number;
/**
* The default sort order.
* @default undefined
* @type `${Extract<keyof TData, string | number>}.${"asc" | "desc"}` | undefined
* @example "createdAt.desc"
*/
defaultSort?: `${Extract<keyof TData, string | number>}.${"asc" | "desc"}`;
/** /**
* Defines filter fields for the table. Supports both dynamic faceted filters and search filters. * Defines filter fields for the table. Supports both dynamic faceted filters and search filters.
* - Faceted filters are rendered when `options` are provided for a filter field. * - Faceted filters are rendered when `options` are provided for a filter field.
@ -110,78 +124,13 @@ export function useDataTable<TData, TValue>({
enableSorting = false, enableSorting = false,
enableHiding = false, enableHiding = false,
enableRowSelection = false, enableRowSelection = false,
onPaginationChange,
filterFields = [],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
enableAdvancedFilter = false,
}: UseDataTableProps<TData, TValue>) { }: UseDataTableProps<TData, TValue>) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { pagination, setPagination, sorting, setSorting } = useDataTableContext();
const [searchParams, setSearchParams] = useSearchParams();
const { pagination, setPagination } = useDataTableContext();
console.log("pagination TABLA =>", pagination);
const [sorting, setSorting] = useState<SortingState>([]);
// Memoize computation of searchableColumns and filterableColumns
const { searchableColumns, filterableColumns } = useMemo(() => {
return {
searchableColumns: filterFields.filter((field) => !field.options),
filterableColumns: filterFields.filter((field) => field.options),
};
}, [filterFields]);
// Create query string
/*const createQueryString = useCallback(
(params: Record<string, string | number | null>) => {
const newSearchParams = new URLSearchParams(searchParams?.toString());
for (const [key, value] of Object.entries(params)) {
if (value === null) {
newSearchParams.delete(key);
} else {
newSearchParams.set(key, String(value));
}
}
return newSearchParams.toString();
},
[searchParams]
);*/
// Initial column filters
const initialColumnFilters: ColumnFiltersState = useMemo(() => {
return Array.from(searchParams.entries()).reduce<ColumnFiltersState>(
(filters, [key, value]) => {
const filterableColumn = filterableColumns.find((column) => column.value === key);
const searchableColumn = searchableColumns.find((column) => column.value === key);
if (filterableColumn) {
filters.push({
id: key,
value: value.split("."),
});
} else if (searchableColumn) {
filters.push({
id: key,
value: [value],
});
}
return filters;
},
[]
);
}, [filterableColumns, searchableColumns, searchParams]);
// Table states // Table states
const [rowSelection, setRowSelection] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>(initialColumnFilters);
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => { const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
if (typeof updater === "function") { if (typeof updater === "function") {
const newPagination = updater(pagination); const newPagination = updater(pagination);
@ -191,84 +140,12 @@ export function useDataTable<TData, TValue>({
const sortingUpdater: OnChangeFn<SortingState> = (updater) => { const sortingUpdater: OnChangeFn<SortingState> = (updater) => {
if (typeof updater === "function") { if (typeof updater === "function") {
setSorting(updater(sorting)); const newSorting = updater(sorting);
console.log(newSorting);
setSorting(newSorting);
} }
}; };
// Handle server-side filtering
/*const debouncedSearchableColumnFilters = JSON.parse(
useDebounce(
JSON.stringify(
columnFilters.filter((filter) => {
return searchableColumns.find((column) => column.value === filter.id);
})
),
500
)
) as ColumnFiltersState;*/
/*const filterableColumnFilters = columnFilters.filter((filter) => {
return filterableColumns.find((column) => column.value === filter.id);
});
const [mounted, setMounted] = useState(false);*/
/*useEffect(() => {
// Opt out when advanced filter is enabled, because it contains additional params
if (enableAdvancedFilter) return;
// Prevent resetting the page on initial render
if (!mounted) {
setMounted(true);
return;
}
// Initialize new params
const newParamsObject = {
page: 1,
};
// Handle debounced searchable column filters
for (const column of debouncedSearchableColumnFilters) {
if (typeof column.value === "string") {
Object.assign(newParamsObject, {
[column.id]: typeof column.value === "string" ? column.value : null,
});
}
}
// Handle filterable column filters
for (const column of filterableColumnFilters) {
if (typeof column.value === "object" && Array.isArray(column.value)) {
Object.assign(newParamsObject, { [column.id]: column.value.join(".") });
}
}
// Remove deleted values
for (const key of searchParams.keys()) {
if (
(searchableColumns.find((column) => column.value === key) &&
!debouncedSearchableColumnFilters.find(
(column) => column.id === key
)) ||
(filterableColumns.find((column) => column.value === key) &&
!filterableColumnFilters.find((column) => column.id === key))
) {
Object.assign(newParamsObject, { [key]: null });
}
}
// After cumulating all the changes, push new params
navigate(`${location.pathname}?${createQueryString(newParamsObject)}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
// eslint-disable-next-line react-hooks/exhaustive-deps
//JSON.stringify(debouncedSearchableColumnFilters),
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(filterableColumnFilters),
]);*/
const getTableColumns = useCallback(() => { const getTableColumns = useCallback(() => {
const _columns = columns; const _columns = columns;
if (enableRowSelection) { if (enableRowSelection) {
@ -278,17 +155,16 @@ export function useDataTable<TData, TValue>({
}, [columns, enableRowSelection]); }, [columns, enableRowSelection]);
const table = useReactTable({ const table = useReactTable({
columns: getTableColumns(),
data, data,
columns: getTableColumns(),
pageCount: pageCount ?? -1,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
//getPaginationRowModel: getPaginationRowModel(),
state: { state: {
pagination, pagination,
sorting, sorting,
columnVisibility, columnVisibility,
rowSelection, rowSelection,
columnFilters,
}, },
enableRowSelection, enableRowSelection,
@ -303,16 +179,17 @@ export function useDataTable<TData, TValue>({
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
manualPagination: true, manualPagination: true,
pageCount: pageCount ?? -1,
onPaginationChange: paginationUpdater, onPaginationChange: paginationUpdater,
getFilteredRowModel: getFilteredRowModel(),
manualFiltering: true, manualFiltering: true,
onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(), getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedUniqueValues: getFacetedUniqueValues(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
}); });
return { table }; return { table };

View File

@ -40,22 +40,6 @@ export const usePaginationParams = (
const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize); const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize);
/*useEffect(() => {
// Actualizar la URL cuando cambia la paginación
const actualSearchParam = Object.fromEntries(new URLSearchParams(urlSearchParams));
if (
String(pagination.pageIndex) !== actualSearchParam.page_index ||
String(pagination.pageSize) !== actualSearchParam.page_size
) {
setUrlSearchParams({
...actualSearchParam,
page_index: String(pagination.pageIndex),
page_size: String(pagination.pageSize),
});
}
}, [pagination]);*/
const updatePagination = (newPagination: PaginationState) => { const updatePagination = (newPagination: PaginationState) => {
const _validatedPagination = setPagination(newPagination); const _validatedPagination = setPagination(newPagination);

View File

@ -4,7 +4,21 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"no": "No", "no": "No",
"yes": "Sí", "yes": "Sí",
"Accept": "Aceptar" "accept": "Aceptar",
"hide": "Ocultar",
"sort_asc": "Asc",
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
"sort_desc": "Desc",
"sort_desc_description": "En orden descendente. Click para ordenar ascendentemente.",
"sort_none_description": "Sin orden. Click para ordenar ascendentemente.",
"rows_selected": "{{count}} de {{total}} fila(s) seleccionadas.",
"rows_per_page": "Filas por página",
"num_page_of_total": "Página {{count}} de {{total}}",
"go_to_first_page": "Ir a la primera página",
"go_to_prev_page": "Ir a la página anterior",
"go_to_next_page": "Ir a la página siguiente",
"go_to_last_page": "Ir a la última página",
"reset_filter": "Quitar el filtro"
}, },
"main_menu": { "main_menu": {
"home": "Inicio", "home": "Inicio",
@ -39,7 +53,15 @@
"welcome": "Bienvenido" "welcome": "Bienvenido"
}, },
"catalog": { "catalog": {
"title": "Catálogo de artículos" "title": "Catálogo de artículos",
"list": {
"global_filter_placeholder": "Escribe aquí para filtrar los artículos...",
"columns": {
"description": "Descripción",
"points": "Puntos",
"retail_price": "PVP"
}
}
} }
} }
} }

View File

@ -2,18 +2,13 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Table = React.forwardRef< const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
HTMLTableElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableElement> <div className='relative w-full overflow-auto'>
>(({ className, ...props }, ref) => ( <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
<div className="relative w-full overflow-auto"> </div>
<table )
ref={ref} );
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table"; Table.displayName = "Table";
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
@ -28,11 +23,7 @@ const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)); ));
TableBody.displayName = "TableBody"; TableBody.displayName = "TableBody";
@ -42,28 +33,24 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props} {...props}
/> />
)); ));
TableFooter.displayName = "TableFooter"; TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef< const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
HTMLTableRowElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableRowElement> <tr
>(({ className, ...props }, ref) => ( ref={ref}
<tr className={cn(
ref={ref} "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className={cn( className
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", )}
className {...props}
)} />
{...props} )
/> );
));
TableRow.displayName = "TableRow"; TableRow.displayName = "TableRow";
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
@ -87,7 +74,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn("py-2 px-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} {...props}
/> />
)); ));
@ -97,21 +84,8 @@ const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement> React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)); ));
TableCaption.displayName = "TableCaption"; TableCaption.displayName = "TableCaption";
export { export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
};