Guardar preferencias y returnTo en grid de listados
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
c98bc6cafc
commit
6e53e5bd58
@ -1,6 +1,7 @@
|
||||
export * from "./use-datasource";
|
||||
export * from "./use-debounce";
|
||||
export * from "./use-hook-form";
|
||||
export * from "./use-return-to-navigate";
|
||||
export * from "./use-rhf-error-focus";
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
export * from "./use-url-param-id";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./use-return-to-navigation";
|
||||
@ -0,0 +1,28 @@
|
||||
import * as React from "react";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
interface UseReturnToNavigationParams {
|
||||
fallbackPath: string;
|
||||
}
|
||||
|
||||
export const useReturnToNavigation = ({ fallbackPath }: UseReturnToNavigationParams) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const currentReturnTo = React.useMemo(() => {
|
||||
return `${location.pathname}${location.search}`;
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
const returnTo = searchParams.get("returnTo") || fallbackPath;
|
||||
|
||||
const navigateBack = React.useCallback(() => {
|
||||
navigate(returnTo);
|
||||
}, [navigate, returnTo]);
|
||||
|
||||
return {
|
||||
currentReturnTo,
|
||||
returnTo,
|
||||
navigateBack,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,8 @@
|
||||
import { useDebounce } from "@erp/core/hooks";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDataTablePreferences } from "@repo/rdx-ui/components";
|
||||
import { NumberHelper } from "@repo/rdx-utils";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
type ListProformasByCriteriaParams,
|
||||
@ -19,10 +22,33 @@ const EMPTY_PROFORMAS_LIST: ProformaList = {
|
||||
};
|
||||
|
||||
export const useListProformasController = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("all");
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const tablePreferences = useDataTablePreferences({
|
||||
storageKey: "proformas:list:grid",
|
||||
defaultPageSize: 5,
|
||||
defaultColumnVisibility: {
|
||||
reference: true,
|
||||
recipientName: true,
|
||||
status: true,
|
||||
totalAmountFmt: true,
|
||||
invoiceDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { pageSize: preferencesPageSize, setPageSize: setPageSizePreference } = tablePreferences;
|
||||
|
||||
// Parse page from URL (1-based) or default to 1
|
||||
const urlPage = NumberHelper.parsePositiveInteger(searchParams.get("page"), 1);
|
||||
const pageIndex = urlPage - 1; // Convert to 0-based for internal use
|
||||
|
||||
// Parse pageSize from URL or use preferences
|
||||
const urlPageSize = searchParams.get("pageSize");
|
||||
const pageSize = urlPageSize
|
||||
? NumberHelper.parsePositiveInteger(urlPageSize, preferencesPageSize)
|
||||
: preferencesPageSize;
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
|
||||
@ -41,42 +67,71 @@ export const useListProformasController = () => {
|
||||
|
||||
const query = useProformasListQuery({ criteria });
|
||||
|
||||
const setStatusFilterValue = (value: string) => {
|
||||
const setStatusFilterValue = useCallback(
|
||||
(value: string) => {
|
||||
const nextValue = (value || "all") as ProformaListStatusFilter;
|
||||
|
||||
setStatusFilter((prev) => {
|
||||
if (prev === nextValue) return prev;
|
||||
|
||||
// Sólo si la búsqueda realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
// Reset page to 1 when status filter changes
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("page", "1");
|
||||
return params;
|
||||
});
|
||||
return nextValue;
|
||||
});
|
||||
};
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
const setSearchValue = (value: string) => {
|
||||
const setSearchValue = useCallback(
|
||||
(value: string) => {
|
||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||
|
||||
setSearch((prev) => {
|
||||
if (prev === nextValue) return prev;
|
||||
|
||||
// Sólo si la búsqueda realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
// Reset page to 1 when search changes
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("page", "1");
|
||||
return params;
|
||||
});
|
||||
return nextValue;
|
||||
});
|
||||
};
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
const setPageSizeValue = (value: number) => {
|
||||
setPageSize((prev) => {
|
||||
if (prev === value) return prev;
|
||||
|
||||
// Sólo si el tamaño de página realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return value;
|
||||
const setPageIndexValue = useCallback(
|
||||
(newPageIndex: number) => {
|
||||
const newPage = newPageIndex + 1; // Convert to 1-based for URL
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("page", String(newPage));
|
||||
return params;
|
||||
});
|
||||
};
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
const setPageSizeValue = useCallback(
|
||||
(value: number) => {
|
||||
if (pageSize === value) return;
|
||||
|
||||
// Reset page to 1 and update pageSize when it changes
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("page", "1");
|
||||
params.set("pageSize", String(value));
|
||||
return params;
|
||||
});
|
||||
setPageSizePreference(value);
|
||||
},
|
||||
[pageSize, setSearchParams, setPageSizePreference]
|
||||
);
|
||||
|
||||
return {
|
||||
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
||||
@ -88,9 +143,11 @@ export const useListProformasController = () => {
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
tablePreferences,
|
||||
|
||||
pageIndex,
|
||||
pageSize,
|
||||
setPageIndex,
|
||||
setPageIndex: setPageIndexValue,
|
||||
setPageSize: setPageSizeValue,
|
||||
|
||||
search,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
import type { ProformaList, ProformaListRow } from "../../../../shared";
|
||||
@ -16,6 +16,9 @@ interface ProformasGridProps {
|
||||
onPageChange: (pageIndex: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
|
||||
columnVisibility?: VisibilityState;
|
||||
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||
|
||||
onRowClick?: (proformaId: string) => void;
|
||||
}
|
||||
|
||||
@ -28,6 +31,8 @@ export const ProformasGrid = ({
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
columnVisibility,
|
||||
onColumnVisibilityChange,
|
||||
onRowClick,
|
||||
}: ProformasGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -47,10 +52,12 @@ export const ProformasGrid = ({
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
data={items}
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
onColumnVisibilityChange={onColumnVisibilityChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
//onRowClick={(row) => onRowClick?.(row.id)}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
@ -12,7 +13,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FilterIcon, PlusIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createSearchParams, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { ChangeProformaStatusDialog } from "../../../change-status";
|
||||
@ -29,12 +30,26 @@ import { ProformaStatusBadge } from "../components";
|
||||
export const ListProformasPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const { currentReturnTo } = useReturnToNavigation({
|
||||
fallbackPath: "/proformas",
|
||||
});
|
||||
|
||||
const { listCtrl, panelCtrl, deleteDialogCtrl, issueDialogCtrl, changeStatusDialogCtrl } =
|
||||
useListProformasPageController();
|
||||
|
||||
const handleEditClick = (proformaId: string) => {
|
||||
navigate({
|
||||
pathname: `/proformas/${proformaId}/edit`,
|
||||
search: createSearchParams({
|
||||
returnTo: currentReturnTo,
|
||||
}).toString(),
|
||||
});
|
||||
};
|
||||
|
||||
const columns = useProformasGridColumns({
|
||||
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||
onIssueClick: (proformaRow) =>
|
||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||
onDeleteClick: (proformaRow: ProformaListRow) =>
|
||||
@ -75,9 +90,11 @@ export const ListProformasPage = () => {
|
||||
<div>
|
||||
<ProformasGrid
|
||||
columns={columns}
|
||||
columnVisibility={listCtrl.tablePreferences.columnVisibility}
|
||||
data={listCtrl.data}
|
||||
fetching={listCtrl.isFetching}
|
||||
loading={listCtrl.isLoading}
|
||||
onColumnVisibilityChange={listCtrl.tablePreferences.setColumnVisibility}
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
||||
@ -86,26 +103,33 @@ export const ListProformasPage = () => {
|
||||
/>
|
||||
|
||||
{/* Explicación técnica */}
|
||||
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-2">
|
||||
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-4">
|
||||
<p className="font-semibold text-foreground">Estado de proforma</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<ul className="list-disc list-inside space-y-4">
|
||||
<li>
|
||||
<ProformaStatusBadge status="draft" />
|
||||
<strong className="text-foreground">Borrador:</strong> Un{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">div</code> absoluto con{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">pointer-events: none</code> y{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">linear-gradient</code> se superpone
|
||||
encima del scroll.
|
||||
<strong className="text-foreground">Borrador:</strong> Una proforma sin terminar y
|
||||
pendiente de modificaciones.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Columna sticky:</strong> La celda de acciones usa{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">sticky right-0 z-20</code>.
|
||||
<ProformaStatusBadge status="sent" />
|
||||
<strong className="text-foreground">Enviada:</strong> Se ha enviado la proforma al
|
||||
cliente para su aprobación.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Responsive:</strong> Columnas ocultas en móviles (
|
||||
<code className="rounded bg-muted px-1 text-xs">hidden sm:</code>,{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">hidden md:</code>,{" "}
|
||||
<code className="rounded bg-muted px-1 text-xs">hidden lg:</code>).
|
||||
<ProformaStatusBadge status="approved" />
|
||||
<strong className="text-foreground">Aprobada:</strong> La proforma ha sido aprobada
|
||||
por el cliente y puede pasar a factura.
|
||||
</li>
|
||||
<li>
|
||||
<ProformaStatusBadge status="rejected" />
|
||||
<strong className="text-foreground">Rechazada:</strong> La proforma ha sido rechazada
|
||||
por el cliente. Puede quedarse así o volver a "borrador".
|
||||
</li>
|
||||
<li>
|
||||
<ProformaStatusBadge status="issued" />
|
||||
<strong className="text-foreground">Facturada:</strong> La proforma ya ha sido pasada
|
||||
a factura.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -162,7 +186,7 @@ export const ListProformasPage = () => {
|
||||
<ProformaSummaryPanel
|
||||
className="border bg-background"
|
||||
mode={panelCtrl.panelState.mode}
|
||||
onEdit={(proforma) => navigate(`/proformas/${proforma.id}/edit`)}
|
||||
onEdit={(proforma) => handleEditClick(proforma.id)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
panelCtrl.closePanel();
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
import { useCustomerSelectionFlow } from "@erp/customers/common";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
||||
|
||||
export const useUpdateProformaPageController = () => {
|
||||
const proformaId = useUrlParamId();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const updateCtrl = useUpdateProformaController(proformaId);
|
||||
|
||||
const returnTo = searchParams.get("returnTo") ?? "/proformas";
|
||||
|
||||
const selectCustomerCtrl = useCustomerSelectionFlow({
|
||||
defaultLanguageCode: updateCtrl.form.watch("languageCode"),
|
||||
defaultCurrencyCode: updateCtrl.form.watch("currencyCode"),
|
||||
@ -19,5 +23,6 @@ export const useUpdateProformaPageController = () => {
|
||||
return {
|
||||
updateCtrl,
|
||||
selectCustomerCtrl,
|
||||
returnTo,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
||||
import {
|
||||
FormCommitButtonGroup,
|
||||
UnsavedChangesProvider,
|
||||
useReturnToNavigation,
|
||||
} from "@erp/core/hooks";
|
||||
import { SelectCustomerDialog } from "@erp/customers";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
||||
@ -14,10 +17,13 @@ import { ProformaUpdateEditorForm } from "../editors";
|
||||
|
||||
export const ProformaUpdatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||
|
||||
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
||||
const { updateCtrl, selectCustomerCtrl, returnTo } = useUpdateProformaPageController();
|
||||
|
||||
const { navigateBack } = useReturnToNavigation({
|
||||
fallbackPath: returnTo,
|
||||
});
|
||||
|
||||
if (updateCtrl.isLoading) {
|
||||
return <ProformaUpdateSkeleton />;
|
||||
@ -36,7 +42,7 @@ export const ProformaUpdatePage = () => {
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<BackHistoryButton />
|
||||
<BackHistoryButton onClick={() => navigateBack()} />
|
||||
</div>
|
||||
</AppContent>
|
||||
);
|
||||
@ -61,14 +67,15 @@ export const ProformaUpdatePage = () => {
|
||||
<AppHeader className="mx-auto max-w-5xl space-y-4">
|
||||
<PageHeader
|
||||
description={t("pages.proformas.update.description")}
|
||||
onBackClick={() => navigate("/proformas/list")}
|
||||
onBackClick={() => navigateBack()}
|
||||
rightSlot={
|
||||
<FormCommitButtonGroup
|
||||
cancel={{
|
||||
to: "/proformas/list",
|
||||
onCancel: () => navigateBack(),
|
||||
}}
|
||||
disabled={updateCtrl.isUpdating}
|
||||
isLoading={updateCtrl.isUpdating}
|
||||
onBack={() => navigateBack()}
|
||||
onReset={updateCtrl.form.formState.isDirty ? updateCtrl.resetForm : undefined}
|
||||
submit={{
|
||||
formId: updateCtrl.formId,
|
||||
|
||||
@ -4,12 +4,17 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
|
||||
export const BackHistoryButton = () => {
|
||||
export const BackHistoryButton = ({ onClick }: { onClick?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Button className="h-7 w-7" onClick={() => navigate(-1)} size="icon" variant="outline">
|
||||
<Button
|
||||
className="h-7 w-7"
|
||||
onClick={() => navigate(-1) || onClick}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
<span className="sr-only">{t("common.back")}</span>
|
||||
</Button>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
type ColumnSizingState,
|
||||
type OnChangeFn,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type Table,
|
||||
@ -71,6 +72,8 @@ export interface DataTableProps<TData, TValue> {
|
||||
|
||||
// Configuración
|
||||
columnVisibility?: VisibilityState;
|
||||
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||
|
||||
readOnly?: boolean;
|
||||
enablePagination?: boolean;
|
||||
pageSize?: number;
|
||||
@ -86,7 +89,11 @@ export interface DataTableProps<TData, TValue> {
|
||||
onPageSizeChange?: (pageSize: number) => void;
|
||||
|
||||
// Acción al hacer click en una fila
|
||||
onRowClick?: (row: TData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
|
||||
onRowClick?: (
|
||||
row: TData,
|
||||
index: number,
|
||||
event: React.MouseEvent<HTMLTableRowElement> | React.KeyboardEvent<HTMLTableRowElement>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@ -94,7 +101,9 @@ export function DataTable<TData, TValue>({
|
||||
data,
|
||||
meta,
|
||||
|
||||
columnVisibility: inititalcolumnVisibility = {},
|
||||
columnVisibility,
|
||||
onColumnVisibilityChange,
|
||||
|
||||
readOnly = false,
|
||||
enablePagination = true,
|
||||
pageSize = 10,
|
||||
@ -114,11 +123,17 @@ export function DataTable<TData, TValue>({
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>(inititalcolumnVisibility);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||
|
||||
const [internalColumnVisibility, setInternalColumnVisibility] = React.useState<VisibilityState>(
|
||||
{}
|
||||
);
|
||||
|
||||
const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility;
|
||||
|
||||
const handleColumnVisibilityChange = onColumnVisibilityChange ?? setInternalColumnVisibility;
|
||||
|
||||
// Configuración TanStack
|
||||
const table = useReactTable({
|
||||
data,
|
||||
@ -137,7 +152,7 @@ export function DataTable<TData, TValue>({
|
||||
state: {
|
||||
columnSizing: colSizes,
|
||||
sorting,
|
||||
columnVisibility,
|
||||
columnVisibility: resolvedColumnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
pagination: { pageIndex, pageSize },
|
||||
@ -165,7 +180,7 @@ export function DataTable<TData, TValue>({
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
@ -229,10 +244,16 @@ export function DataTable<TData, TValue>({
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
key={row.id}
|
||||
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
onRowClick?.(row.original, rowIndex, e as any);
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
onRowClick?.(row.original, rowIndex, event);
|
||||
}}
|
||||
role={onRowClick ? "button" : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
/*
|
||||
|
||||
1
packages/rdx-ui/src/components/datatable/hooks/index.ts
Normal file
1
packages/rdx-ui/src/components/datatable/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./use-data-table-preferences.ts";
|
||||
@ -0,0 +1,91 @@
|
||||
import type { OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||
import * as React from "react";
|
||||
|
||||
export interface DataTablePreferences {
|
||||
pageSize: number;
|
||||
columnVisibility: VisibilityState;
|
||||
}
|
||||
|
||||
interface UseDataTablePreferencesParams {
|
||||
storageKey: string;
|
||||
defaultPageSize: number;
|
||||
defaultColumnVisibility?: VisibilityState;
|
||||
}
|
||||
|
||||
export const useDataTablePreferences = ({
|
||||
storageKey,
|
||||
defaultPageSize,
|
||||
defaultColumnVisibility = {},
|
||||
}: UseDataTablePreferencesParams) => {
|
||||
const [preferences, setPreferences] = React.useState<DataTablePreferences>(() => {
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(storageKey);
|
||||
|
||||
if (!rawValue) {
|
||||
return {
|
||||
pageSize: defaultPageSize,
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(rawValue) as Partial<DataTablePreferences>;
|
||||
|
||||
return {
|
||||
pageSize: parsed.pageSize ?? defaultPageSize,
|
||||
columnVisibility: {
|
||||
...defaultColumnVisibility,
|
||||
...parsed.columnVisibility,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
pageSize: defaultPageSize,
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const persist = React.useCallback(
|
||||
(next: DataTablePreferences) => {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(next));
|
||||
},
|
||||
[storageKey]
|
||||
);
|
||||
|
||||
const setPageSize = React.useCallback(
|
||||
(pageSize: number) => {
|
||||
setPreferences((previous) => {
|
||||
const next = { ...previous, pageSize };
|
||||
persist(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[persist]
|
||||
);
|
||||
|
||||
const setColumnVisibility = React.useCallback<OnChangeFn<VisibilityState>>(
|
||||
(updater) => {
|
||||
setPreferences((previous) => {
|
||||
const nextColumnVisibility =
|
||||
typeof updater === "function" ? updater(previous.columnVisibility) : updater;
|
||||
|
||||
const next = {
|
||||
...previous,
|
||||
columnVisibility: nextColumnVisibility,
|
||||
};
|
||||
|
||||
persist(next);
|
||||
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[persist]
|
||||
);
|
||||
|
||||
return {
|
||||
pageSize: preferences.pageSize,
|
||||
columnVisibility: preferences.columnVisibility,
|
||||
setPageSize,
|
||||
setColumnVisibility,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./data-table-column-header.tsx";
|
||||
export * from "./data-table.tsx";
|
||||
|
||||
export * from "./data-table-column-header.tsx";
|
||||
export * from "./hooks/index.ts";
|
||||
export * from "./skeleton-data-table.tsx";
|
||||
export * from "./with-row-selection.tsx";
|
||||
|
||||
@ -51,7 +51,18 @@ const roundToScale = (n: number, scale = 2) => {
|
||||
// Stepping teclado con redondeo a escala
|
||||
const stepNumber = (base: number, step = 0.01, scale = 2) => roundToScale(base + step, scale);
|
||||
|
||||
const parsePositiveInteger = (value: string | null, fallback: number): number => {
|
||||
const parsed = Number(value);
|
||||
|
||||
if (!Number.isInteger(parsed) || parsed < 1) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const NumberHelper = {
|
||||
parsePositiveInteger,
|
||||
toSafeNumber,
|
||||
formatNumber,
|
||||
parseLocaleNumber,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user