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"
},
{
"name": "Launch Chrome localhost",
"type": "pwa-chrome",
"port": 9222
},
{
"type": "msedge",
"request": "launch",

View File

@ -1,22 +1,59 @@
import { Card, CardContent } from "@/ui";
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { DataTable } from "@/components";
import { useDataTable } from "@/lib/hooks";
import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts";
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
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 { useNavigate } from "react-router-dom";
import { useCatalogList } from "../hooks";
type CatalogTableViewProps = {
data: IListResponse_DTO<IListArticles_Response_DTO>;
};
export const CatalogDataTable = () => {
const navigate = useNavigate();
const { pagination, globalFilter, isFiltered } = useDataTableContext();
console.log("pagination PADRE => ", pagination);
export const CatalogDataTable = ({ data }: CatalogTableViewProps) => {
const columns = useMemo(
const { data, isPending, isError, error } = useCatalogList({
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
searchTerm: globalFilter,
});
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
() => [
{
id: "description" as const,
accessorKey: "description",
size: 400,
enableHiding: false,
enableSorting: false,
header: () => <>{t("catalog.list.columns.description")}</>,
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,
});
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 (
<>
<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 { DataTableProvider } from "@/lib/hooks";
import { PropsWithChildren } from "react";
import { Trans } from "react-i18next";
import { CatalogProvider } from "./CatalogContext";
export const CatalogLayout = ({ children }: PropsWithChildren) => {
@ -9,8 +9,14 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
<Layout>
<LayoutHeader />
<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>
1
</Layout>
</CatalogProvider>
);

View File

@ -28,108 +28,17 @@ import {
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { useMemo } from "react";
import { DataTableProvider } from "@/lib/hooks";
import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CatalogDataTable } from "./components";
import { useCatalogList } from "./hooks/useCatalogList";
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 (
<>
<Button
onClick={() => {
setPagination({
pageIndex: pagination.pageIndex + 1,
});
}}
>
+ Página
</Button>
{data.items.map((row) => (
<p>{row.description}</p>
))}
<CatalogDataTable data={data} />;
</>
<DataTableProvider>
<CatalogDataTable />
</DataTableProvider>
);
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 (
<>
<Tabs defaultValue='all'>

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { DEFAULT_PAGE_SIZES, INITIAL_PAGE_INDEX } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
import { Table } from "@tanstack/react-table";
import { t } from "i18next";
import {
ChevronLeftIcon,
ChevronRightIcon,
@ -31,12 +32,15 @@ export function DataTablePagination<TData>({
return (
<div className={cn("flex items-center justify-between px-2", className)}>
<div className='flex-1 text-base text-muted-foreground'>
{table.getFilteredSelectedRowModel().rows.length} de{" "}
{table.getFilteredRowModel().rows.length} filas(s) seleccionadas.
{t("common.rows_selected", {
count: table.getFilteredSelectedRowModel().rows.length,
total: table.getFilteredRowModel().rows.length,
})}
</div>
<div className='flex items-center space-x-6 lg:space-x-8'>
<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
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
@ -56,7 +60,10 @@ export function DataTablePagination<TData>({
</Select>
</div>
<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 className='flex items-center space-x-2'>
<Button
@ -65,7 +72,7 @@ export function DataTablePagination<TData>({
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
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' />
</Button>
<Button
@ -74,7 +81,7 @@ export function DataTablePagination<TData>({
onClick={() => table.previousPage()}
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' />
</Button>
<Button
@ -83,7 +90,7 @@ export function DataTablePagination<TData>({
onClick={() => table.nextPage()}
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' />
</Button>
<Button
@ -92,7 +99,7 @@ export function DataTablePagination<TData>({
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
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' />
</Button>
</div>

View File

@ -1,12 +1,5 @@
import {
Skeleton,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/ui";
import { cn } from "@/lib/utils";
import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/ui";
interface DataTableSkeletonProps {
/**
@ -53,11 +46,20 @@ interface DataTableSkeletonProps {
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
* @type boolean | undefined
*/
shrinkZero?: boolean;
className?: string;
}
export function DataTableSkeleton({
@ -67,32 +69,33 @@ export function DataTableSkeleton({
filterableColumnCount = 0,
showViewOptions = true,
cellWidths = ["auto"],
withPagination = true,
shrinkZero = false,
className,
...skeletonProps
}: DataTableSkeletonProps) {
return (
<div className="w-full space-y-3 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={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 flex-1 space-x-2'>
{searchableColumnCount > 0
? 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}
{filterableColumnCount > 0
? 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}
</div>
{showViewOptions ? (
<Skeleton className="ml-auto hidden h-7 w-[4.5rem] lg:flex" />
) : null}
{showViewOptions ? <Skeleton className='ml-auto hidden h-7 w-[4.5rem] lg:flex' /> : null}
</div>
<div className="border rounded-md">
<div className='border rounded-md'>
<Table>
<TableHeader>
{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) => (
<TableHead
key={j}
@ -101,7 +104,7 @@ export function DataTableSkeleton({
minWidth: shrinkZero ? cellWidths[j] : "auto",
}}
>
<Skeleton className="w-full h-6" />
<Skeleton className='w-full h-6' />
</TableHead>
))}
</TableRow>
@ -109,7 +112,7 @@ export function DataTableSkeleton({
</TableHeader>
<TableBody>
{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) => (
<TableCell
key={j}
@ -118,7 +121,7 @@ export function DataTableSkeleton({
minWidth: shrinkZero ? cellWidths[j] : "auto",
}}
>
<Skeleton className="w-full h-6" />
<Skeleton className='w-full h-6' />
</TableCell>
))}
</TableRow>
@ -126,24 +129,26 @@ export function DataTableSkeleton({
</TableBody>
</Table>
</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">
<Skeleton className="w-40 h-8" />
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
<div className="flex items-center space-x-2">
<Skeleton className="w-24 h-8" />
<Skeleton className="h-8 w-[4.5rem]" />
</div>
<div className="flex items-center justify-center text-sm font-medium">
<Skeleton className="w-20 h-8" />
</div>
<div className="flex items-center space-x-2">
<Skeleton className="hidden size-8 lg:block" />
<Skeleton className="size-8" />
<Skeleton className="size-8" />
<Skeleton className="hidden size-8 lg:block" />
{withPagination ? (
<div className='flex items-center justify-between w-full gap-4 p-1 overflow-auto sm:gap-8'>
<Skeleton className='w-40 h-7 shrink-0' />
<div className='flex items-center gap-4 sm:gap-6 lg:gap-8'>
<div className='flex items-center space-x-2'>
<Skeleton className='w-24 h-7' />
<Skeleton className='h-7 w-[4.5rem]' />
</div>
<div className='flex items-center justify-center text-sm font-medium'>
<Skeleton className='w-20 h-7' />
</div>
<div className='flex items-center space-x-2'>
<Skeleton className='hidden size-7 lg:block' />
<Skeleton className='size-7' />
<Skeleton className='size-7' />
<Skeleton className='hidden size-7 lg:block' />
</div>
</div>
</div>
</div>
) : null}
</div>
);
}

View File

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

View File

@ -1,18 +1,44 @@
import { usePaginationParams } from "@/lib/hooks";
import { PropsWithChildren, createContext } from "react";
import { PaginationState, usePaginationParams } from "@/lib/hooks";
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 DataTableProvider = ({ children }: PropsWithChildren) => {
export const DataTableProvider = ({
initialGlobalFilter = "",
children,
}: PropsWithChildren<{
initialGlobalFilter?: string;
}>) => {
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 (
<DataTableContext.Provider
value={{
pagination,
setPagination,
sorting,
setSorting,
globalFilter,
setGlobalFilter,
resetGlobalFilter,
isFiltered,
}}
>
{children}

View File

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

View File

@ -8,14 +8,12 @@ import {
getSortedRowModel,
useReactTable,
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type VisibilityState,
} from "@tanstack/react-table";
import { getDataTableSelectionColumn } from "@/components";
import React, { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import React, { useCallback } from "react";
import { DataTableFilterField } from "./types";
import { useDataTableContext } from "./useDataTableContext";
@ -63,6 +61,22 @@ interface UseDataTableProps<TData, TValue> {
*/
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.
* - Faceted filters are rendered when `options` are provided for a filter field.
@ -110,78 +124,13 @@ export function useDataTable<TData, TValue>({
enableSorting = false,
enableHiding = false,
enableRowSelection = false,
onPaginationChange,
filterFields = [],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
enableAdvancedFilter = false,
}: UseDataTableProps<TData, TValue>) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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]);
const { pagination, setPagination, sorting, setSorting } = useDataTableContext();
// Table states
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>(initialColumnFilters);
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
if (typeof updater === "function") {
const newPagination = updater(pagination);
@ -191,84 +140,12 @@ export function useDataTable<TData, TValue>({
const sortingUpdater: OnChangeFn<SortingState> = (updater) => {
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 _columns = columns;
if (enableRowSelection) {
@ -278,17 +155,16 @@ export function useDataTable<TData, TValue>({
}, [columns, enableRowSelection]);
const table = useReactTable({
columns: getTableColumns(),
data,
columns: getTableColumns(),
pageCount: pageCount ?? -1,
getCoreRowModel: getCoreRowModel(),
//getPaginationRowModel: getPaginationRowModel(),
state: {
pagination,
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
enableRowSelection,
@ -303,16 +179,17 @@ export function useDataTable<TData, TValue>({
onColumnVisibilityChange: setColumnVisibility,
manualPagination: true,
pageCount: pageCount ?? -1,
onPaginationChange: paginationUpdater,
getFilteredRowModel: getFilteredRowModel(),
manualFiltering: true,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
});
return { table };

View File

@ -40,22 +40,6 @@ export const usePaginationParams = (
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 _validatedPagination = setPagination(newPagination);

View File

@ -4,7 +4,21 @@
"cancel": "Cancelar",
"no": "No",
"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": {
"home": "Inicio",
@ -39,7 +53,15 @@
"welcome": "Bienvenido"
},
"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";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className='relative w-full overflow-auto'>
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
)
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<
@ -28,11 +23,7 @@ const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
));
TableBody.displayName = "TableBody";
@ -42,28 +33,24 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
));
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
@ -87,7 +74,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
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}
/>
));
@ -97,21 +84,8 @@ const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
};
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };