feat(ui): support disabling pagination in useTable complete mode

Add `CompletePaginationOptions` type extending `PaginationOptions`
with a `type` field supporting `'page'` (default) and `'none'`.
When using `mode: 'complete'` with `type: 'none'`, `useTable` skips
data slicing and produces `pagination: { type: 'none' }` in
`tableProps` directly.

Also sync `pageSize` state when `paginationOptions.pageSize` changes
dynamically, fixing cases where the initial value became stale.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-03-27 10:29:02 +01:00
parent 40c8ec9615
commit 3bc23a5587
8 changed files with 127 additions and 41 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added support for disabling pagination in `useTable` complete mode by setting `paginationOptions: { type: 'none' }`. This skips data slicing and produces `pagination: { type: 'none' }` in `tableProps`, removing the need for consumers to manually override the pagination prop on `Table`. Also fixed complete mode not reacting to dynamic changes in `paginationOptions.pageSize`.
**Affected components:** `useTable`
+1 -1
View File
@@ -119,7 +119,7 @@ With `mode: 'complete'`, sorting happens client-side. Provide a `sortFn` that re
### Pagination
Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically.
Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically. In `complete` mode, set `type: 'none'` to disable pagination and show all rows.
<CodeBlock code={tablePaginationSnippet} />
@@ -45,15 +45,44 @@ export const useTableOptionsPropDefs: Record<string, PropDef> = {
'The data for the table. Only applicable for "complete" mode, and either this or `getData` must be provided.',
},
paginationOptions: {
type: 'enum',
values: ['object'],
description: (
<>
Pagination configuration including <Chip>pageSize</Chip>,{' '}
<Chip>pageSizeOptions</Chip>, <Chip>initialOffset</Chip>, and{' '}
<Chip>showPaginationLabel</Chip>.
</>
),
type: 'complex',
description: 'Pagination configuration.',
complexType: {
name: 'PaginationOptions',
properties: {
type: {
type: "'page' | 'none'",
description:
"Pagination mode. Set to 'none' to disable pagination and show all rows (complete mode only). Defaults to 'page'.",
},
pageSize: {
type: 'number',
description: 'Number of items per page. Defaults to 20.',
},
pageSizeOptions: {
type: 'number[]',
description: 'Available page size options for the dropdown.',
},
initialOffset: {
type: 'number',
description: 'Starting offset for the first page.',
},
showPageSizeOptions: {
type: 'boolean',
description:
'Whether to show the page size dropdown. Defaults to true.',
},
showPaginationLabel: {
type: 'boolean',
description:
"Whether to display the pagination label (e.g., '1 - 20 of 150').",
},
getLabel: {
type: '(props) => string',
description: 'Custom function to generate the pagination label text.',
},
},
},
},
// Uncontrolled state
initialSort: {
+18 -2
View File
@@ -906,6 +906,12 @@ export type Columns =
| '12'
| 'auto';
// @public (undocumented)
export interface CompletePaginationOptions extends PaginationOptions {
// (undocumented)
type?: 'page' | 'none';
}
// @public (undocumented)
export const Container: ForwardRefExoticComponent<
ContainerProps & RefAttributes<HTMLDivElement>
@@ -3162,7 +3168,17 @@ export const useBreakpoint: () => {
// @public (undocumented)
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableOptions<T, TFilter>,
options: UseTableCompleteOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
// @public (undocumented)
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableOffsetOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
// @public (undocumented)
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableCursorOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
// @public (undocumented)
@@ -3171,7 +3187,7 @@ export type UseTableCompleteOptions<
TFilter = unknown,
> = QueryOptions<TFilter> & {
mode: 'complete';
paginationOptions?: PaginationOptions;
paginationOptions?: CompletePaginationOptions;
sortFn?: (data: T[], sort: SortDescriptor) => T[];
filterFn?: (data: T[], filter: TFilter) => T[];
searchFn?: (data: T[], search: string) => T[];
@@ -62,6 +62,11 @@ export interface PaginationOptions
initialOffset?: number;
}
/** @public */
export interface CompletePaginationOptions extends PaginationOptions {
type?: 'page' | 'none';
}
/** @public */
export interface OffsetParams<TFilter> {
offset: number;
@@ -102,7 +107,7 @@ export type UseTableCompleteOptions<
TFilter = unknown,
> = QueryOptions<TFilter> & {
mode: 'complete';
paginationOptions?: PaginationOptions;
paginationOptions?: CompletePaginationOptions;
sortFn?: (data: T[], sort: SortDescriptor) => T[];
filterFn?: (data: T[], filter: TFilter) => T[];
searchFn?: (data: T[], search: string) => T[];
@@ -38,8 +38,11 @@ export function useCompletePagination<T extends TableItem, TFilter>(
searchFn,
} = options;
const hasGetData = 'getData' in options;
const noPagination = paginationOptions.type === 'none';
const { initialOffset = 0 } = paginationOptions;
const defaultPageSize = getEffectivePageSize(paginationOptions);
const defaultPageSize = noPagination
? Infinity
: getEffectivePageSize(paginationOptions);
const getData = useStableCallback(getDataProp);
const { sort, filter, search } = query;
@@ -52,6 +55,12 @@ export function useCompletePagination<T extends TableItem, TFilter>(
const [offset, setOffset] = useState(initialOffset);
const [pageSize, setPageSize] = useState(defaultPageSize);
// Sync pageSize when the caller changes paginationOptions.pageSize
useEffect(() => {
setPageSize(defaultPageSize);
setOffset(0);
}, [defaultPageSize]);
// Load data on mount and when loadCount changes (reload trigger)
useEffect(() => {
if (data) {
@@ -121,12 +130,15 @@ export function useCompletePagination<T extends TableItem, TFilter>(
// Paginate the processed data
const paginatedData = useMemo(
() => processedData?.slice(offset, offset + pageSize),
[processedData, offset, pageSize],
() =>
noPagination
? processedData
: processedData?.slice(offset, offset + pageSize),
[processedData, offset, pageSize, noPagination],
);
const hasNextPage = offset + pageSize < totalCount;
const hasPreviousPage = offset > 0;
const hasNextPage = !noPagination && offset + pageSize < totalCount;
const hasPreviousPage = !noPagination && offset > 0;
const onNextPage = useCallback(() => {
if (offset + pageSize < totalCount) {
@@ -16,8 +16,10 @@
import { useMemo, useRef } from 'react';
import type { SortState, TableItem, TableProps } from '../types';
import type {
PaginationOptions,
PaginationResult,
UseTableCompleteOptions,
UseTableCursorOptions,
UseTableOffsetOptions,
UseTableOptions,
UseTableResult,
} from './types';
@@ -29,7 +31,7 @@ import { useOffsetPagination } from './useOffsetPagination';
function useTableProps<T extends TableItem>(
paginationResult: PaginationResult<T>,
sortState: SortState,
paginationOptions: PaginationOptions = {},
paginationOptions: UseTableCompleteOptions<T>['paginationOptions'] = {},
): Omit<
TableProps<T>,
'columnConfig' | 'rowConfig' | 'selection' | 'emptyState'
@@ -52,8 +54,11 @@ function useTableProps<T extends TableItem>(
const displayData = paginationResult.data ?? previousDataRef.current;
const isStale = paginationResult.loading && displayData !== undefined;
const pagination = useMemo(
() => ({
const pagination = useMemo(() => {
if (paginationOptions.type === 'none') {
return { type: 'none' as const };
}
return {
type: 'page' as const,
pageSize: paginationResult.pageSize,
pageSizeOptions,
@@ -76,25 +81,25 @@ function useTableProps<T extends TableItem>(
showPageSizeOptions,
getLabel,
showPaginationLabel,
}),
[
paginationResult.pageSize,
pageSizeOptions,
paginationResult.offset,
paginationResult.totalCount,
paginationResult.hasNextPage,
paginationResult.hasPreviousPage,
paginationResult.onNextPage,
paginationResult.onPreviousPage,
paginationResult.onPageSizeChange,
onNextPageCallback,
onPreviousPageCallback,
onPageSizeChangeCallback,
showPageSizeOptions,
getLabel,
showPaginationLabel,
],
);
};
}, [
paginationOptions.type,
paginationResult.pageSize,
pageSizeOptions,
paginationResult.offset,
paginationResult.totalCount,
paginationResult.hasNextPage,
paginationResult.hasPreviousPage,
paginationResult.onNextPage,
paginationResult.onPreviousPage,
paginationResult.onPageSizeChange,
onNextPageCallback,
onPreviousPageCallback,
onPageSizeChangeCallback,
showPageSizeOptions,
getLabel,
showPaginationLabel,
]);
return useMemo(
() => ({
@@ -117,6 +122,17 @@ function useTableProps<T extends TableItem>(
}
/** @public */
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableCompleteOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
/** @public */
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableOffsetOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
/** @public */
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableCursorOptions<T, TFilter>,
): UseTableResult<T, TFilter>;
export function useTable<T extends TableItem, TFilter = unknown>(
options: UseTableOptions<T, TFilter>,
): UseTableResult<T, TFilter> {
@@ -69,6 +69,7 @@ export type {
SearchState,
QueryOptions,
PaginationOptions,
CompletePaginationOptions,
} from './hooks/types';
export { TableDefinition } from './definition';