This commit is contained in:
David Arranz 2025-11-13 19:57:45 +01:00
parent 2a1a42fd9c
commit 5bacdcc2fc
6 changed files with 109 additions and 105 deletions

View File

@ -1,57 +1,50 @@
"use client" "use client";
import { Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
import { import {
Button, DropdownMenu, Button,
DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger,
} from '@repo/shadcn-ui/components' } from "@repo/shadcn-ui/components";
import type { Table } from "@tanstack/react-table";
import { Settings2 } from "lucide-react";
export function DataTableViewOptions<TData>({ import { useTranslation } from "../../locales/i18n.ts";
table,
}: { export function DataTableViewOptions<TData>({ table }: { table: Table<TData> }) {
table: Table<TData> const { t } = useTranslation();
}) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button className="ml-auto hidden h-8 lg:flex" size="sm" type="button" variant="outline">
type="button"
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
>
<Settings2 /> <Settings2 />
View {t("components.datatable_view_options.view_button")}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]"> <DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel> <DropdownMenuLabel>
{t("components.datatable_view_options.toggle_columns")}
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{table {table
.getAllColumns() .getAllColumns()
.filter( .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => { .map((column) => {
return ( return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()} checked={column.getIsVisible()}
className="capitalize"
key={column.id}
onCheckedChange={(value) => column.toggleVisibility(!!value)} onCheckedChange={(value) => column.toggleVisibility(!!value)}
> >
{column.id} {column.id}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
) );
})} })}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }

View File

