@backstage/ui 0.18.0-next.1 → 0.18.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @backstage/ui
2
2
 
3
+ ## 0.18.0-next.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 0dd80f7: Fixed pagination state handling in `useTable`, including initial offsets, shrinking complete datasets, controlled loading transitions, valid zero and empty cursors, cached error recovery, immediately resolving reloads, and unnecessary reloads when controlled callback identities change.
8
+
9
+ **Affected components:** `useTable`
10
+
3
11
  ## 0.18.0-next.1
4
12
 
5
13
  ### Minor Changes
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
1
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
2
2
  import { useStableCallback } from './useStableCallback.esm.js';
3
3
  import { useDebouncedValue } from './useDebouncedValue.esm.js';
4
4
  import { getEffectivePageSize } from './getEffectivePageSize.esm.js';
@@ -26,9 +26,13 @@ function useCompletePagination(options, query) {
26
26
  const [loadCount, setLoadCount] = useState(0);
27
27
  const [offset, setOffset] = useState(initialOffset);
28
28
  const [pageSize, setPageSize] = useState(defaultPageSize);
29
+ const previousDefaultPageSize = useRef(defaultPageSize);
29
30
  useEffect(() => {
30
- setPageSize(defaultPageSize);
31
- setOffset(0);
31
+ if (previousDefaultPageSize.current !== defaultPageSize) {
32
+ previousDefaultPageSize.current = defaultPageSize;
33
+ setPageSize(defaultPageSize);
34
+ setOffset(0);
35
+ }
32
36
  }, [defaultPageSize]);
33
37
  useEffect(() => {
34
38
  if (data) {
@@ -36,6 +40,7 @@ function useCompletePagination(options, query) {
36
40
  return;
37
41
  }
38
42
  if (!hasGetData) {
43
+ setIsPending(true);
39
44
  return;
40
45
  }
41
46
  let cancelled = false;
@@ -98,23 +103,48 @@ function useCompletePagination(options, query) {
98
103
  searchFn,
99
104
  sortFn
100
105
  ]);
101
- const totalCount = processedData?.length ?? 0;
106
+ const previousProcessedDataRef = useRef(processedData);
107
+ if (processedData !== void 0) {
108
+ previousProcessedDataRef.current = processedData;
109
+ }
110
+ const retainedProcessedData = processedData ?? previousProcessedDataRef.current;
111
+ const totalCount = retainedProcessedData?.length ?? 0;
112
+ const effectiveOffset = useMemo(() => {
113
+ if (noPagination) {
114
+ return 0;
115
+ }
116
+ if (retainedProcessedData === void 0) {
117
+ return offset;
118
+ }
119
+ if (totalCount === 0) {
120
+ return 0;
121
+ }
122
+ return Math.min(offset, Math.floor((totalCount - 1) / pageSize) * pageSize);
123
+ }, [noPagination, offset, pageSize, retainedProcessedData, totalCount]);
124
+ useEffect(() => {
125
+ if (offset !== effectiveOffset) {
126
+ setOffset(effectiveOffset);
127
+ }
128
+ }, [effectiveOffset, offset]);
102
129
  const paginatedData = useMemo(
103
- () => noPagination ? processedData : processedData?.slice(offset, offset + pageSize),
104
- [processedData, offset, pageSize, noPagination]
130
+ () => noPagination ? retainedProcessedData : retainedProcessedData?.slice(
131
+ effectiveOffset,
132
+ effectiveOffset + pageSize
133
+ ),
134
+ [retainedProcessedData, effectiveOffset, pageSize, noPagination]
105
135
  );
106
- const hasNextPage = !noPagination && offset + pageSize < totalCount;
107
- const hasPreviousPage = !noPagination && offset > 0;
136
+ const hasNextPage = !noPagination && effectiveOffset + pageSize < totalCount;
137
+ const hasPreviousPage = !noPagination && effectiveOffset > 0;
108
138
  const onNextPage = useCallback(() => {
109
- if (offset + pageSize < totalCount) {
110
- setOffset(offset + pageSize);
139
+ if (effectiveOffset + pageSize < totalCount) {
140
+ setOffset(effectiveOffset + pageSize);
111
141
  }
112
- }, [offset, pageSize, totalCount]);
142
+ }, [effectiveOffset, pageSize, totalCount]);
113
143
  const onPreviousPage = useCallback(() => {
114
- if (offset > 0) {
115
- setOffset(Math.max(0, offset - pageSize));
144
+ if (effectiveOffset > 0) {
145
+ setOffset(Math.max(0, effectiveOffset - pageSize));
116
146
  }
117
- }, [offset, pageSize]);
147
+ }, [effectiveOffset, pageSize]);
118
148
  const onPageSizeChange = useCallback((newSize) => {
119
149
  setPageSize(newSize);
120
150
  setOffset(0);
@@ -128,7 +158,7 @@ function useCompletePagination(options, query) {
128
158
  isPending,
129
159
  error,
130
160
  totalCount,
131
- offset,
161
+ offset: effectiveOffset,
132
162
  pageSize,
133
163
  hasNextPage,
134
164
  hasPreviousPage,
@@ -1 +1 @@
1
- {"version":3,"file":"useCompletePagination.esm.js","sources":["../../../../src/components/Table/hooks/useCompletePagination.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport type { TableItem } from '../types';\nimport type {\n PaginationResult,\n QueryState,\n UseTableCompleteOptions,\n} from './types';\nimport { useStableCallback } from './useStableCallback';\nimport { useDebouncedValue } from './useDebouncedValue';\nimport { getEffectivePageSize } from './getEffectivePageSize';\n\n/** @internal */\nexport function useCompletePagination<T extends TableItem, TFilter>(\n options: UseTableCompleteOptions<T, TFilter>,\n query: QueryState<TFilter>,\n): PaginationResult<T> & { reload: () => void } {\n const {\n data,\n getData: getDataProp = () => [],\n paginationOptions = {},\n sortFn,\n filterFn,\n searchFn,\n searchDebounceMs = 0,\n filterDebounceMs = 0,\n } = options;\n const hasGetData = 'getData' in options;\n const noPagination = paginationOptions.type === 'none';\n const { initialOffset = 0 } = paginationOptions;\n const defaultPageSize = noPagination\n ? Infinity\n : getEffectivePageSize(paginationOptions);\n\n const getData = useStableCallback(getDataProp);\n const { sort, filter, search } = query;\n\n const [items, setItems] = useState<T[] | undefined>(undefined);\n const [isPending, setIsPending] = useState(!data);\n const [error, setError] = useState<Error | undefined>(undefined);\n const [loadCount, setLoadCount] = useState(0);\n\n const [offset, setOffset] = useState(initialOffset);\n const [pageSize, setPageSize] = useState(defaultPageSize);\n\n // Sync pageSize when the caller changes paginationOptions.pageSize\n useEffect(() => {\n setPageSize(defaultPageSize);\n setOffset(0);\n }, [defaultPageSize]);\n\n // Load data on mount and when loadCount changes (reload trigger)\n useEffect(() => {\n if (data) {\n setIsPending(false);\n return;\n }\n\n if (!hasGetData) {\n return;\n }\n\n let cancelled = false;\n setIsPending(true);\n setError(undefined);\n\n (async () => {\n try {\n const result = getData();\n const resolvedData = result instanceof Promise ? await result : result;\n if (!cancelled) {\n setItems(resolvedData);\n setIsPending(false);\n }\n } catch (err) {\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsPending(false);\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [data, getData, hasGetData, loadCount]);\n\n // Debounced surrogates of search and filter feed the processing pipeline.\n // At delayMs === 0 (the default) these are referentially equal to the live\n // values, so behavior is identical to before this refactor.\n const debouncedSearch = useDebouncedValue(search, searchDebounceMs);\n const debouncedFilter = useDebouncedValue(filter, filterDebounceMs);\n\n // Reset offset when the *debounced* query changes — keying on the live query\n // would briefly flash page 1 of unfiltered data while the debounce settles.\n const debouncedQuery = useMemo(\n () => ({ sort, filter: debouncedFilter, search: debouncedSearch }),\n [sort, debouncedFilter, debouncedSearch],\n );\n const prevDebouncedQueryRef = useRef(debouncedQuery);\n useEffect(() => {\n if (prevDebouncedQueryRef.current !== debouncedQuery) {\n prevDebouncedQueryRef.current = debouncedQuery;\n setOffset(0);\n }\n }, [debouncedQuery]);\n\n const resolvedItems = useMemo(() => data ?? items, [data, items]);\n\n // Process data client-side (filter, search, sort)\n const processedData = useMemo(() => {\n if (!resolvedItems) {\n return undefined;\n }\n let result = [...resolvedItems];\n if (debouncedFilter !== undefined && filterFn) {\n result = filterFn(result, debouncedFilter);\n }\n if (debouncedSearch && searchFn) {\n result = searchFn(result, debouncedSearch);\n }\n if (sort && sortFn) {\n result = sortFn(result, sort);\n }\n return result;\n }, [\n resolvedItems,\n sort,\n debouncedFilter,\n debouncedSearch,\n filterFn,\n searchFn,\n sortFn,\n ]);\n\n const totalCount = processedData?.length ?? 0;\n\n // Paginate the processed data\n const paginatedData = useMemo(\n () =>\n noPagination\n ? processedData\n : processedData?.slice(offset, offset + pageSize),\n [processedData, offset, pageSize, noPagination],\n );\n\n const hasNextPage = !noPagination && offset + pageSize < totalCount;\n const hasPreviousPage = !noPagination && offset > 0;\n\n const onNextPage = useCallback(() => {\n if (offset + pageSize < totalCount) {\n setOffset(offset + pageSize);\n }\n }, [offset, pageSize, totalCount]);\n\n const onPreviousPage = useCallback(() => {\n if (offset > 0) {\n setOffset(Math.max(0, offset - pageSize));\n }\n }, [offset, pageSize]);\n\n const onPageSizeChange = useCallback((newSize: number) => {\n setPageSize(newSize);\n setOffset(0);\n }, []);\n\n const reload = useCallback(() => {\n setOffset(0);\n setLoadCount(c => c + 1);\n }, []);\n\n return {\n data: paginatedData,\n isPending: isPending,\n error,\n totalCount,\n offset,\n pageSize,\n hasNextPage,\n hasPreviousPage,\n onNextPage,\n onPreviousPage,\n onPageSizeChange,\n reload,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useCompletePagination.esm.js","sources":["../../../../src/components/Table/hooks/useCompletePagination.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport type { TableItem } from '../types';\nimport type {\n PaginationResult,\n QueryState,\n UseTableCompleteOptions,\n} from './types';\nimport { useStableCallback } from './useStableCallback';\nimport { useDebouncedValue } from './useDebouncedValue';\nimport { getEffectivePageSize } from './getEffectivePageSize';\n\n/** @internal */\nexport function useCompletePagination<T extends TableItem, TFilter>(\n options: UseTableCompleteOptions<T, TFilter>,\n query: QueryState<TFilter>,\n): PaginationResult<T> & { reload: () => void } {\n const {\n data,\n getData: getDataProp = () => [],\n paginationOptions = {},\n sortFn,\n filterFn,\n searchFn,\n searchDebounceMs = 0,\n filterDebounceMs = 0,\n } = options;\n const hasGetData = 'getData' in options;\n const noPagination = paginationOptions.type === 'none';\n const { initialOffset = 0 } = paginationOptions;\n const defaultPageSize = noPagination\n ? Infinity\n : getEffectivePageSize(paginationOptions);\n\n const getData = useStableCallback(getDataProp);\n const { sort, filter, search } = query;\n\n const [items, setItems] = useState<T[] | undefined>(undefined);\n const [isPending, setIsPending] = useState(!data);\n const [error, setError] = useState<Error | undefined>(undefined);\n const [loadCount, setLoadCount] = useState(0);\n\n const [offset, setOffset] = useState(initialOffset);\n const [pageSize, setPageSize] = useState(defaultPageSize);\n\n // Sync pageSize when the caller changes paginationOptions.pageSize\n const previousDefaultPageSize = useRef(defaultPageSize);\n useEffect(() => {\n if (previousDefaultPageSize.current !== defaultPageSize) {\n previousDefaultPageSize.current = defaultPageSize;\n setPageSize(defaultPageSize);\n setOffset(0);\n }\n }, [defaultPageSize]);\n\n // Load data on mount and when loadCount changes (reload trigger)\n useEffect(() => {\n if (data) {\n setIsPending(false);\n return;\n }\n\n if (!hasGetData) {\n setIsPending(true);\n return;\n }\n\n let cancelled = false;\n setIsPending(true);\n setError(undefined);\n\n (async () => {\n try {\n const result = getData();\n const resolvedData = result instanceof Promise ? await result : result;\n if (!cancelled) {\n setItems(resolvedData);\n setIsPending(false);\n }\n } catch (err) {\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsPending(false);\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [data, getData, hasGetData, loadCount]);\n\n // Debounced surrogates of search and filter feed the processing pipeline.\n // At delayMs === 0 (the default) these are referentially equal to the live\n // values, so behavior is identical to before this refactor.\n const debouncedSearch = useDebouncedValue(search, searchDebounceMs);\n const debouncedFilter = useDebouncedValue(filter, filterDebounceMs);\n\n // Reset offset when the *debounced* query changes — keying on the live query\n // would briefly flash page 1 of unfiltered data while the debounce settles.\n const debouncedQuery = useMemo(\n () => ({ sort, filter: debouncedFilter, search: debouncedSearch }),\n [sort, debouncedFilter, debouncedSearch],\n );\n const prevDebouncedQueryRef = useRef(debouncedQuery);\n useEffect(() => {\n if (prevDebouncedQueryRef.current !== debouncedQuery) {\n prevDebouncedQueryRef.current = debouncedQuery;\n setOffset(0);\n }\n }, [debouncedQuery]);\n\n const resolvedItems = useMemo(() => data ?? items, [data, items]);\n\n // Process data client-side (filter, search, sort)\n const processedData = useMemo(() => {\n if (!resolvedItems) {\n return undefined;\n }\n let result = [...resolvedItems];\n if (debouncedFilter !== undefined && filterFn) {\n result = filterFn(result, debouncedFilter);\n }\n if (debouncedSearch && searchFn) {\n result = searchFn(result, debouncedSearch);\n }\n if (sort && sortFn) {\n result = sortFn(result, sort);\n }\n return result;\n }, [\n resolvedItems,\n sort,\n debouncedFilter,\n debouncedSearch,\n filterFn,\n searchFn,\n sortFn,\n ]);\n\n const previousProcessedDataRef = useRef(processedData);\n if (processedData !== undefined) {\n previousProcessedDataRef.current = processedData;\n }\n const retainedProcessedData =\n processedData ?? previousProcessedDataRef.current;\n\n const totalCount = retainedProcessedData?.length ?? 0;\n\n const effectiveOffset = useMemo(() => {\n if (noPagination) {\n return 0;\n }\n if (retainedProcessedData === undefined) {\n return offset;\n }\n if (totalCount === 0) {\n return 0;\n }\n return Math.min(offset, Math.floor((totalCount - 1) / pageSize) * pageSize);\n }, [noPagination, offset, pageSize, retainedProcessedData, totalCount]);\n\n // Persist the corrected offset so later data growth does not restore it.\n useEffect(() => {\n if (offset !== effectiveOffset) {\n setOffset(effectiveOffset);\n }\n }, [effectiveOffset, offset]);\n\n // Paginate the processed data\n const paginatedData = useMemo(\n () =>\n noPagination\n ? retainedProcessedData\n : retainedProcessedData?.slice(\n effectiveOffset,\n effectiveOffset + pageSize,\n ),\n [retainedProcessedData, effectiveOffset, pageSize, noPagination],\n );\n\n const hasNextPage = !noPagination && effectiveOffset + pageSize < totalCount;\n const hasPreviousPage = !noPagination && effectiveOffset > 0;\n\n const onNextPage = useCallback(() => {\n if (effectiveOffset + pageSize < totalCount) {\n setOffset(effectiveOffset + pageSize);\n }\n }, [effectiveOffset, pageSize, totalCount]);\n\n const onPreviousPage = useCallback(() => {\n if (effectiveOffset > 0) {\n setOffset(Math.max(0, effectiveOffset - pageSize));\n }\n }, [effectiveOffset, pageSize]);\n\n const onPageSizeChange = useCallback((newSize: number) => {\n setPageSize(newSize);\n setOffset(0);\n }, []);\n\n const reload = useCallback(() => {\n setOffset(0);\n setLoadCount(c => c + 1);\n }, []);\n\n return {\n data: paginatedData,\n isPending: isPending,\n error,\n totalCount,\n offset: effectiveOffset,\n pageSize,\n hasNextPage,\n hasPreviousPage,\n onNextPage,\n onPreviousPage,\n onPageSizeChange,\n reload,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1,16 +1,17 @@
1
1
  import { useRef, useEffect } from 'react';
2
2
 
3
3
  function useDebouncedReload(query, pageSize, reload, delay = 200) {
4
- const prevDepsRef = useRef({ query, pageSize });
4
+ const { sort, filter, search } = query;
5
+ const prevDepsRef = useRef({ sort, filter, search, pageSize });
5
6
  useEffect(() => {
6
7
  const prev = prevDepsRef.current;
7
- if (prev.query !== query || prev.pageSize !== pageSize) {
8
- prevDepsRef.current = { query, pageSize };
8
+ if (prev.sort !== sort || prev.filter !== filter || prev.search !== search || prev.pageSize !== pageSize) {
9
+ prevDepsRef.current = { sort, filter, search, pageSize };
9
10
  const timer = setTimeout(reload, delay);
10
11
  return () => clearTimeout(timer);
11
12
  }
12
13
  return void 0;
13
- }, [query, pageSize, reload, delay]);
14
+ }, [sort, filter, search, pageSize, reload, delay]);
14
15
  }
15
16
 
16
17
  export { useDebouncedReload };
@@ -1 +1 @@
1
- {"version":3,"file":"useDebouncedReload.esm.js","sources":["../../../../src/components/Table/hooks/useDebouncedReload.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useEffect, useRef } from 'react';\nimport type { QueryState } from './types';\n\n/**\n * Triggers a debounced reload when query or pageSize changes.\n * Debouncing reduces backend load during rapid changes (e.g., typing in search).\n */\n/** @internal */\nexport function useDebouncedReload<TFilter>(\n query: QueryState<TFilter>,\n pageSize: number,\n reload: () => void,\n delay: number = 200,\n): void {\n const prevDepsRef = useRef({ query, pageSize });\n\n useEffect(() => {\n const prev = prevDepsRef.current;\n if (prev.query !== query || prev.pageSize !== pageSize) {\n prevDepsRef.current = { query, pageSize };\n const timer = setTimeout(reload, delay);\n return () => clearTimeout(timer);\n }\n return undefined;\n }, [query, pageSize, reload, delay]);\n}\n"],"names":[],"mappings":";;AAwBO,SAAS,kBAAA,CACd,KAAA,EACA,QAAA,EACA,MAAA,EACA,QAAgB,GAAA,EACV;AACN,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,EAAE,KAAA,EAAO,UAAU,CAAA;AAE9C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,OAAO,WAAA,CAAY,OAAA;AACzB,IAAA,IAAI,IAAA,CAAK,KAAA,KAAU,KAAA,IAAS,IAAA,CAAK,aAAa,QAAA,EAAU;AACtD,MAAA,WAAA,CAAY,OAAA,GAAU,EAAE,KAAA,EAAO,QAAA,EAAS;AACxC,MAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAA,EAAQ,KAAK,CAAA;AACtC,MAAA,OAAO,MAAM,aAAa,KAAK,CAAA;AAAA,IACjC;AACA,IAAA,OAAO,MAAA;AAAA,EACT,GAAG,CAAC,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,KAAK,CAAC,CAAA;AACrC;;;;"}
1
+ {"version":3,"file":"useDebouncedReload.esm.js","sources":["../../../../src/components/Table/hooks/useDebouncedReload.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useEffect, useRef } from 'react';\nimport type { QueryState } from './types';\n\n/**\n * Triggers a debounced reload when query or pageSize changes.\n * Debouncing reduces backend load during rapid changes (e.g., typing in search).\n */\n/** @internal */\nexport function useDebouncedReload<TFilter>(\n query: QueryState<TFilter>,\n pageSize: number,\n reload: () => void,\n delay: number = 200,\n): void {\n const { sort, filter, search } = query;\n const prevDepsRef = useRef({ sort, filter, search, pageSize });\n\n useEffect(() => {\n const prev = prevDepsRef.current;\n if (\n prev.sort !== sort ||\n prev.filter !== filter ||\n prev.search !== search ||\n prev.pageSize !== pageSize\n ) {\n prevDepsRef.current = { sort, filter, search, pageSize };\n const timer = setTimeout(reload, delay);\n return () => clearTimeout(timer);\n }\n return undefined;\n }, [sort, filter, search, pageSize, reload, delay]);\n}\n"],"names":[],"mappings":";;AAwBO,SAAS,kBAAA,CACd,KAAA,EACA,QAAA,EACA,MAAA,EACA,QAAgB,GAAA,EACV;AACN,EAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAA,EAAO,GAAI,KAAA;AACjC,EAAA,MAAM,cAAc,MAAA,CAAO,EAAE,MAAM,MAAA,EAAQ,MAAA,EAAQ,UAAU,CAAA;AAE7D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,OAAO,WAAA,CAAY,OAAA;AACzB,IAAA,IACE,IAAA,CAAK,IAAA,KAAS,IAAA,IACd,IAAA,CAAK,MAAA,KAAW,MAAA,IAChB,IAAA,CAAK,MAAA,KAAW,MAAA,IAChB,IAAA,CAAK,QAAA,KAAa,QAAA,EAClB;AACA,MAAA,WAAA,CAAY,OAAA,GAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,QAAQ,QAAA,EAAS;AACvD,MAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAA,EAAQ,KAAK,CAAA;AACtC,MAAA,OAAO,MAAM,aAAa,KAAK,CAAA;AAAA,IACjC;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,EAAG,CAAC,IAAA,EAAM,MAAA,EAAQ,QAAQ,QAAA,EAAU,MAAA,EAAQ,KAAK,CAAC,CAAA;AACpD;;;;"}
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useCallback, useEffect } from 'react';
1
+ import { useState, useRef, useReducer, useCallback, useEffect } from 'react';
2
2
 
3
3
  const FIRST_PAGE_CURSOR = /* @__PURE__ */ Symbol("firstPage");
4
4
  class PageCacheStore {
@@ -61,6 +61,7 @@ function usePageCache(options) {
61
61
  const [isPending, setIsPending] = useState(true);
62
62
  const [error, setError] = useState(void 0);
63
63
  const [totalCount, setTotalCount] = useState(void 0);
64
+ const [, notifyCacheUpdate] = useReducer((version) => version + 1, 0);
64
65
  const abortControllerRef = useRef(null);
65
66
  const currentPage = cacheStore.get(currentCursor);
66
67
  const data = currentPage?.data;
@@ -73,12 +74,13 @@ function usePageCache(options) {
73
74
  currentCursor,
74
75
  initialCurrentCursor
75
76
  );
76
- if (!targetCursor) {
77
+ if (targetCursor === void 0) {
77
78
  return;
78
79
  }
79
80
  const existingEntry = cacheStore.get(targetCursor);
80
81
  if (existingEntry?.data !== void 0) {
81
82
  setCurrentCursor(targetCursor);
83
+ setError(void 0);
82
84
  return;
83
85
  }
84
86
  const entry = cacheStore.getOrCreate(targetCursor);
@@ -109,6 +111,7 @@ function usePageCache(options) {
109
111
  if (result.totalCount !== void 0) {
110
112
  setTotalCount(result.totalCount);
111
113
  }
114
+ notifyCacheUpdate();
112
115
  setIsPending(false);
113
116
  } catch (err) {
114
117
  if (abortController.signal.aborted) {
@@ -131,13 +134,13 @@ function usePageCache(options) {
131
134
  const onNextPage = useCallback(() => {
132
135
  if (isPending) return;
133
136
  const page = cacheStore.get(currentCursor);
134
- if (!page?.nextCursor) return;
137
+ if (page?.nextCursor === void 0) return;
135
138
  goToPage("next");
136
139
  }, [isPending, currentCursor, goToPage, cacheStore]);
137
140
  const onPreviousPage = useCallback(() => {
138
141
  if (isPending) return;
139
142
  const page = cacheStore.get(currentCursor);
140
- if (!page?.prevCursor) return;
143
+ if (page?.prevCursor === void 0) return;
141
144
  goToPage("prev");
142
145
  }, [isPending, currentCursor, goToPage, cacheStore]);
143
146
  const reload = useCallback(
@@ -1 +1 @@
1
- {"version":3,"file":"usePageCache.esm.js","sources":["../../../../src/components/Table/hooks/usePageCache.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useState, useCallback, useRef, useEffect } from 'react';\n\nconst FIRST_PAGE_CURSOR = Symbol('firstPage');\n\ntype CursorType = string | number;\n\ntype InternalCursor<TCursor extends CursorType> =\n | TCursor\n | typeof FIRST_PAGE_CURSOR;\n\ninterface PageEntry<T, TCursor extends CursorType> {\n data: T[] | undefined;\n nextCursor: InternalCursor<TCursor> | undefined;\n prevCursor: InternalCursor<TCursor> | undefined;\n}\n\ninterface GetDataResult<T, TCursor extends CursorType> {\n data: T[];\n nextCursor?: TCursor;\n prevCursor?: TCursor;\n totalCount?: number;\n}\n\n/** @internal */\nexport interface UsePageCacheOptions<T, TCursor extends CursorType = string> {\n getData: (params: {\n cursor: TCursor | undefined;\n signal: AbortSignal;\n }) => Promise<GetDataResult<T, TCursor>>;\n initialCurrentCursor?: TCursor;\n}\n\n/** @internal */\nexport interface UsePageCacheResult<T, TCursor extends CursorType = string> {\n isPending: boolean;\n error: Error | undefined;\n data: T[] | undefined;\n totalCount: number | undefined;\n currentCursor: TCursor | undefined;\n hasPreviousPage: boolean;\n onPreviousPage: () => void;\n hasNextPage: boolean;\n onNextPage: () => void;\n reload: (options?: { keepCurrentCursor?: boolean }) => void;\n}\n\ntype Direction = 'mount' | 'reset' | 'refresh' | 'next' | 'prev';\n\nclass PageCacheStore<T, TCursor extends CursorType> {\n private cache = new Map<InternalCursor<TCursor>, PageEntry<T, TCursor>>();\n\n get(cursor: InternalCursor<TCursor>): PageEntry<T, TCursor> | undefined {\n return this.cache.get(cursor);\n }\n\n getOrCreate(cursor: InternalCursor<TCursor>): PageEntry<T, TCursor> {\n const existing = this.cache.get(cursor);\n if (existing) {\n return existing;\n }\n const entry: PageEntry<T, TCursor> = {\n data: undefined,\n nextCursor: undefined,\n prevCursor: undefined,\n };\n this.cache.set(cursor, entry);\n return entry;\n }\n\n clear() {\n this.cache.clear();\n }\n\n getTargetCursor(\n direction: Direction,\n currentCursor: InternalCursor<TCursor>,\n initialCurrentCursor: TCursor | undefined,\n ): InternalCursor<TCursor> | undefined {\n if (direction === 'mount') {\n return toInternalCursor(initialCurrentCursor);\n }\n if (direction === 'reset') {\n return FIRST_PAGE_CURSOR;\n }\n if (direction === 'refresh') {\n return currentCursor;\n }\n const currentEntry = this.cache.get(currentCursor);\n\n if (!currentEntry) {\n return;\n }\n\n return direction === 'next'\n ? currentEntry.nextCursor\n : currentEntry.prevCursor;\n }\n\n linkEntryToSource(\n entry: PageEntry<T, TCursor>,\n direction: Direction,\n currentCursor: InternalCursor<TCursor>,\n ) {\n if (direction === 'next') {\n entry.prevCursor = currentCursor;\n } else if (direction === 'prev') {\n entry.nextCursor = currentCursor;\n }\n }\n}\n\nfunction toInternalCursor<TCursor extends CursorType>(\n cursor: TCursor | undefined,\n): InternalCursor<TCursor> {\n return cursor === undefined ? FIRST_PAGE_CURSOR : cursor;\n}\n\nfunction toExternalCursor<TCursor extends CursorType>(\n cursor: InternalCursor<TCursor>,\n): TCursor | undefined {\n return cursor === FIRST_PAGE_CURSOR ? undefined : cursor;\n}\n\n/** @internal */\nexport function usePageCache<T, TCursor extends CursorType = string>(\n options: UsePageCacheOptions<T, TCursor>,\n): UsePageCacheResult<T, TCursor> {\n const { getData, initialCurrentCursor } = options;\n\n const [currentCursor, setCurrentCursor] = useState<InternalCursor<TCursor>>(\n () => toInternalCursor(initialCurrentCursor),\n );\n\n const cacheStore = useRef(new PageCacheStore<T, TCursor>()).current;\n\n const [isPending, setIsPending] = useState(true);\n const [error, setError] = useState<Error | undefined>(undefined);\n const [totalCount, setTotalCount] = useState<number | undefined>(undefined);\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const currentPage = cacheStore.get(currentCursor);\n const data = currentPage?.data;\n const hasNextPage = currentPage?.nextCursor !== undefined;\n const hasPreviousPage = currentPage?.prevCursor !== undefined;\n\n const goToPage = useCallback(\n async (direction: Direction) => {\n const targetCursor = cacheStore.getTargetCursor(\n direction,\n currentCursor,\n initialCurrentCursor,\n );\n\n if (!targetCursor) {\n return;\n }\n\n const existingEntry = cacheStore.get(targetCursor);\n if (existingEntry?.data !== undefined) {\n setCurrentCursor(targetCursor);\n return;\n }\n\n const entry = cacheStore.getOrCreate(targetCursor);\n cacheStore.linkEntryToSource(entry, direction, currentCursor);\n setCurrentCursor(targetCursor);\n\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsPending(true);\n setError(undefined);\n\n try {\n const result = await getData({\n cursor: toExternalCursor(targetCursor),\n signal: abortController.signal,\n });\n\n if (abortController.signal.aborted) {\n return;\n }\n\n entry.data = result.data;\n\n if (entry.nextCursor === undefined && result.nextCursor !== undefined) {\n entry.nextCursor = result.nextCursor;\n }\n if (entry.prevCursor === undefined && result.prevCursor !== undefined) {\n entry.prevCursor = result.prevCursor;\n }\n\n if (result.totalCount !== undefined) {\n setTotalCount(result.totalCount);\n }\n\n setIsPending(false);\n } catch (err) {\n if (abortController.signal.aborted) {\n return;\n }\n\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsPending(false);\n }\n },\n [getData, initialCurrentCursor, currentCursor, cacheStore],\n );\n\n useEffect(() => {\n goToPage('mount');\n\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, []);\n\n const onNextPage = useCallback(() => {\n if (isPending) return;\n const page = cacheStore.get(currentCursor);\n if (!page?.nextCursor) return;\n goToPage('next');\n }, [isPending, currentCursor, goToPage, cacheStore]);\n\n const onPreviousPage = useCallback(() => {\n if (isPending) return;\n const page = cacheStore.get(currentCursor);\n if (!page?.prevCursor) return;\n goToPage('prev');\n }, [isPending, currentCursor, goToPage, cacheStore]);\n\n const reload = useCallback(\n (reloadOptions?: { keepCurrentCursor?: boolean }) => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n cacheStore.clear();\n\n goToPage(reloadOptions?.keepCurrentCursor ? 'refresh' : 'reset');\n },\n [goToPage, cacheStore],\n );\n\n return {\n isPending,\n error,\n data,\n totalCount,\n currentCursor: toExternalCursor(currentCursor),\n hasPreviousPage,\n onPreviousPage,\n hasNextPage,\n onNextPage,\n reload,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usePageCache.esm.js","sources":["../../../../src/components/Table/hooks/usePageCache.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useState, useCallback, useRef, useEffect, useReducer } from 'react';\n\nconst FIRST_PAGE_CURSOR = Symbol('firstPage');\n\ntype CursorType = string | number;\n\ntype InternalCursor<TCursor extends CursorType> =\n | TCursor\n | typeof FIRST_PAGE_CURSOR;\n\ninterface PageEntry<T, TCursor extends CursorType> {\n data: T[] | undefined;\n nextCursor: InternalCursor<TCursor> | undefined;\n prevCursor: InternalCursor<TCursor> | undefined;\n}\n\ninterface GetDataResult<T, TCursor extends CursorType> {\n data: T[];\n nextCursor?: TCursor;\n prevCursor?: TCursor;\n totalCount?: number;\n}\n\n/** @internal */\nexport interface UsePageCacheOptions<T, TCursor extends CursorType = string> {\n getData: (params: {\n cursor: TCursor | undefined;\n signal: AbortSignal;\n }) => Promise<GetDataResult<T, TCursor>>;\n initialCurrentCursor?: TCursor;\n}\n\n/** @internal */\nexport interface UsePageCacheResult<T, TCursor extends CursorType = string> {\n isPending: boolean;\n error: Error | undefined;\n data: T[] | undefined;\n totalCount: number | undefined;\n currentCursor: TCursor | undefined;\n hasPreviousPage: boolean;\n onPreviousPage: () => void;\n hasNextPage: boolean;\n onNextPage: () => void;\n reload: (options?: { keepCurrentCursor?: boolean }) => void;\n}\n\ntype Direction = 'mount' | 'reset' | 'refresh' | 'next' | 'prev';\n\nclass PageCacheStore<T, TCursor extends CursorType> {\n private cache = new Map<InternalCursor<TCursor>, PageEntry<T, TCursor>>();\n\n get(cursor: InternalCursor<TCursor>): PageEntry<T, TCursor> | undefined {\n return this.cache.get(cursor);\n }\n\n getOrCreate(cursor: InternalCursor<TCursor>): PageEntry<T, TCursor> {\n const existing = this.cache.get(cursor);\n if (existing) {\n return existing;\n }\n const entry: PageEntry<T, TCursor> = {\n data: undefined,\n nextCursor: undefined,\n prevCursor: undefined,\n };\n this.cache.set(cursor, entry);\n return entry;\n }\n\n clear() {\n this.cache.clear();\n }\n\n getTargetCursor(\n direction: Direction,\n currentCursor: InternalCursor<TCursor>,\n initialCurrentCursor: TCursor | undefined,\n ): InternalCursor<TCursor> | undefined {\n if (direction === 'mount') {\n return toInternalCursor(initialCurrentCursor);\n }\n if (direction === 'reset') {\n return FIRST_PAGE_CURSOR;\n }\n if (direction === 'refresh') {\n return currentCursor;\n }\n const currentEntry = this.cache.get(currentCursor);\n\n if (!currentEntry) {\n return;\n }\n\n return direction === 'next'\n ? currentEntry.nextCursor\n : currentEntry.prevCursor;\n }\n\n linkEntryToSource(\n entry: PageEntry<T, TCursor>,\n direction: Direction,\n currentCursor: InternalCursor<TCursor>,\n ) {\n if (direction === 'next') {\n entry.prevCursor = currentCursor;\n } else if (direction === 'prev') {\n entry.nextCursor = currentCursor;\n }\n }\n}\n\nfunction toInternalCursor<TCursor extends CursorType>(\n cursor: TCursor | undefined,\n): InternalCursor<TCursor> {\n return cursor === undefined ? FIRST_PAGE_CURSOR : cursor;\n}\n\nfunction toExternalCursor<TCursor extends CursorType>(\n cursor: InternalCursor<TCursor>,\n): TCursor | undefined {\n return cursor === FIRST_PAGE_CURSOR ? undefined : cursor;\n}\n\n/** @internal */\nexport function usePageCache<T, TCursor extends CursorType = string>(\n options: UsePageCacheOptions<T, TCursor>,\n): UsePageCacheResult<T, TCursor> {\n const { getData, initialCurrentCursor } = options;\n\n const [currentCursor, setCurrentCursor] = useState<InternalCursor<TCursor>>(\n () => toInternalCursor(initialCurrentCursor),\n );\n\n const cacheStore = useRef(new PageCacheStore<T, TCursor>()).current;\n\n const [isPending, setIsPending] = useState(true);\n const [error, setError] = useState<Error | undefined>(undefined);\n const [totalCount, setTotalCount] = useState<number | undefined>(undefined);\n const [, notifyCacheUpdate] = useReducer(version => version + 1, 0);\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const currentPage = cacheStore.get(currentCursor);\n const data = currentPage?.data;\n const hasNextPage = currentPage?.nextCursor !== undefined;\n const hasPreviousPage = currentPage?.prevCursor !== undefined;\n\n const goToPage = useCallback(\n async (direction: Direction) => {\n const targetCursor = cacheStore.getTargetCursor(\n direction,\n currentCursor,\n initialCurrentCursor,\n );\n\n if (targetCursor === undefined) {\n return;\n }\n\n const existingEntry = cacheStore.get(targetCursor);\n if (existingEntry?.data !== undefined) {\n setCurrentCursor(targetCursor);\n setError(undefined);\n return;\n }\n\n const entry = cacheStore.getOrCreate(targetCursor);\n cacheStore.linkEntryToSource(entry, direction, currentCursor);\n setCurrentCursor(targetCursor);\n\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsPending(true);\n setError(undefined);\n\n try {\n const result = await getData({\n cursor: toExternalCursor(targetCursor),\n signal: abortController.signal,\n });\n\n if (abortController.signal.aborted) {\n return;\n }\n\n entry.data = result.data;\n\n if (entry.nextCursor === undefined && result.nextCursor !== undefined) {\n entry.nextCursor = result.nextCursor;\n }\n if (entry.prevCursor === undefined && result.prevCursor !== undefined) {\n entry.prevCursor = result.prevCursor;\n }\n\n if (result.totalCount !== undefined) {\n setTotalCount(result.totalCount);\n }\n\n notifyCacheUpdate();\n setIsPending(false);\n } catch (err) {\n if (abortController.signal.aborted) {\n return;\n }\n\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsPending(false);\n }\n },\n [getData, initialCurrentCursor, currentCursor, cacheStore],\n );\n\n useEffect(() => {\n goToPage('mount');\n\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, []);\n\n const onNextPage = useCallback(() => {\n if (isPending) return;\n const page = cacheStore.get(currentCursor);\n if (page?.nextCursor === undefined) return;\n goToPage('next');\n }, [isPending, currentCursor, goToPage, cacheStore]);\n\n const onPreviousPage = useCallback(() => {\n if (isPending) return;\n const page = cacheStore.get(currentCursor);\n if (page?.prevCursor === undefined) return;\n goToPage('prev');\n }, [isPending, currentCursor, goToPage, cacheStore]);\n\n const reload = useCallback(\n (reloadOptions?: { keepCurrentCursor?: boolean }) => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n cacheStore.clear();\n\n goToPage(reloadOptions?.keepCurrentCursor ? 'refresh' : 'reset');\n },\n [goToPage, cacheStore],\n );\n\n return {\n isPending,\n error,\n data,\n totalCount,\n currentCursor: toExternalCursor(currentCursor),\n hasPreviousPage,\n onPreviousPage,\n hasNextPage,\n onNextPage,\n reload,\n };\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3143,6 +3143,12 @@ interface QueryOptions<TFilter> {
3143
3143
  }
3144
3144
  /** @public */
3145
3145
  interface PaginationOptions extends Partial<Pick<TablePaginationProps, 'pageSize' | 'pageSizeOptions' | 'onPageSizeChange' | 'onNextPage' | 'onPreviousPage' | 'showPageSizeOptions' | 'getLabel' | 'showPaginationLabel'>> {
3146
+ /**
3147
+ * The zero-based item offset to use for the initial page.
3148
+ *
3149
+ * In complete mode, the offset is adjusted to the last available page if
3150
+ * the data later becomes too short for the current page.
3151
+ */
3146
3152
  initialOffset?: number;
3147
3153
  }
3148
3154
  /** @public */
@@ -3199,6 +3205,11 @@ type UseTableCompleteOptions<T extends TableItem, TFilter = unknown> = QueryOpti
3199
3205
  */
3200
3206
  filterDebounceMs?: number;
3201
3207
  } & ({
3208
+ /**
3209
+ * Controlled table data. Set to `undefined` while loading new data;
3210
+ * the previous data and pagination metadata remain visible and are
3211
+ * reported as stale.
3212
+ */
3202
3213
  data: T[] | undefined;
3203
3214
  getData?: never;
3204
3215
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/ui",
3
- "version": "0.18.0-next.1",
3
+ "version": "0.18.0-next.2",
4
4
  "backstage": {
5
5
  "role": "web-library"
6
6
  },
@@ -63,8 +63,8 @@
63
63
  "use-sync-external-store": "^1.4.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@backstage/cli": "0.36.6-next.0",
67
- "@backstage/core-components": "0.18.14-next.1",
66
+ "@backstage/cli": "0.36.6-next.1",
67
+ "@backstage/core-components": "0.18.14-next.2",
68
68
  "@storybook/react-vite": "^10.4.0",
69
69
  "@testing-library/dom": "^10.0.0",
70
70
  "@testing-library/jest-dom": "^6.0.0",