.
This commit is contained in:
parent
e6498b4104
commit
f39dbe95cc
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user