@ -1,24 +1,4 @@
"use client" "use client";
import {
ColumnDef,
ColumnFiltersState,
ColumnSizingState,
Row,
SortingState,
Table,
TableMeta,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from "@tanstack/react-table"
import * as React from "react"
import { import {
Button, Button,
@ -34,16 +14,36 @@ import {
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow TableRow,
} from '@repo/shadcn-ui/components' } from "@repo/shadcn-ui/components";
import { DataTablePagination } from './data-table-pagination.tsx' import {
import { DataTableToolbar } from "./data-table-toolbar.tsx" type ColumnDef,
type ColumnFiltersState,
type ColumnSizingState,
type Row,
type SortingState,
type Table,
type TableMeta,
type VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import * as React from "react";
import { useTranslation } from "../../locales/i18n.ts" import { useTranslation } from "../../locales/i18n.ts";
import { DataTablePagination } from "./data-table-pagination.tsx";
import { DataTableToolbar } from "./data-table-toolbar.tsx";
export type DataTableOps<TData> = { export type DataTableOps<TData> = {
onAdd?: (table: Table<TData>) => void; onAdd?: (table: Table<TData>) => void;
} };
export type DataTableRowOps<TData> = { export type DataTableRowOps<TData> = {
duplicate?(index: number, table: Table<TData>): void; duplicate?(index: number, table: Table<TData>): void;
@ -61,25 +61,25 @@ export type DataTableBulkRowOps<TData> = {
}; };
export type DataTableMeta<TData> = TableMeta<TData> & { export type DataTableMeta<TData> = TableMeta<TData> & {
totalItems?: number; // para paginación server-side totalItems?: number; // para paginación server-side
readOnly?: boolean; readOnly?: boolean;
tableOps?: DataTableOps<TData> tableOps?: DataTableOps<TData>;
rowOps?: DataTableRowOps<TData> rowOps?: DataTableRowOps<TData>;
bulkOps?: DataTableBulkRowOps<TData> bulkOps?: DataTableBulkRowOps<TData>;
} };
export interface DataTableProps<TData, TValue> { export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[];
data: TData[] data: TData[];
meta?: DataTableMeta<TData> meta?: DataTableMeta<TData>;
// Configuración // Configuración
readOnly?: boolean readOnly?: boolean;
enablePagination?: boolean enablePagination?: boolean;
pageSize?: number pageSize?: number;
enableRowSelection?: boolean enableRowSelection?: boolean;
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }> EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>;
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string; getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
@ -155,9 +155,7 @@ export function DataTable<TData, TValue>({
// Propagar cambios al padre // Propagar cambios al padre
onPaginationChange: (updater) => { onPaginationChange: (updater) => {
const next = typeof updater === "function" const next = typeof updater === "function" ? updater({ pageIndex, pageSize }) : updater;
? updater({ pageIndex, pageSize })
: updater;
if (typeof next.pageIndex === "number") onPageChange?.(next.pageIndex); if (typeof next.pageIndex === "number") onPageChange?.(next.pageIndex);
if (typeof next.pageSize === "number") onPageSizeChange?.(next.pageSize); if (typeof next.pageSize === "number") onPageSizeChange?.(next.pageSize);
@ -175,17 +173,15 @@ export function DataTable<TData, TValue>({
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(), getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedUniqueValues: getFacetedUniqueValues(),
}) });
const handleCloseEditor = React.useCallback(() => setEditIndex(null), []) const handleCloseEditor = React.useCallback(() => setEditIndex(null), []);
// Render principal // Render principal
return ( return (
<div <div className="transition-[max-height] duration-300 ease-in-out">
className="transition-[max-height] duration-300 ease-in-out"
>
<div className="flex flex-col gap-0"> <div className="flex flex-col gap-0">
<DataTableToolbar table={table} showViewOptions={!readOnly} /> <DataTableToolbar showViewOptions={!readOnly} table={table} />
<div className="overflow-hidden rounded-md border"> <div className="overflow-hidden rounded-md border">
<TableComp className="w-full text-sm"> <TableComp className="w-full text-sm">
@ -199,8 +195,8 @@ export function DataTable<TData, TValue>({
const maxW = h.column.columnDef.maxSize; const maxW = h.column.columnDef.maxSize;
return ( return (
<TableHead <TableHead
key={h.id}
colSpan={h.colSpan} colSpan={h.colSpan}
key={h.id}
style={{ style={{
width: w ? `${w}px` : undefined, width: w ? `${w}px` : undefined,
minWidth: typeof minW === "number" ? `${minW}px` : undefined, minWidth: typeof minW === "number" ? `${minW}px` : undefined,
@ -224,26 +220,28 @@ export function DataTable<TData, TValue>({
{table.getRowModel().rows.length ? ( {table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row, rowIndex) => ( table.getRowModel().rows.map((row, rowIndex) => (
<TableRow <TableRow
key={row.id}
role="button"
tabIndex={0}
data-state={row.getIsSelected() && "selected"}
className={"group bg-background cursor-pointer"} className={"group bg-background cursor-pointer"}
onKeyDown={(e) => { data-state={row.getIsSelected() && "selected"}
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any); key={row.id}
}}
onClick={(e) => onRowClick?.(row.original, rowIndex, e)} onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
onDoubleClick={ onDoubleClick={
!readOnly && !onRowClick ? () => setEditIndex(rowIndex) : undefined readOnly || onRowClick ? undefined : () => setEditIndex(rowIndex)
} > }
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ")
onRowClick?.(row.original, rowIndex, e as any);
}}
role="button"
tabIndex={0}
>
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
const w = cell.column.getSize(); const w = cell.column.getSize();
const minW = cell.column.columnDef.minSize; const minW = cell.column.columnDef.minSize;
const maxW = cell.column.columnDef.maxSize; const maxW = cell.column.columnDef.maxSize;
return ( return (
<TableCell <TableCell
key={cell.id}
className="align-top" className="align-top"
key={cell.id}
style={{ style={{
width: w ? `${w}px` : undefined, width: w ? `${w}px` : undefined,
minWidth: typeof minW === "number" ? `${minW}px` : undefined, minWidth: typeof minW === "number" ? `${minW}px` : undefined,
@ -258,7 +256,10 @@ export function DataTable<TData, TValue>({
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground"> <TableCell
className="h-24 text-center text-muted-foreground"
colSpan={columns.length}
>
{t("components.datatable.empty")} {t("components.datatable.empty")}
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -270,18 +271,21 @@ export function DataTable<TData, TValue>({
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={100}> <TableCell colSpan={100}>
<DataTablePagination table={table} onPageChange={onPageChange} onPageSizeChange={onPageSizeChange} /> <DataTablePagination
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
table={table}
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter>) </TableFooter>
} )}
</TableComp> </TableComp>
</div> </div>
{/* Editor modal */} {/* Editor modal */}
{EditorComponent && editIndex !== null && ( {EditorComponent && editIndex !== null && (
<Dialog open onOpenChange={handleCloseEditor}> <Dialog onOpenChange={handleCloseEditor} open>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("components.datatable.editor.title")}</DialogTitle> <DialogTitle>{t("components.datatable.editor.title")}</DialogTitle>
@ -290,14 +294,14 @@ export function DataTable<TData, TValue>({
<div className="mt-4"> <div className="mt-4">
<EditorComponent <EditorComponent
row={data[editIndex]}
index={editIndex} index={editIndex}
onClose={handleCloseEditor} onClose={handleCloseEditor}
row={data[editIndex]}
/> />
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="button" variant="secondary" onClick={handleCloseEditor}> <Button onClick={handleCloseEditor} type="button" variant="secondary">
{t("common.close")} {t("common.close")}
</Button> </Button>
</DialogFooter> </DialogFooter>
@ -306,5 +310,5 @@ export function DataTable<TData, TValue>({
)} )}
</div> </div>
</div> </div>
) );
} }

View File

@ -1,5 +1,5 @@
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
export const AppContent = ({ export const AppContent = ({
className, className,
@ -8,10 +8,8 @@ export const AppContent = ({
}: PropsWithChildren<{ className?: string }>) => { }: PropsWithChildren<{ className?: string }>) => {
return ( return (
<div <div
className={cn( className={cn("app-content flex flex-1 flex-col gap-4 p-4 pt-6 min-h-screen", className)}
"app-content flex flex-1 flex-col gap-4 p-4 pt-6 bg-primary/5 min-h-screen", style={{ backgroundColor: "#fdfdfd" }}
className
)}
{...props} {...props}
> >
{children} {children}

View File

@ -1,5 +1,6 @@
import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components"; import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { AppSidebar } from "./app-sidebar.tsx"; import { AppSidebar } from "./app-sidebar.tsx";
export const AppLayout = () => { export const AppLayout = () => {
@ -12,9 +13,9 @@ export const AppLayout = () => {
} as React.CSSProperties } as React.CSSProperties
} }
> >
<AppSidebar variant='inset' /> <AppSidebar variant="inset" />
{/* Aquí está el MAIN */} {/* Aquí está el MAIN */}
<SidebarInset className='app-main'> <SidebarInset className="app-main">
<Outlet /> <Outlet />
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View File

@ -35,6 +35,10 @@
"rows_selected": "{{count}} of {{total}} selected rows" "rows_selected": "{{count}} of {{total}} selected rows"
} }
}, },
"datatable_view_options": {
"view_button": "View",
"toggle_columns": "Toggle columns"
},
"loading_indicator": { "loading_indicator": {
"title": "Loading...", "title": "Loading...",
"subtitle": "This may take a few seconds. Please do not close this page." "subtitle": "This may take a few seconds. Please do not close this page."

View File

@ -38,6 +38,10 @@
"rows_selected": "{{count}} de {{total}} filas seleccionadas" "rows_selected": "{{count}} de {{total}} filas seleccionadas"
} }
}, },
"datatable_view_options": {
"view_button": "Ver",
"toggle_columns": "Alternar columnas"
},
"loading_indicator": { "loading_indicator": {
"title": "Cargando...", "title": "Cargando...",
"subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página." "subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página."