This commit is contained in:
rgrgogu
2026-05-09 22:17:33 +08:00
parent ca6eae1d56
commit 1d94f27e61
28 changed files with 1863 additions and 220 deletions
+86 -56
View File
@@ -1,4 +1,4 @@
import { useState, useMemo, useRef, useEffect } from "react";
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
import { useReactTable, getCoreRowModel, flexRender, } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table";
import { Spinner } from "@/components/ui/spinner";
@@ -35,31 +35,62 @@ export default function DataTable({
}) {
// ─── Refs — always hold latest filters and sort ───────────────────────────
const filtersRef = useRef([]);
const sortRef = useRef([]);
const sortRef = useRef([]);
// ─── State ────────────────────────────────────────────────────────────────
const [activeColumn, setActiveColumn] = useState(null);
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
const [columnVisibility, setColumnVisibility] = useState({});
const [rowSelection, setRowSelection] = useState({});
const [filterState, setFilterState] = useState({
const [activeColumn, setActiveColumn] = useState(null);
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
const [columnVisibility, setColumnVisibility] = useState({});
const [rowSelection, setRowSelection] = useState({});
const [filterState, setFilterState] = useState({
open: false, column: null, attr: null, data: [],
});
const activeFilters = columnFilters.filter((f) => f.value !== "");
// ─── Initial fetch on mount only ─────────────────────────────────────────
// ─── setFilters — called externally by dashboard charts ──────────────────
// Merges incoming filters with existing ones (replaces by id, appends new).
// Pass an empty array [] to clear all filters.
const setFilters = useCallback((incomingFilters) => {
setColumnFilters((prev) => {
let next;
if (!incomingFilters.length) {
next = [];
} else {
// Replace matching ids, keep the rest
const incomingIds = new Set(incomingFilters.map((f) => f.id));
const kept = prev.filter((f) => !incomingIds.has(f.id));
next = [...kept, ...incomingFilters];
}
const newFilters = next.filter((f) => f.value !== "");
filtersRef.current = newFilters;
onFetch({
page: 1,
limit: pagination.limit,
filters: newFilters,
sort: sortRef.current,
});
return next;
});
}, [onFetch, pagination.limit]);
// ─── Expose refs to parent ────────────────────────────────────────────────
useEffect(() => {
onRefsReady?.({
getFilters: () => filtersRef.current,
getSort: () => sortRef.current,
resetSelection: () => table.resetRowSelection(), // ← expose this
getFilters: () => filtersRef.current,
getSort: () => sortRef.current,
resetSelection: () => table.resetRowSelection(),
setFilters, // ← new
});
onFetch({ page: 1, limit: pagination.limit, filters: [], sort: [] });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [setFilters]);
const handleOpenFilterSheet = async (e, column, attr) => {
e.preventDefault();
@@ -79,61 +110,60 @@ export default function DataTable({
rowSelection,
pagination: {
pageIndex: pagination.page - 1,
pageSize: pagination.limit,
pageSize: pagination.limit,
},
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
pageCount: pagination.totalPages,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
pageCount: pagination.totalPages,
// ─── Sort change ────────────────────────────────────────────────────────
// ─── Sort change ──────────────────────────────────────────────────────
onSortingChange: (updater) => {
const next = typeof updater === "function" ? updater(sorting) : updater;
const next = typeof updater === "function" ? updater(sorting) : updater;
const newSort = next.length ? [{ id: next[0].id, desc: next[0].desc }] : [];
setSorting(next);
sortRef.current = newSort;
onFetch({
page: 1,
limit: pagination.limit,
page: 1,
limit: pagination.limit,
filters: filtersRef.current,
sort: newSort,
sort: newSort,
});
},
// ─── Filter change ──────────────────────────────────────────────────────
// ─── Filter change ────────────────────────────────────────────────────
onColumnFiltersChange: (updater) => {
const next = typeof updater === "function" ? updater(columnFilters) : updater;
const next = typeof updater === "function" ? updater(columnFilters) : updater;
const newFilters = next.filter((f) => f.value !== "");
setColumnFilters(next);
filtersRef.current = newFilters;
onFetch({
page: 1,
limit: pagination.limit,
page: 1,
limit: pagination.limit,
filters: newFilters,
sort: sortRef.current,
sort: sortRef.current,
});
},
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getCoreRowModel: getCoreRowModel(),
});
// ─── Single handler for all page changes ──────────────────────────────────────
// ─── Page change ──────────────────────────────────────────────────────────
const handlePageChange = (page) => {
setPagination((p) => ({ ...p, page }));
onFetch({
page,
limit: pagination.limit,
limit: pagination.limit,
filters: filtersRef.current,
sort: sortRef.current,
sort: sortRef.current,
});
};
@@ -169,7 +199,7 @@ export default function DataTable({
onFetch({ page: 1, limit: pagination.limit, filters: [], sort: sortRef.current });
}}
onRemove={(id) => {
const next = columnFilters.filter((c) => c.id !== id);
const next = columnFilters.filter((c) => c.id !== id);
const newFilters = next.filter((f) => f.value !== "");
setColumnFilters(next);
filtersRef.current = newFilters;
@@ -187,7 +217,7 @@ export default function DataTable({
filters={activeFilters}
attributes={attributes}
onRemove={(id) => {
const next = columnFilters.filter((c) => c.id !== id);
const next = columnFilters.filter((c) => c.id !== id);
const newFilters = next.filter((f) => f.value !== "");
setColumnFilters(next);
filtersRef.current = newFilters;
@@ -204,8 +234,8 @@ export default function DataTable({
<TableRow key={hg.id} className="bg-muted/40 hover:bg-muted/40">
{hg.headers.map((header) => {
const isPinned = header.column.getIsPinned();
const attr = header.column.columnDef.meta?.attr;
const isPlain = header.column.id === "select" || header.column.id === "actions";
const attr = header.column.columnDef.meta?.attr;
const isPlain = header.column.id === "select" || header.column.id === "actions";
return (
<TableHead
@@ -213,10 +243,10 @@ export default function DataTable({
className={cn("align-middle whitespace-nowrap", isPinned ? "bg-muted" : "")}
style={{
position: isPinned ? "sticky" : "relative",
right: isPinned === "right" ? header.column.getStart("right") : undefined,
left: isPinned === "left" ? header.column.getStart("left") : undefined,
zIndex: isPinned ? 2 : 0,
width: header.column.columnDef.size,
right: isPinned === "right" ? header.column.getStart("right") : undefined,
left: isPinned === "left" ? header.column.getStart("left") : undefined,
zIndex: isPinned ? 2 : 0,
width: header.column.columnDef.size,
}}
>
{isPlain ? (
@@ -238,11 +268,11 @@ export default function DataTable({
</TableHeader>
{renderFilterSheet?.({
open: filterState.open,
open: filterState.open,
onOpenChange: (v) => setFilterState((p) => ({ ...p, open: v })),
column: filterState.column,
attr: filterState.attr,
data: filterState.data,
column: filterState.column,
attr: filterState.attr,
data: filterState.data,
loading,
})}
@@ -271,9 +301,9 @@ export default function DataTable({
className={cn("m-0", isPinned && "bg-card")}
style={{
position: isPinned ? "sticky" : "relative",
right: isPinned === "right" ? 0 : undefined,
left: isPinned === "left" ? cell.column.getStart("left") : undefined,
zIndex: isPinned ? 1 : 0,
right: isPinned === "right" ? 0 : undefined,
left: isPinned === "left" ? cell.column.getStart("left") : undefined,
zIndex: isPinned ? 1 : 0,
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -298,16 +328,16 @@ export default function DataTable({
<TablePagination
table={table}
pagination={pagination}
onPageChange={handlePageChange} // ← replaces setPagination
onPageChange={handlePageChange}
totalRecords={pagination.totalRecords}
rowCount={data.length}
onPageSizeChange={(size) => {
setPagination((p) => ({ ...p, limit: size, page: 1 })); // ← updates your state
setPagination((p) => ({ ...p, limit: size, page: 1 }));
onFetch({
page: 1,
limit: size,
page: 1,
limit: size,
filters: filtersRef.current,
sort: sortRef.current,
sort: sortRef.current,
});
}}
pageSizeOptions={pageSizeOptions}