@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 +8 -0
- package/dist/components/Table/hooks/useCompletePagination.esm.js +45 -15
- package/dist/components/Table/hooks/useCompletePagination.esm.js.map +1 -1
- package/dist/components/Table/hooks/useDebouncedReload.esm.js +5 -4
- package/dist/components/Table/hooks/useDebouncedReload.esm.js.map +1 -1
- package/dist/components/Table/hooks/usePageCache.esm.js +7 -4
- package/dist/components/Table/hooks/usePageCache.esm.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/package.json +3 -3
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,
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
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 ?
|
|
104
|
-
|
|
130
|
+
() => noPagination ? retainedProcessedData : retainedProcessedData?.slice(
|
|
131
|
+
effectiveOffset,
|
|
132
|
+
effectiveOffset + pageSize
|
|
133
|
+
),
|
|
134
|
+
[retainedProcessedData, effectiveOffset, pageSize, noPagination]
|
|
105
135
|
);
|
|
106
|
-
const hasNextPage = !noPagination &&
|
|
107
|
-
const hasPreviousPage = !noPagination &&
|
|
136
|
+
const hasNextPage = !noPagination && effectiveOffset + pageSize < totalCount;
|
|
137
|
+
const hasPreviousPage = !noPagination && effectiveOffset > 0;
|
|
108
138
|
const onNextPage = useCallback(() => {
|
|
109
|
-
if (
|
|
110
|
-
setOffset(
|
|
139
|
+
if (effectiveOffset + pageSize < totalCount) {
|
|
140
|
+
setOffset(effectiveOffset + pageSize);
|
|
111
141
|
}
|
|
112
|
-
}, [
|
|
142
|
+
}, [effectiveOffset, pageSize, totalCount]);
|
|
113
143
|
const onPreviousPage = useCallback(() => {
|
|
114
|
-
if (
|
|
115
|
-
setOffset(Math.max(0,
|
|
144
|
+
if (effectiveOffset > 0) {
|
|
145
|
+
setOffset(Math.max(0, effectiveOffset - pageSize));
|
|
116
146
|
}
|
|
117
|
-
}, [
|
|
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
|
|
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.
|
|
8
|
-
prevDepsRef.current = {
|
|
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
|
-
}, [
|
|
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({
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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":";;AAkBA,MAAM,iBAAA,0BAA2B,WAAW,CAAA;AA8C5C,MAAM,cAAA,CAA8C;AAAA,EAC1C,KAAA,uBAAY,GAAA,EAAoD;AAAA,EAExE,IAAI,MAAA,EAAoE;AACtE,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AAAA,EAC9B;AAAA,EAEA,YAAY,MAAA,EAAwD;AAClE,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AACtC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA;AAAA,IACT;AACA,IAAA,MAAM,KAAA,GAA+B;AAAA,MACnC,IAAA,EAAM,MAAA;AAAA,MACN,UAAA,EAAY,MAAA;AAAA,MACZ,UAAA,EAAY;AAAA,KACd;AACA,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAA,EAAQ,KAAK,CAAA;AAC5B,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,KAAA,GAAQ;AACN,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AAAA,EACnB;AAAA,EAEA,eAAA,CACE,SAAA,EACA,aAAA,EACA,oBAAA,EACqC;AACrC,IAAA,IAAI,cAAc,OAAA,EAAS;AACzB,MAAA,OAAO,iBAAiB,oBAAoB,CAAA;AAAA,IAC9C;AACA,IAAA,IAAI,cAAc,OAAA,EAAS;AACzB,MAAA,OAAO,iBAAA;AAAA,IACT;AACA,IAAA,IAAI,cAAc,SAAA,EAAW;AAC3B,MAAA,OAAO,aAAA;AAAA,IACT;AACA,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,aAAa,CAAA;AAEjD,IAAA,IAAI,CAAC,YAAA,EAAc;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,OAAO,SAAA,KAAc,MAAA,GACjB,YAAA,CAAa,UAAA,GACb,YAAA,CAAa,UAAA;AAAA,EACnB;AAAA,EAEA,iBAAA,CACE,KAAA,EACA,SAAA,EACA,aAAA,EACA;AACA,IAAA,IAAI,cAAc,MAAA,EAAQ;AACxB,MAAA,KAAA,CAAM,UAAA,GAAa,aAAA;AAAA,IACrB,CAAA,MAAA,IAAW,cAAc,MAAA,EAAQ;AAC/B,MAAA,KAAA,CAAM,UAAA,GAAa,aAAA;AAAA,IACrB;AAAA,EACF;AACF;AAEA,SAAS,iBACP,MAAA,EACyB;AACzB,EAAA,OAAO,MAAA,KAAW,SAAY,iBAAA,GAAoB,MAAA;AACpD;AAEA,SAAS,iBACP,MAAA,EACqB;AACrB,EAAA,OAAO,MAAA,KAAW,oBAAoB,MAAA,GAAY,MAAA;AACpD;AAGO,SAAS,aACd,OAAA,EACgC;AAChC,EAAA,MAAM,EAAE,OAAA,EAAS,oBAAA,EAAqB,GAAI,OAAA;AAE1C,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAI,QAAA;AAAA,IACxC,MAAM,iBAAiB,oBAAoB;AAAA,GAC7C;AAEA,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,IAAI,cAAA,EAA4B,CAAA,CAAE,OAAA;AAE5D,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,IAAI,CAAA;AAC/C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAA4B,MAAS,CAAA;AAC/D,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAA6B,MAAS,CAAA;AAE1E,EAAA,MAAM,kBAAA,GAAqB,OAA+B,IAAI,CAAA;AAE9D,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,GAAA,CAAI,aAAa,CAAA;AAChD,EAAA,MAAM,OAAO,WAAA,EAAa,IAAA;AAC1B,EAAA,MAAM,WAAA,GAAc,aAAa,UAAA,KAAe,MAAA;AAChD,EAAA,MAAM,eAAA,GAAkB,aAAa,UAAA,KAAe,MAAA;AAEpD,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,OAAO,SAAA,KAAyB;AAC9B,MAAA,MAAM,eAAe,UAAA,CAAW,eAAA;AAAA,QAC9B,SAAA;AAAA,QACA,aAAA;AAAA,QACA;AAAA,OACF;AAEA,MAAA,IAAI,CAAC,YAAA,EAAc;AACjB,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,aAAA,GAAgB,UAAA,CAAW,GAAA,CAAI,YAAY,CAAA;AACjD,MAAA,IAAI,aAAA,EAAe,SAAS,MAAA,EAAW;AACrC,QAAA,gBAAA,CAAiB,YAAY,CAAA;AAC7B,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,WAAA,CAAY,YAAY,CAAA;AACjD,MAAA,UAAA,CAAW,iBAAA,CAAkB,KAAA,EAAO,SAAA,EAAW,aAAa,CAAA;AAC5D,MAAA,gBAAA,CAAiB,YAAY,CAAA;AAE7B,MAAA,IAAI,mBAAmB,OAAA,EAAS;AAC9B,QAAA,kBAAA,CAAmB,QAAQ,KAAA,EAAM;AAAA,MACnC;AAEA,MAAA,MAAM,eAAA,GAAkB,IAAI,eAAA,EAAgB;AAC5C,MAAA,kBAAA,CAAmB,OAAA,GAAU,eAAA;AAE7B,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,QAAA,CAAS,MAAS,CAAA;AAElB,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ;AAAA,UAC3B,MAAA,EAAQ,iBAAiB,YAAY,CAAA;AAAA,UACrC,QAAQ,eAAA,CAAgB;AAAA,SACzB,CAAA;AAED,QAAA,IAAI,eAAA,CAAgB,OAAO,OAAA,EAAS;AAClC,UAAA;AAAA,QACF;AAEA,QAAA,KAAA,CAAM,OAAO,MAAA,CAAO,IAAA;AAEpB,QAAA,IAAI,KAAA,CAAM,UAAA,KAAe,KAAA,CAAA,IAAa,MAAA,CAAO,eAAe,KAAA,CAAA,EAAW;AACrE,UAAA,KAAA,CAAM,aAAa,MAAA,CAAO,UAAA;AAAA,QAC5B;AACA,QAAA,IAAI,KAAA,CAAM,UAAA,KAAe,KAAA,CAAA,IAAa,MAAA,CAAO,eAAe,KAAA,CAAA,EAAW;AACrE,UAAA,KAAA,CAAM,aAAa,MAAA,CAAO,UAAA;AAAA,QAC5B;AAEA,QAAA,IAAI,MAAA,CAAO,eAAe,KAAA,CAAA,EAAW;AACnC,UAAA,aAAA,CAAc,OAAO,UAAU,CAAA;AAAA,QACjC;AAEA,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACpB,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,eAAA,CAAgB,OAAO,OAAA,EAAS;AAClC,UAAA;AAAA,QACF;AAEA,QAAA,QAAA,CAAS,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAC5D,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACpB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,OAAA,EAAS,oBAAA,EAAsB,aAAA,EAAe,UAAU;AAAA,GAC3D;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,QAAA,CAAS,OAAO,CAAA;AAEhB,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,mBAAmB,OAAA,EAAS;AAC9B,QAAA,kBAAA,CAAmB,QAAQ,KAAA,EAAM;AAAA,MACnC;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAa,YAAY,MAAM;AACnC,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,MAAM,IAAA,GAAO,UAAA,CAAW,GAAA,CAAI,aAAa,CAAA;AACzC,IAAA,IAAI,CAAC,MAAM,UAAA,EAAY;AACvB,IAAA,QAAA,CAAS,MAAM,CAAA;AAAA,EACjB,GAAG,CAAC,SAAA,EAAW,aAAA,EAAe,QAAA,EAAU,UAAU,CAAC,CAAA;AAEnD,EAAA,MAAM,cAAA,GAAiB,YAAY,MAAM;AACvC,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,MAAM,IAAA,GAAO,UAAA,CAAW,GAAA,CAAI,aAAa,CAAA;AACzC,IAAA,IAAI,CAAC,MAAM,UAAA,EAAY;AACvB,IAAA,QAAA,CAAS,MAAM,CAAA;AAAA,EACjB,GAAG,CAAC,SAAA,EAAW,aAAA,EAAe,QAAA,EAAU,UAAU,CAAC,CAAA;AAEnD,EAAA,MAAM,MAAA,GAAS,WAAA;AAAA,IACb,CAAC,aAAA,KAAoD;AACnD,MAAA,IAAI,mBAAmB,OAAA,EAAS;AAC9B,QAAA,kBAAA,CAAmB,QAAQ,KAAA,EAAM;AAAA,MACnC;AAEA,MAAA,UAAA,CAAW,KAAA,EAAM;AAEjB,MAAA,QAAA,CAAS,aAAA,EAAe,iBAAA,GAAoB,SAAA,GAAY,OAAO,CAAA;AAAA,IACjE,CAAA;AAAA,IACA,CAAC,UAAU,UAAU;AAAA,GACvB;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,KAAA;AAAA,IACA,IAAA;AAAA,IACA,UAAA;AAAA,IACA,aAAA,EAAe,iBAAiB,aAAa,CAAA;AAAA,IAC7C,eAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AACF;;;;"}
|
|
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.
|
|
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.
|
|
67
|
-
"@backstage/core-components": "0.18.14-next.
|
|
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",
|