feat: implement DataTableProvider for centralized data management and refactor related components

This commit is contained in:
Marco Pedone 2026-03-18 14:54:33 +01:00
parent 083a97e74f
commit 4ac90699b6
6 changed files with 204 additions and 158 deletions

View file

@ -31,9 +31,10 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { DataTableProvider } from "~/providers/DataTableProvider";
import { ContextMenu, ContextMenuTrigger } from "../ui/context-menu"; import { ContextMenu, ContextMenuTrigger } from "../ui/context-menu";
type DataTableProps<TData, TValue> = { export type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
pinnedFiltri?: PinnedFiltro[]; pinnedFiltri?: PinnedFiltro[];
@ -143,14 +144,15 @@ export function DataTable<TData, TValue>({
}, [table, onTableInit]); }, [table, onTableInit]);
return ( return (
<div className="space-y-4"> <DataTableProvider
<DataTableToolbar
columns_titles={columns_titles} columns_titles={columns_titles}
globalFilter={globalFilter} hasSelectRow={hasSelectRow}
pinnedColumns={pinnedFiltri} pinnedFiltri={pinnedFiltri}
searchColumn={searchColumn} searchColumn={searchColumn}
table={table} table={table}
/> >
<div className="space-y-4">
<DataTableToolbar />
<div <div
className="rounded-md border dark:border-muted-foreground/50" className="rounded-md border dark:border-muted-foreground/50"
ref={parentRef} ref={parentRef}
@ -218,7 +220,9 @@ export function DataTable<TData, TValue>({
if (withContextMenu && contextMenuContent) { if (withContextMenu && contextMenuContent) {
return ( return (
<ContextMenu key={row.id}> <ContextMenu key={row.id}>
<ContextMenuTrigger asChild>{rowElem}</ContextMenuTrigger> <ContextMenuTrigger asChild>
{rowElem}
</ContextMenuTrigger>
{contextMenuContent(row.original)} {contextMenuContent(row.original)}
</ContextMenu> </ContextMenu>
@ -240,7 +244,8 @@ export function DataTable<TData, TValue>({
</Table> </Table>
</div> </div>
<DataTablePagination hasSelectRow={hasSelectRow} table={table} /> <DataTablePagination />
</div> </div>
</DataTableProvider>
); );
} }

View file

@ -1,4 +1,3 @@
import type { Table } from "@tanstack/react-table";
import { Filter } from "lucide-react"; import { Filter } from "lucide-react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
@ -9,16 +8,10 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { useDataTable } from "~/providers/DataTableProvider";
interface DataTableViewOptionsProps<TData> { export function DataTableViewOptions<TData>() {
table: Table<TData>; const { table, columns_titles } = useDataTable<TData>();
columns_titles: Record<string, string>;
}
export function DataTableViewOptions<TData>({
table,
columns_titles,
}: DataTableViewOptionsProps<TData>) {
const hidableColumns = table const hidableColumns = table
.getAllColumns() .getAllColumns()
.filter( .filter(

View file

@ -1,4 +1,3 @@
import type { Table } from "@tanstack/react-table";
import { import {
ChevronFirst, ChevronFirst,
ChevronLast, ChevronLast,
@ -13,16 +12,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "~/components/ui/select"; } from "~/components/ui/select";
import { useDataTable } from "~/providers/DataTableProvider";
interface DataTablePaginationProps<TData> { export function DataTablePagination<TData>() {
table: Table<TData>; const { table, hasSelectRow } = useDataTable<TData>();
hasSelectRow?: boolean;
}
export function DataTablePagination<TData>({
table,
hasSelectRow,
}: DataTablePaginationProps<TData>) {
return ( return (
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<div className="flex-1 text-muted-foreground text-sm"> <div className="flex-1 text-muted-foreground text-sm">
@ -40,7 +33,7 @@ export function DataTablePagination<TData>({
}} }}
value={`${table.getState().pagination.pageSize}`} value={`${table.getState().pagination.pageSize}`}
> >
<SelectTrigger className="h-8 w-[70px]"> <SelectTrigger className="h-8 w-17.5">
<SelectValue placeholder={table.getState().pagination.pageSize} /> <SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">

View file

@ -1,10 +1,10 @@
import type { Table } from "@tanstack/react-table";
import { RotateCcw } from "lucide-react"; import { RotateCcw } from "lucide-react";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { DataTableFacetedFilter } from "~/components/custom_ui/dataTable-FacetedFilter"; import { DataTableFacetedFilter } from "~/components/custom_ui/dataTable-FacetedFilter";
import { DataTableViewOptions } from "~/components/custom_ui/dataTable-ViewToggle"; import { DataTableViewOptions } from "~/components/custom_ui/dataTable-ViewToggle";
import Input from "~/components/custom_ui/input"; import Input from "~/components/custom_ui/input";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { useDataTable } from "~/providers/DataTableProvider";
export type PinnedFiltro = { export type PinnedFiltro = {
title: string; title: string;
@ -15,51 +15,22 @@ export type PinnedFiltro = {
icon?: ComponentType<{ className?: string }>; icon?: ComponentType<{ className?: string }>;
}[]; }[];
}; };
/**
* columnName: null per ricerca globale
*/
export type SearchFiltro = { export type SearchFiltro = {
columnName: string; columnName: string | null;
placeholder: string; placeholder: string;
}; };
export function DataTableToolbar<TData>(props: { export function DataTableToolbar<TData>() {
table: Table<TData>; const { table, pinnedFiltri } = useDataTable<TData>();
pinnedColumns?: PinnedFiltro[];
searchColumn?: SearchFiltro;
columns_titles: Record<string, string>;
globalFilter: string;
}) {
const { table, pinnedColumns, searchColumn, columns_titles, globalFilter } =
props;
const isFiltered = table.getState().columnFilters.length > 0; const isFiltered = table.getState().columnFilters.length > 0;
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-1 flex-wrap items-center gap-2"> <div className="flex flex-1 flex-wrap items-center gap-2">
{searchColumn && <SearchInput />
(searchColumn.columnName ? ( {pinnedFiltri?.map(
<Input
className="mt-0 h-8 w-37.5"
onChange={(event) =>
table
.getColumn(searchColumn.columnName)
?.setFilterValue(event.target.value)
}
placeholder={searchColumn.placeholder}
value={
(table
.getColumn(searchColumn.columnName)
?.getFilterValue() as string) ?? ""
}
/>
) : (
<Input
className="mt-0 h-8 w-37.5"
onChange={(event) =>
table.setGlobalFilter(String(event.target.value))
}
placeholder={searchColumn.placeholder}
value={globalFilter ?? ""}
/>
))}
{pinnedColumns?.map(
(column) => (column) =>
table.getColumn(column.columnName) && ( table.getColumn(column.columnName) && (
<DataTableFacetedFilter <DataTableFacetedFilter
@ -82,7 +53,34 @@ export function DataTableToolbar<TData>(props: {
</Button> </Button>
)} )}
</div> </div>
<DataTableViewOptions columns_titles={columns_titles} table={table} /> <DataTableViewOptions />
</div> </div>
); );
} }
function SearchInput<TData>() {
const { table, searchColumn } = useDataTable<TData>();
const { globalFilter } = table.getState();
if (!searchColumn) return null;
const { columnName, placeholder } = searchColumn;
if (columnName) {
return (
<Input
className="mt-0 h-8 w-37.5"
onChange={(event) =>
table.getColumn(columnName)?.setFilterValue(event.target.value)
}
placeholder={placeholder}
value={(table.getColumn(columnName)?.getFilterValue() as string) ?? ""}
/>
);
}
return (
<Input
className="mt-0 h-8 w-37.5"
onChange={(event) => table.setGlobalFilter(String(event.target.value))}
placeholder={searchColumn.placeholder}
value={globalFilter ?? ""}
/>
);
}

View file

@ -248,8 +248,8 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
}, },
]; ];
const searchFiltro: SearchFiltro = { const searchFiltro: SearchFiltro = {
columnName: "username", columnName: null,
placeholder: "Filtra utenti...", placeholder: "Cerca...",
}; };
return ( return (
<div className="mx-auto w-full px-2 py-10"> <div className="mx-auto w-full px-2 py-10">

View file

@ -0,0 +1,57 @@
import type { Table } from "@tanstack/react-table";
import { createContext, type ReactNode, useContext } from "react";
import type {
PinnedFiltro,
SearchFiltro,
} from "~/components/custom_ui/dataTable-toolbar";
type DataTableContextType<TData> = {
table: Table<TData>;
searchColumn?: SearchFiltro;
columns_titles: Record<string, string>;
pinnedFiltri?: PinnedFiltro[];
hasSelectRow?: boolean;
};
// biome-ignore lint/suspicious/noExplicitAny: <needeed>
const DataTableContext = createContext<DataTableContextType<any> | null>(null);
export function DataTableProvider<TData>({
children,
table,
searchColumn,
columns_titles,
pinnedFiltri,
hasSelectRow,
}: {
children: ReactNode;
table: Table<TData>;
searchColumn?: SearchFiltro;
columns_titles: Record<string, string>;
pinnedFiltri?: PinnedFiltro[];
hasSelectRow?: boolean;
}) {
return (
<DataTableContext.Provider
value={{
table,
searchColumn,
columns_titles,
pinnedFiltri,
hasSelectRow,
}}
>
{children}
</DataTableContext.Provider>
);
}
export const useDataTable = <TData,>() => {
const value = useContext(
DataTableContext,
) as DataTableContextType<TData> | null;
if (!value) {
throw new Error("useDataTable must be used within a DataTableProvider");
}
return value;
};