From 3bc23a558778ebbb1fc3eac6ba66ba37ed119f90 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Fri, 27 Mar 2026 10:29:02 +0100 Subject: [PATCH] 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 --- .changeset/hip-parents-fall.md | 7 +++ docs-ui/src/app/components/table/page.mdx | 2 +- .../app/components/table/props-definition.tsx | 47 +++++++++++--- packages/ui/report.api.md | 20 +++++- .../ui/src/components/Table/hooks/types.ts | 7 ++- .../Table/hooks/useCompletePagination.ts | 22 +++++-- .../ui/src/components/Table/hooks/useTable.ts | 62 ++++++++++++------- packages/ui/src/components/Table/index.ts | 1 + 8 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 .changeset/hip-parents-fall.md diff --git a/.changeset/hip-parents-fall.md b/.changeset/hip-parents-fall.md new file mode 100644 index 0000000000..2beddba466 --- /dev/null +++ b/.changeset/hip-parents-fall.md @@ -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` diff --git a/docs-ui/src/app/components/table/page.mdx b/docs-ui/src/app/components/table/page.mdx index fc43256adb..5323804c88 100644 --- a/docs-ui/src/app/components/table/page.mdx +++ b/docs-ui/src/app/components/table/page.mdx @@ -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. diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index 77254ae0a1..6e5809412e 100644 --- a/docs-ui/src/app/components/table/props-definition.tsx +++ b/docs-ui/src/app/components/table/props-definition.tsx @@ -45,15 +45,44 @@ export const useTableOptionsPropDefs: Record = { '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 pageSize,{' '} - pageSizeOptions, initialOffset, and{' '} - showPaginationLabel. - - ), + 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: { diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index f8aa8a024f..d19fdc8c56 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -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 @@ -3162,7 +3168,17 @@ export const useBreakpoint: () => { // @public (undocumented) export function useTable( - options: UseTableOptions, + options: UseTableCompleteOptions, +): UseTableResult; + +// @public (undocumented) +export function useTable( + options: UseTableOffsetOptions, +): UseTableResult; + +// @public (undocumented) +export function useTable( + options: UseTableCursorOptions, ): UseTableResult; // @public (undocumented) @@ -3171,7 +3187,7 @@ export type UseTableCompleteOptions< TFilter = unknown, > = QueryOptions & { mode: 'complete'; - paginationOptions?: PaginationOptions; + paginationOptions?: CompletePaginationOptions; sortFn?: (data: T[], sort: SortDescriptor) => T[]; filterFn?: (data: T[], filter: TFilter) => T[]; searchFn?: (data: T[], search: string) => T[]; diff --git a/packages/ui/src/components/Table/hooks/types.ts b/packages/ui/src/components/Table/hooks/types.ts index 1c69cc7591..06c980efdf 100644 --- a/packages/ui/src/components/Table/hooks/types.ts +++ b/packages/ui/src/components/Table/hooks/types.ts @@ -62,6 +62,11 @@ export interface PaginationOptions initialOffset?: number; } +/** @public */ +export interface CompletePaginationOptions extends PaginationOptions { + type?: 'page' | 'none'; +} + /** @public */ export interface OffsetParams { offset: number; @@ -102,7 +107,7 @@ export type UseTableCompleteOptions< TFilter = unknown, > = QueryOptions & { mode: 'complete'; - paginationOptions?: PaginationOptions; + paginationOptions?: CompletePaginationOptions; sortFn?: (data: T[], sort: SortDescriptor) => T[]; filterFn?: (data: T[], filter: TFilter) => T[]; searchFn?: (data: T[], search: string) => T[]; diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts index e382cf6413..f34e2c8c06 100644 --- a/packages/ui/src/components/Table/hooks/useCompletePagination.ts +++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts @@ -38,8 +38,11 @@ export function useCompletePagination( 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( 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( // 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) { diff --git a/packages/ui/src/components/Table/hooks/useTable.ts b/packages/ui/src/components/Table/hooks/useTable.ts index de70534280..5997416915 100644 --- a/packages/ui/src/components/Table/hooks/useTable.ts +++ b/packages/ui/src/components/Table/hooks/useTable.ts @@ -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( paginationResult: PaginationResult, sortState: SortState, - paginationOptions: PaginationOptions = {}, + paginationOptions: UseTableCompleteOptions['paginationOptions'] = {}, ): Omit< TableProps, 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState' @@ -52,8 +54,11 @@ function useTableProps( 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( 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( } /** @public */ +export function useTable( + options: UseTableCompleteOptions, +): UseTableResult; +/** @public */ +export function useTable( + options: UseTableOffsetOptions, +): UseTableResult; +/** @public */ +export function useTable( + options: UseTableCursorOptions, +): UseTableResult; export function useTable( options: UseTableOptions, ): UseTableResult { diff --git a/packages/ui/src/components/Table/index.ts b/packages/ui/src/components/Table/index.ts index 033d51cdaa..d2847fae38 100644 --- a/packages/ui/src/components/Table/index.ts +++ b/packages/ui/src/components/Table/index.ts @@ -69,6 +69,7 @@ export type { SearchState, QueryOptions, PaginationOptions, + CompletePaginationOptions, } from './hooks/types'; export { TableDefinition } from './definition';