mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): clamp server-paginated DataTable page index when rowCount shrinks
Server-mode tables kept whatever page index the user was on after the server's total dropped below it, for example after deleting the last rows of the final page or when a refetch came back empty. The footer then read "Page 2 of 1" and "Showing 26-25 of 25" with Previous and First enabled over an empty body, and every one of the 13 server-mode consumers was exposed since none of them clamped The shared DataTable now snaps the controlled page index to the last valid page as soon as a non-loading rowCount no longer reaches it, so the fix applies to every consumer without per-table clamps. Loading responses are ignored so a pending fetch never bounces the user to page 1
This commit is contained in:
parent
f74bc9427b
commit
d05d2a6f05
2 changed files with 85 additions and 2 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import type { ColumnDef, ExpandedState } from "@tanstack/react-table";
|
||||
import type { ColumnDef, ExpandedState, OnChangeFn, PaginationState } from "@tanstack/react-table";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useState } from "react";
|
||||
|
|
@ -274,6 +274,71 @@ describe("DataTable pagination", () => {
|
|||
await user.click(screen.getByTestId("pagination-next"));
|
||||
expect(onPaginationChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
type ServerPageHarnessProps = {
|
||||
rowCount: number;
|
||||
isLoading?: boolean;
|
||||
initialPageIndex: number;
|
||||
onChange: (next: PaginationState) => void;
|
||||
};
|
||||
|
||||
function ServerPageHarness({ rowCount, isLoading = false, initialPageIndex, onChange }: ServerPageHarnessProps) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: initialPageIndex, pageSize: 10 });
|
||||
const handleChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
onChange(next);
|
||||
setPagination(next);
|
||||
};
|
||||
return (
|
||||
<DataTable
|
||||
data={[]}
|
||||
columns={nameCellColumns}
|
||||
paginationMode="server"
|
||||
pagination={pagination}
|
||||
onPaginationChange={handleChange}
|
||||
rowCount={rowCount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
it("server mode snaps to the last page when rowCount no longer reaches the current page", async () => {
|
||||
const onChange = vi.fn();
|
||||
render(<ServerPageHarness rowCount={15} initialPageIndex={2} onChange={onChange} />);
|
||||
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 10 }));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 11-15 of 15");
|
||||
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("pagination-next")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("server mode falls back to the first page when rowCount drops to zero", async () => {
|
||||
const onChange = vi.fn();
|
||||
render(<ServerPageHarness rowCount={0} initialPageIndex={2} onChange={onChange} />);
|
||||
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledWith({ pageIndex: 0, pageSize: 10 }));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId("pagination-range")).toHaveTextContent("No results");
|
||||
expect(screen.getByText("Page 1 of 1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("pagination-first")).toBeDisabled();
|
||||
expect(screen.getByTestId("pagination-prev")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("server mode leaves the page index alone while loading and clamps once the response lands", async () => {
|
||||
const onChange = vi.fn();
|
||||
const { rerender } = render(<ServerPageHarness rowCount={0} isLoading initialPageIndex={2} onChange={onChange} />);
|
||||
|
||||
expect(screen.getByText("Page 3 of 1")).toBeInTheDocument();
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
rerender(<ServerPageHarness rowCount={15} initialPageIndex={2} onChange={onChange} />);
|
||||
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 10 }));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataTable filtering", () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
getSortedRowModel,
|
||||
type Header,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type Row,
|
||||
type RowData,
|
||||
type RowSelectionState,
|
||||
|
|
@ -26,7 +27,7 @@ import {
|
|||
} from "@tanstack/react-table";
|
||||
import { SearchX } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
|
|
@ -417,6 +418,21 @@ function useControllable<T>(
|
|||
return { value: internal, onChange: setInternal };
|
||||
}
|
||||
|
||||
function useServerPageClamp(
|
||||
active: boolean,
|
||||
rowCount: number | undefined,
|
||||
pagination: { value: PaginationState; onChange: OnChangeFn<PaginationState> },
|
||||
): void {
|
||||
const { pageIndex, pageSize } = pagination.value;
|
||||
const { onChange } = pagination;
|
||||
useEffect(() => {
|
||||
if (!active || rowCount === undefined) return;
|
||||
const lastPageIndex = Math.max(Math.ceil(rowCount / pageSize) - 1, 0);
|
||||
if (pageIndex <= lastPageIndex) return;
|
||||
onChange({ pageIndex: lastPageIndex, pageSize });
|
||||
}, [active, rowCount, pageIndex, pageSize, onChange]);
|
||||
}
|
||||
|
||||
function useDataTableInstance<TData extends RowData, TValue>(
|
||||
props: DataTableResolvedProps<TData, TValue>,
|
||||
): Table<TData> {
|
||||
|
|
@ -433,6 +449,7 @@ function useDataTableInstance<TData extends RowData, TValue>(
|
|||
pagination,
|
||||
onPaginationChange,
|
||||
rowCount,
|
||||
isLoading = false,
|
||||
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
||||
filterMode = "none",
|
||||
columnFilters,
|
||||
|
|
@ -457,6 +474,7 @@ function useDataTableInstance<TData extends RowData, TValue>(
|
|||
pageIndex: 0,
|
||||
pageSize: pageSizeOptions[0] ?? 25,
|
||||
});
|
||||
useServerPageClamp(paginationMode === "server" && !isLoading, rowCount, paginationState);
|
||||
const filterState = useControllable<ColumnFiltersState>(
|
||||
columnFilters,
|
||||
onColumnFiltersChange,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue