2026-10-07 15:27:23 +00:00
|
|
|
import type {
|
|
|
|
|
ColumnDef,
|
|
|
|
|
PaginationState,
|
|
|
|
|
SortingState,
|
|
|
|
|
VisibilityState,
|
|
|
|
|
} from "@tanstack/react-table";
|
|
|
|
|
import {
|
|
|
|
|
getCoreRowModel,
|
|
|
|
|
getFilteredRowModel,
|
|
|
|
|
getPaginationRowModel,
|
|
|
|
|
getSortedRowModel,
|
|
|
|
|
useReactTable,
|
|
|
|
|
} from "@tanstack/react-table";
|
2026-10-09 21:00:34 +00:00
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2026-10-07 15:27:23 +00:00
|
|
|
|
|
|
|
|
import type { EntityColumn } from "../components/entity-list";
|
2026-10-09 21:00:34 +00:00
|
|
|
import { ENTITY_PAGE_SIZES, entityListRange } from "../lib/entity-list";
|
|
|
|
|
|
|
|
|
|
const ENTITY_PAGE_SIZE_STORAGE_KEY = "entity-page-size";
|
|
|
|
|
|
|
|
|
|
function storedPageSize(): number {
|
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
return ENTITY_PAGE_SIZES[0];
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const value = Number(window.localStorage.getItem(ENTITY_PAGE_SIZE_STORAGE_KEY));
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 21:00:34 +00:00
|
|
|
return (ENTITY_PAGE_SIZES as readonly number[]).includes(value) ? value : ENTITY_PAGE_SIZES[0];
|
|
|
|
|
} catch {
|
|
|
|
|
return ENTITY_PAGE_SIZES[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 15:27:23 +00:00
|
|
|
export function useEntityTable<T extends { id: string }>({
|
|
|
|
|
items,
|
|
|
|
|
columns,
|
|
|
|
|
search,
|
|
|
|
|
onSearchChange,
|
|
|
|
|
filterKey,
|
|
|
|
|
}: {
|
|
|
|
|
items: readonly T[];
|
|
|
|
|
columns: readonly EntityColumn<T>[];
|
|
|
|
|
search: string | undefined;
|
|
|
|
|
onSearchChange: ((value: string) => void) | undefined;
|
|
|
|
|
filterKey: string;
|
|
|
|
|
}) {
|
|
|
|
|
const [localSearch, setLocalSearch] = useState("");
|
|
|
|
|
const currentSearch = search ?? localSearch;
|
2026-10-15 09:36:42 +00:00
|
|
|
const sortableColumns = columns.filter((column) => column.id !== "actions" && column.value);
|
|
|
|
|
const defaultColumn = sortableColumns.find((column) => column.defaultSort) ?? sortableColumns[0];
|
|
|
|
|
const initialSorting: SortingState = defaultColumn
|
|
|
|
|
? [{ id: defaultColumn.id, desc: defaultColumn.defaultSort === "desc" }]
|
|
|
|
|
: [];
|
|
|
|
|
const [sorting, setSorting] = useState<SortingState>(initialSorting);
|
2026-10-07 15:27:23 +00:00
|
|
|
const [visibility, setVisibility] = useState<VisibilityState>({});
|
2026-10-09 21:00:34 +00:00
|
|
|
const [pagination, setPagination] = useState<PaginationState>(() => ({
|
|
|
|
|
pageIndex: 0,
|
|
|
|
|
pageSize: storedPageSize(),
|
|
|
|
|
}));
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 21:00:34 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
try {
|
|
|
|
|
window.localStorage.setItem(ENTITY_PAGE_SIZE_STORAGE_KEY, String(pagination.pageSize));
|
|
|
|
|
} catch {
|
|
|
|
|
// La paginación sigue funcionando aunque el almacenamiento no esté disponible.
|
|
|
|
|
}
|
|
|
|
|
}, [pagination.pageSize]);
|
2026-10-07 15:27:23 +00:00
|
|
|
const resetKey = `${currentSearch}\0${filterKey}`;
|
|
|
|
|
const [previousKey, setPreviousKey] = useState(resetKey);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 15:27:23 +00:00
|
|
|
if (previousKey !== resetKey) {
|
|
|
|
|
setPreviousKey(resetKey);
|
|
|
|
|
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
|
|
|
|
|
}
|
|
|
|
|
const definitions = useMemo<ColumnDef<T>[]>(
|
|
|
|
|
() =>
|
|
|
|
|
columns.map((column) => ({
|
|
|
|
|
id: column.id,
|
|
|
|
|
header: column.label,
|
|
|
|
|
accessorFn: column.value ?? (() => ""),
|
2026-10-07 16:29:44 +00:00
|
|
|
enableSorting: column.id !== "actions" && !!column.value,
|
|
|
|
|
enableGlobalFilter: column.id !== "actions" && !!column.value,
|
|
|
|
|
enableHiding: column.id !== "actions" && column.hideable !== false,
|
2026-10-07 15:27:23 +00:00
|
|
|
})),
|
|
|
|
|
[columns]
|
|
|
|
|
);
|
|
|
|
|
const table = useReactTable({
|
|
|
|
|
data: [...items],
|
|
|
|
|
columns: definitions,
|
2026-10-15 09:36:42 +00:00
|
|
|
initialState: { sorting: initialSorting },
|
|
|
|
|
enableSortingRemoval: false,
|
2026-10-07 15:27:23 +00:00
|
|
|
getRowId: (item) => item.id,
|
|
|
|
|
state: {
|
|
|
|
|
sorting,
|
|
|
|
|
columnVisibility: visibility,
|
2026-10-07 16:29:44 +00:00
|
|
|
columnPinning: {
|
|
|
|
|
right: columns.filter((column) => column.id === "actions").map((column) => column.id),
|
|
|
|
|
},
|
2026-10-07 15:27:23 +00:00
|
|
|
pagination,
|
|
|
|
|
globalFilter: onSearchChange ? "" : currentSearch.trim(),
|
|
|
|
|
},
|
|
|
|
|
onSortingChange: setSorting,
|
|
|
|
|
onColumnVisibilityChange: setVisibility,
|
|
|
|
|
onPaginationChange: setPagination,
|
|
|
|
|
autoResetPageIndex: false,
|
|
|
|
|
globalFilterFn: "includesString",
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
|
|
|
getSortedRowModel: getSortedRowModel(),
|
|
|
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
|
|
|
});
|
|
|
|
|
const total = table.getFilteredRowModel().rows.length;
|
|
|
|
|
const range = entityListRange(total, pagination.pageIndex, pagination.pageSize);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 15:27:23 +00:00
|
|
|
// Una actualización puede reducir el conjunto sin cambiar los filtros.
|
|
|
|
|
if (pagination.pageIndex !== range.page) {
|
|
|
|
|
setPagination({ ...pagination, pageIndex: range.page });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
table,
|
|
|
|
|
total,
|
|
|
|
|
range,
|
|
|
|
|
pagination,
|
|
|
|
|
setPagination,
|
|
|
|
|
sorting,
|
|
|
|
|
setSorting,
|
|
|
|
|
currentSearch,
|
|
|
|
|
setLocalSearch,
|
|
|
|
|
};
|
|
|
|
|
}
|