feat: implement DataTableProvider for centralized data management and refactor related components
This commit is contained in:
parent
083a97e74f
commit
4ac90699b6
6 changed files with 204 additions and 158 deletions
|
|
@ -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,104 +144,108 @@ 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}
|
hasSelectRow={hasSelectRow}
|
||||||
globalFilter={globalFilter}
|
pinnedFiltri={pinnedFiltri}
|
||||||
pinnedColumns={pinnedFiltri}
|
searchColumn={searchColumn}
|
||||||
searchColumn={searchColumn}
|
table={table}
|
||||||
table={table}
|
>
|
||||||
/>
|
<div className="space-y-4">
|
||||||
<div
|
<DataTableToolbar />
|
||||||
className="rounded-md border dark:border-muted-foreground/50"
|
<div
|
||||||
ref={parentRef}
|
className="rounded-md border dark:border-muted-foreground/50"
|
||||||
>
|
ref={parentRef}
|
||||||
<Table>
|
>
|
||||||
<TableHeader>
|
<Table>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableHeader>
|
||||||
<TableRow key={headerGroup.id}>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
{headerGroup.headers.map((header) => {
|
<TableRow key={headerGroup.id}>
|
||||||
return (
|
{headerGroup.headers.map((header) => {
|
||||||
<TableHead key={header.id}>
|
return (
|
||||||
{header.isPlaceholder
|
<TableHead key={header.id}>
|
||||||
? null
|
{header.isPlaceholder
|
||||||
: flexRender(
|
? null
|
||||||
header.column.columnDef.header,
|
: flexRender(
|
||||||
header.getContext(),
|
header.column.columnDef.header,
|
||||||
)}
|
header.getContext(),
|
||||||
</TableHead>
|
)}
|
||||||
);
|
</TableHead>
|
||||||
})}
|
);
|
||||||
</TableRow>
|
})}
|
||||||
))}
|
</TableRow>
|
||||||
</TableHeader>
|
))}
|
||||||
<TableBody className={tabClassName}>
|
</TableHeader>
|
||||||
{virtualizer.getVirtualItems().length ? (
|
<TableBody className={tabClassName}>
|
||||||
virtualizer.getVirtualItems().map((virtualRow, index) => {
|
{virtualizer.getVirtualItems().length ? (
|
||||||
// biome-ignore lint/style/noNonNullAssertion: <always exists>
|
virtualizer.getVirtualItems().map((virtualRow, index) => {
|
||||||
const row = rows[virtualRow.index]!;
|
// biome-ignore lint/style/noNonNullAssertion: <always exists>
|
||||||
|
const row = rows[virtualRow.index]!;
|
||||||
|
|
||||||
const rowElem = (
|
const rowElem = (
|
||||||
<TableRow
|
<TableRow
|
||||||
className={cn(
|
className={cn(
|
||||||
rowOnClick && "cursor-pointer",
|
rowOnClick && "cursor-pointer",
|
||||||
highlightedRow === row.id && "bg-neutral-100/50",
|
highlightedRow === row.id && "bg-neutral-100/50",
|
||||||
"group/row",
|
"group/row",
|
||||||
)}
|
)}
|
||||||
data-row-id={row.id}
|
data-row-id={row.id}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
key={row.id}
|
key={row.id}
|
||||||
onClick={
|
onClick={
|
||||||
rowOnClick
|
rowOnClick
|
||||||
? () => rowOnClick(row.original, row.id)
|
? () => rowOnClick(row.original, row.id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
height: `${virtualRow.size}px`,
|
height: `${virtualRow.size}px`,
|
||||||
transform: `translateY(${
|
transform: `translateY(${
|
||||||
virtualRow.start - index * virtualRow.size
|
virtualRow.start - index * virtualRow.size
|
||||||
}px)`,
|
}px)`,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
return (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (withContextMenu && contextMenuContent) {
|
||||||
|
return (
|
||||||
|
<ContextMenu key={row.id}>
|
||||||
|
<ContextMenuTrigger asChild>
|
||||||
|
{rowElem}
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
|
||||||
|
{contextMenuContent(row.original)}
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rowElem;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
className="h-24 text-center"
|
||||||
|
colSpan={columns.length}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
No results.
|
||||||
return (
|
</TableCell>
|
||||||
<TableCell key={cell.id}>
|
</TableRow>
|
||||||
{flexRender(
|
)}
|
||||||
cell.column.columnDef.cell,
|
</TableBody>
|
||||||
cell.getContext(),
|
</Table>
|
||||||
)}
|
</div>
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (withContextMenu && contextMenuContent) {
|
<DataTablePagination />
|
||||||
return (
|
|
||||||
<ContextMenu key={row.id}>
|
|
||||||
<ContextMenuTrigger asChild>{rowElem}</ContextMenuTrigger>
|
|
||||||
|
|
||||||
{contextMenuContent(row.original)}
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rowElem;
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
className="h-24 text-center"
|
|
||||||
colSpan={columns.length}
|
|
||||||
>
|
|
||||||
No results.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</DataTableProvider>
|
||||||
<DataTablePagination hasSelectRow={hasSelectRow} table={table} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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 ?? ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
57
apps/infoalloggi/src/providers/DataTableProvider.tsx
Normal file
57
apps/infoalloggi/src/providers/DataTableProvider.tsx
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue