175 lines
5.5 KiB
TypeScript
175 lines
5.5 KiB
TypeScript
import type {
|
|
ColumnDef,
|
|
PaginationState,
|
|
SortingState,
|
|
VisibilityState,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
import type { EntityColumn } from "../components/entity-list";
|
|
import {
|
|
readColumnVisibility,
|
|
writeColumnVisibility,
|
|
} from "../lib/entity-column-visibility-storage";
|
|
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));
|
|
|
|
return (ENTITY_PAGE_SIZES as readonly number[]).includes(value) ? value : ENTITY_PAGE_SIZES[0];
|
|
} catch {
|
|
return ENTITY_PAGE_SIZES[0];
|
|
}
|
|
}
|
|
|
|
export function useEntityTable<T extends { id: string }>({
|
|
items,
|
|
columns,
|
|
locale,
|
|
search,
|
|
onSearchChange,
|
|
filterKey,
|
|
columnVisibilityStorageKey,
|
|
}: {
|
|
items: readonly T[];
|
|
columns: readonly EntityColumn<T>[];
|
|
locale: string;
|
|
search: string | undefined;
|
|
onSearchChange: ((value: string) => void) | undefined;
|
|
filterKey: string;
|
|
columnVisibilityStorageKey?: string | undefined;
|
|
}) {
|
|
const [localSearch, setLocalSearch] = useState("");
|
|
const currentSearch = search ?? localSearch;
|
|
const sortableColumns = columns.filter((column) => column.id !== "actions" && column.value);
|
|
const defaultColumn =
|
|
sortableColumns.find((column) => column.defaultSort) ??
|
|
sortableColumns.find((column) => column.id === "name") ??
|
|
sortableColumns[0];
|
|
const initialSorting: SortingState = defaultColumn
|
|
? [{ id: defaultColumn.id, desc: defaultColumn.defaultSort === "desc" }]
|
|
: [];
|
|
const [sorting, setSorting] = useState<SortingState>(initialSorting);
|
|
const [visibility, setVisibility] = useState<VisibilityState>(() =>
|
|
readColumnVisibility(columnVisibilityStorageKey)
|
|
);
|
|
const [previousStorageKey, setPreviousStorageKey] = useState(columnVisibilityStorageKey);
|
|
|
|
if (previousStorageKey !== columnVisibilityStorageKey) {
|
|
setPreviousStorageKey(columnVisibilityStorageKey);
|
|
setVisibility(readColumnVisibility(columnVisibilityStorageKey));
|
|
}
|
|
useEffect(() => {
|
|
if (previousStorageKey === columnVisibilityStorageKey) {
|
|
writeColumnVisibility(columnVisibilityStorageKey, visibility);
|
|
}
|
|
}, [columnVisibilityStorageKey, previousStorageKey, visibility]);
|
|
const [pagination, setPagination] = useState<PaginationState>(() => ({
|
|
pageIndex: 0,
|
|
pageSize: storedPageSize(),
|
|
}));
|
|
|
|
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]);
|
|
const resetKey = `${currentSearch}\0${filterKey}`;
|
|
const [previousKey, setPreviousKey] = useState(resetKey);
|
|
|
|
if (previousKey !== resetKey) {
|
|
setPreviousKey(resetKey);
|
|
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
|
|
}
|
|
const nameCollator = useMemo(
|
|
() => new Intl.Collator(locale, { sensitivity: "base", numeric: true }),
|
|
[locale]
|
|
);
|
|
const definitions = useMemo<ColumnDef<T>[]>(
|
|
() =>
|
|
columns.map((column) => ({
|
|
id: column.id,
|
|
header: column.label,
|
|
accessorFn: column.value ?? (() => ""),
|
|
sortingFn:
|
|
column.id === "name"
|
|
? (first, second, columnId) =>
|
|
nameCollator.compare(
|
|
String(first.getValue(columnId) ?? ""),
|
|
String(second.getValue(columnId) ?? "")
|
|
)
|
|
: "auto",
|
|
enableSorting: column.id !== "actions" && !!column.value,
|
|
enableGlobalFilter: column.id !== "actions" && !!column.value,
|
|
enableHiding: column.id !== "actions" && column.hideable !== false,
|
|
})),
|
|
[columns, nameCollator]
|
|
);
|
|
const table = useReactTable({
|
|
data: [...items],
|
|
columns: definitions,
|
|
initialState: { sorting: initialSorting },
|
|
enableSortingRemoval: false,
|
|
getRowId: (item) => item.id,
|
|
state: {
|
|
sorting,
|
|
columnVisibility: Object.fromEntries(
|
|
columns.map((column) => [
|
|
column.id,
|
|
column.hideable === false || column.id === "actions"
|
|
? true
|
|
: (visibility[column.id] ?? column.defaultVisible ?? true),
|
|
])
|
|
),
|
|
columnPinning: {
|
|
right: columns.filter((column) => column.id === "actions").map((column) => column.id),
|
|
},
|
|
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);
|
|
|
|
// 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,
|
|
};
|
|
}
|