@strapi/upload 5.51.2 → 5.52.0
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/dist/admin/future/App.js +3 -3
- package/dist/admin/future/App.js.map +1 -1
- package/dist/admin/future/App.mjs +4 -4
- package/dist/admin/future/App.mjs.map +1 -1
- package/dist/admin/future/components/BetaNotice.js +72 -0
- package/dist/admin/future/components/BetaNotice.js.map +1 -0
- package/dist/admin/future/components/BetaNotice.mjs +70 -0
- package/dist/admin/future/components/BetaNotice.mjs.map +1 -0
- package/dist/admin/future/components/Drawer.js +16 -3
- package/dist/admin/future/components/Drawer.js.map +1 -1
- package/dist/admin/future/components/Drawer.mjs +16 -3
- package/dist/admin/future/components/Drawer.mjs.map +1 -1
- package/dist/admin/future/hooks/useAIMetadataEnabled.js +39 -0
- package/dist/admin/future/hooks/useAIMetadataEnabled.js.map +1 -0
- package/dist/admin/future/hooks/useAIMetadataEnabled.mjs +37 -0
- package/dist/admin/future/hooks/useAIMetadataEnabled.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/AssetsPage.js +297 -139
- package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.mjs +301 -143
- package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/ActionsMenuContent.js +46 -0
- package/dist/admin/future/pages/Assets/components/ActionsMenuContent.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/ActionsMenuContent.mjs +44 -0
- package/dist/admin/future/pages/Assets/components/ActionsMenuContent.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js +325 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs +323 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +35 -14
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +35 -14
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +54 -53
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +54 -54
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js +89 -24
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +91 -26
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +15 -3
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +16 -4
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.js +168 -58
- package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +171 -61
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +64 -132
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +64 -132
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkMoveDialog.js +123 -76
- package/dist/admin/future/pages/Assets/components/BulkMoveDialog.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkMoveDialog.mjs +126 -79
- package/dist/admin/future/pages/Assets/components/BulkMoveDialog.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BusyOverlay.js +36 -0
- package/dist/admin/future/pages/Assets/components/BusyOverlay.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/BusyOverlay.mjs +34 -0
- package/dist/admin/future/pages/Assets/components/BusyOverlay.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.js +131 -0
- package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.mjs +129 -0
- package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +27 -17
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +28 -18
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.js +28 -31
- package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.mjs +28 -31
- package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js +10 -15
- package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs +10 -15
- package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterBadges.js +13 -4
- package/dist/admin/future/pages/Assets/components/FilterBadges.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterBadges.mjs +13 -4
- package/dist/admin/future/pages/Assets/components/FilterBadges.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterMenu.js +241 -61
- package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +244 -64
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js +144 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs +142 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.js → FolderFormDialog.js} +50 -16
- package/dist/admin/future/pages/Assets/components/FolderFormDialog.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.mjs → FolderFormDialog.mjs} +52 -18
- package/dist/admin/future/pages/Assets/components/FolderFormDialog.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +111 -191
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +114 -194
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useBusyAssets.js +47 -0
- package/dist/admin/future/pages/Assets/hooks/useBusyAssets.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useBusyAssets.mjs +44 -0
- package/dist/admin/future/pages/Assets/hooks/useBusyAssets.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +203 -62
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +204 -63
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.js +75 -0
- package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.mjs +73 -0
- package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.mjs.map +1 -0
- package/dist/admin/future/services/api.js +24 -0
- package/dist/admin/future/services/api.js.map +1 -1
- package/dist/admin/future/services/api.mjs +24 -0
- package/dist/admin/future/services/api.mjs.map +1 -1
- package/dist/admin/future/services/assets.js +11 -0
- package/dist/admin/future/services/assets.js.map +1 -1
- package/dist/admin/future/services/assets.mjs +11 -0
- package/dist/admin/future/services/assets.mjs.map +1 -1
- package/dist/admin/future/services/folders.js +37 -2
- package/dist/admin/future/services/folders.js.map +1 -1
- package/dist/admin/future/services/folders.mjs +37 -3
- package/dist/admin/future/services/folders.mjs.map +1 -1
- package/dist/admin/future/utils/buildDragSetFromSelection.js +7 -5
- package/dist/admin/future/utils/buildDragSetFromSelection.js.map +1 -1
- package/dist/admin/future/utils/buildDragSetFromSelection.mjs +7 -5
- package/dist/admin/future/utils/buildDragSetFromSelection.mjs.map +1 -1
- package/dist/admin/future/utils/formatMoveSuccessMessage.js +12 -1
- package/dist/admin/future/utils/formatMoveSuccessMessage.js.map +1 -1
- package/dist/admin/future/utils/formatMoveSuccessMessage.mjs +12 -1
- package/dist/admin/future/utils/formatMoveSuccessMessage.mjs.map +1 -1
- package/dist/admin/future/utils/getRelationId.js +24 -0
- package/dist/admin/future/utils/getRelationId.js.map +1 -0
- package/dist/admin/future/utils/getRelationId.mjs +22 -0
- package/dist/admin/future/utils/getRelationId.mjs.map +1 -0
- package/dist/admin/future/utils/isEventFromWithin.js +14 -0
- package/dist/admin/future/utils/isEventFromWithin.js.map +1 -0
- package/dist/admin/future/utils/isEventFromWithin.mjs +12 -0
- package/dist/admin/future/utils/isEventFromWithin.mjs.map +1 -0
- package/dist/admin/future/utils/itemLocations.js +40 -0
- package/dist/admin/future/utils/itemLocations.js.map +1 -0
- package/dist/admin/future/utils/itemLocations.mjs +36 -0
- package/dist/admin/future/utils/itemLocations.mjs.map +1 -0
- package/dist/admin/future/utils/itemSource.js +36 -0
- package/dist/admin/future/utils/itemSource.js.map +1 -0
- package/dist/admin/future/utils/itemSource.mjs +32 -0
- package/dist/admin/future/utils/itemSource.mjs.map +1 -0
- package/dist/admin/index.js +11 -17
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +12 -18
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/src/future/App.d.ts +1 -1
- package/dist/admin/src/future/components/BetaNotice.d.ts +11 -0
- package/dist/admin/src/future/components/Drawer.d.ts +5 -1
- package/dist/admin/src/future/hooks/useAIMetadataEnabled.d.ts +26 -0
- package/dist/admin/src/future/pages/Assets/components/ActionsMenuContent.d.ts +33 -0
- package/dist/admin/src/future/pages/Assets/components/AssetActionsMenu.d.ts +24 -0
- package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.d.ts +16 -0
- package/dist/admin/src/future/pages/Assets/components/BulkActionsBar.d.ts +8 -1
- package/dist/admin/src/future/pages/Assets/components/BulkMoveDialog.d.ts +17 -6
- package/dist/admin/src/future/pages/Assets/components/BusyOverlay.d.ts +9 -0
- package/dist/admin/src/future/pages/Assets/components/DeleteItemsDialog.d.ts +27 -0
- package/dist/admin/src/future/pages/Assets/components/Dnd/AssetsDndProvider.d.ts +8 -1
- package/dist/admin/src/future/pages/Assets/components/Dnd/buildDragSet.d.ts +18 -3
- package/dist/admin/src/future/pages/Assets/components/Dnd/useAssetDnd.d.ts +2 -1
- package/dist/admin/src/future/pages/Assets/components/FilterBadges.d.ts +3 -1
- package/dist/admin/src/future/pages/Assets/components/FilterMenu.d.ts +4 -0
- package/dist/admin/src/future/pages/Assets/components/FolderActionsMenu.d.ts +20 -0
- package/dist/admin/src/future/pages/Assets/components/FolderFormDialog.d.ts +16 -0
- package/dist/admin/src/future/pages/Assets/hooks/useBusyAssets.d.ts +38 -0
- package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +20 -6
- package/dist/admin/src/future/pages/Assets/hooks/useInfiniteScrollSentinel.d.ts +41 -0
- package/dist/admin/src/future/services/folders.d.ts +6 -1
- package/dist/admin/src/future/utils/buildDragSetFromSelection.d.ts +4 -3
- package/dist/admin/src/future/utils/formatMoveSuccessMessage.d.ts +7 -1
- package/dist/admin/src/future/utils/getRelationId.d.ts +11 -0
- package/dist/admin/src/future/utils/isEventFromWithin.d.ts +11 -0
- package/dist/admin/src/future/utils/itemLocations.d.ts +31 -0
- package/dist/admin/src/future/utils/itemSource.d.ts +22 -0
- package/dist/admin/translations/en.json.js +25 -4
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +25 -4
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/server/services/image-manipulation.js.map +1 -1
- package/dist/server/services/image-manipulation.mjs.map +1 -1
- package/dist/server/src/services/image-manipulation.d.ts +0 -5
- package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
- package/package.json +9 -9
- package/dist/admin/future/pages/Assets/components/CreateFolderDialog.js.map +0 -1
- package/dist/admin/future/pages/Assets/components/CreateFolderDialog.mjs.map +0 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js +0 -33
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js.map +0 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs +0 -31
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs.map +0 -1
- package/dist/admin/src/future/pages/Assets/components/CreateFolderDialog.d.ts +0 -8
- package/dist/admin/src/future/pages/Assets/components/FolderTree/filterFolderTree.d.ts +0 -16
|
@@ -1,90 +1,230 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useCallback, createElement, Fragment, useRef, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useDispatch, useStore } from 'react-redux';
|
|
3
|
+
import { uploadApi } from '../../../services/api.mjs';
|
|
2
4
|
import { useGetAssetsQuery } from '../../../services/assets.mjs';
|
|
3
5
|
|
|
4
6
|
const PAGE_SIZE = 20;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Concatenates the pages in ascending order, one entry per asset id.
|
|
9
|
+
*
|
|
10
|
+
* `Map.set` keeps the first insertion position but takes the latest value, so
|
|
11
|
+
* an asset that moved between pages after a mutation holds its place and shows
|
|
12
|
+
* fresh data instead of rendering twice under a duplicate key.
|
|
13
|
+
*/ const flattenPages = (pages)=>{
|
|
14
|
+
const byId = new Map();
|
|
15
|
+
for (const page of Object.keys(pages).map(Number).sort((a, b)=>a - b)){
|
|
16
|
+
for (const asset of pages[page]){
|
|
17
|
+
byId.set(asset.id, asset);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return [
|
|
21
|
+
...byId.values()
|
|
22
|
+
];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Keeps one earlier page's `getAssets` query subscribed and reports its results
|
|
26
|
+
* back up whenever they change.
|
|
27
|
+
*
|
|
28
|
+
* The hook itself only subscribes the current (latest) page. Without these, a
|
|
29
|
+
* mutation invalidating `{Asset, LIST}` refetches that page alone and earlier
|
|
30
|
+
* pages stay frozen — a rename or delete on an earlier page never reaches the
|
|
31
|
+
* list until a folder/sort/search change. One subscriber per earlier
|
|
32
|
+
* page keeps them all subscribed so the invalidation refetches the whole list.
|
|
33
|
+
* Renders nothing; it exists only for the subscription + `onRefreshed` report.
|
|
34
|
+
*/ const PageSubscriber = ({ queryArgs, page, onRefreshed })=>{
|
|
35
|
+
const { currentData } = useGetAssetsQuery({
|
|
36
|
+
...queryArgs,
|
|
37
|
+
page,
|
|
38
|
+
pageSize: PAGE_SIZE
|
|
39
|
+
});
|
|
40
|
+
const results = currentData?.results;
|
|
41
|
+
useEffect(()=>{
|
|
42
|
+
if (results) {
|
|
43
|
+
onRefreshed(page, results);
|
|
44
|
+
}
|
|
45
|
+
// `results` is a stable reference per cache entry, so this only re-reports
|
|
46
|
+
// when a refetch actually replaces the page's data. Reporting the same
|
|
47
|
+
// reference the accumulator already holds is a no-op (see `onRefreshed`).
|
|
48
|
+
}, [
|
|
49
|
+
results,
|
|
13
50
|
page,
|
|
14
|
-
|
|
51
|
+
onRefreshed
|
|
52
|
+
]);
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Infinite scroll over `/upload/files`, keeping one accumulated slot per page.
|
|
57
|
+
*
|
|
58
|
+
* The caller MUST render the returned `subscribers` node — that is what keeps
|
|
59
|
+
* every loaded page subscribed, so `{Asset, LIST}` invalidations keep the whole
|
|
60
|
+
* list consistent rather than only the current page.
|
|
61
|
+
*/ const useInfiniteAssets = ({ folder = null, sort, search, filters, enabled = true } = {})=>{
|
|
62
|
+
// Derived from the request args themselves, so a new filter can't reach the
|
|
63
|
+
// API without also invalidating what was accumulated under the old one.
|
|
64
|
+
const queryArgs = {
|
|
65
|
+
folder,
|
|
15
66
|
sort,
|
|
16
67
|
search,
|
|
17
68
|
filters
|
|
69
|
+
};
|
|
70
|
+
const queryKey = JSON.stringify(queryArgs);
|
|
71
|
+
// Identifies the list being viewed, ignoring the search term. Folder, sort
|
|
72
|
+
// and filter changes are single committed actions, so one clean reset per
|
|
73
|
+
// change is honest. Search is committed per keystroke on a 300ms debounce,
|
|
74
|
+
// and blanking the list on each one would strobe as the user types.
|
|
75
|
+
// The cost: through the search window the previous rows stay rendered and
|
|
76
|
+
// `hasNextPage` is false, so the list reads as complete until page 1 lands.
|
|
77
|
+
const listKey = JSON.stringify({
|
|
78
|
+
folder,
|
|
79
|
+
sort,
|
|
80
|
+
filters
|
|
81
|
+
});
|
|
82
|
+
const [pageState, setPageState] = useState({
|
|
83
|
+
queryKey,
|
|
84
|
+
page: 1
|
|
85
|
+
});
|
|
86
|
+
const [accumulated, setAccumulated] = useState({
|
|
87
|
+
queryKey,
|
|
88
|
+
listKey,
|
|
89
|
+
pages: {}
|
|
90
|
+
});
|
|
91
|
+
// Derived rather than read straight from state: the render that discovers
|
|
92
|
+
// the key change still reaches the query below, and a list with no page 1
|
|
93
|
+
// must never be requested at page 2.
|
|
94
|
+
const page = pageState.queryKey === queryKey ? pageState.page : 1;
|
|
95
|
+
if (pageState.queryKey !== queryKey) {
|
|
96
|
+
setPageState({
|
|
97
|
+
queryKey,
|
|
98
|
+
page: 1
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const { currentData, isLoading, isFetching, error } = useGetAssetsQuery({
|
|
102
|
+
...queryArgs,
|
|
103
|
+
page,
|
|
104
|
+
pageSize: PAGE_SIZE
|
|
18
105
|
}, {
|
|
19
106
|
skip: !enabled
|
|
20
107
|
});
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
108
|
+
const isSameQuery = accumulated.queryKey === queryKey;
|
|
109
|
+
// `currentData` is only ever the payload for the args just passed, so it
|
|
110
|
+
// belongs to this key at this page — nothing to infer. Its reference is
|
|
111
|
+
// stable per cache entry, so this settles after one extra render.
|
|
112
|
+
if (currentData && (!isSameQuery || accumulated.pages[page] !== currentData.results)) {
|
|
113
|
+
setAccumulated(isSameQuery ? {
|
|
114
|
+
...accumulated,
|
|
115
|
+
pages: {
|
|
116
|
+
...accumulated.pages,
|
|
117
|
+
[page]: currentData.results
|
|
118
|
+
},
|
|
119
|
+
pagination: currentData.pagination
|
|
120
|
+
} : {
|
|
121
|
+
queryKey,
|
|
122
|
+
listKey,
|
|
123
|
+
pages: {
|
|
124
|
+
[page]: currentData.results
|
|
125
|
+
},
|
|
126
|
+
pagination: currentData.pagination
|
|
127
|
+
});
|
|
24
128
|
}
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (page === 1) {
|
|
33
|
-
lastResultsRef.current = currentPageResults;
|
|
34
|
-
} else {
|
|
35
|
-
// If accumulated length doesn't match expectation, cache was cleared
|
|
36
|
-
const expectedPrior = (page - 1) * PAGE_SIZE;
|
|
37
|
-
if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {
|
|
38
|
-
return lastResultsRef.current;
|
|
129
|
+
// Merges a refetched earlier page back into the accumulator. Ignores reports
|
|
130
|
+
// for a superseded query, and no-ops when the reference is unchanged so a
|
|
131
|
+
// subscriber re-reporting its already-stored page can't loop.
|
|
132
|
+
const handlePageRefreshed = useCallback((refreshedPage, results)=>{
|
|
133
|
+
setAccumulated((prev)=>{
|
|
134
|
+
if (prev.queryKey !== queryKey || prev.pages[refreshedPage] === results) {
|
|
135
|
+
return prev;
|
|
39
136
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return lastResultsRef.current;
|
|
137
|
+
return {
|
|
138
|
+
...prev,
|
|
139
|
+
pages: {
|
|
140
|
+
...prev.pages,
|
|
141
|
+
[refreshedPage]: results
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
});
|
|
49
145
|
}, [
|
|
50
|
-
|
|
51
|
-
page
|
|
146
|
+
queryKey
|
|
52
147
|
]);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
148
|
+
// One subscriber per earlier page (the current page is already subscribed by
|
|
149
|
+
// the query above). Keyed by queryKey so a folder/sort/search change remounts
|
|
150
|
+
// them against the new args instead of reusing a stale cache entry.
|
|
151
|
+
// `createElement` rather than JSX so this stays a plain `.ts` hook file.
|
|
152
|
+
const subscribers = createElement(Fragment, null, Array.from({
|
|
153
|
+
length: Math.max(0, page - 1)
|
|
154
|
+
}, (_, index)=>index + 1).map((earlierPage)=>createElement(PageSubscriber, {
|
|
155
|
+
key: `${queryKey}:${earlierPage}`,
|
|
156
|
+
queryArgs,
|
|
157
|
+
page: earlierPage,
|
|
158
|
+
onRefreshed: handlePageRefreshed
|
|
159
|
+
})));
|
|
160
|
+
// Drop the left folder's cached `getAssets` pages when the folder changes.
|
|
161
|
+
//
|
|
162
|
+
// RTK Query 1.9.7 leaves a query's store subscription in place when its last
|
|
163
|
+
// subscriber unmounts while a refetch is still in flight — e.g. a delete
|
|
164
|
+
// invalidates `{Asset, LIST}`, every loaded page starts refetching, and the
|
|
165
|
+
// user navigates away before those settle. Those stale subscriptions make the
|
|
166
|
+
// next `{Asset, LIST}` invalidation (an upload in the new folder) refetch the
|
|
167
|
+
// *previous* folder's pages: one bogus `/upload/files` request per loaded
|
|
168
|
+
// page. There is no public per-entry unsubscribe, so evict the left folder's
|
|
169
|
+
// entries outright — returning reloads from page 1, which the accumulator
|
|
170
|
+
// already does on a list change.
|
|
171
|
+
const dispatch = useDispatch();
|
|
172
|
+
const store = useStore();
|
|
173
|
+
const prevFolderRef = useRef(folder);
|
|
55
174
|
useEffect(()=>{
|
|
56
|
-
|
|
57
|
-
|
|
175
|
+
const prevFolder = prevFolderRef.current;
|
|
176
|
+
prevFolderRef.current = folder;
|
|
177
|
+
if (prevFolder === folder) {
|
|
58
178
|
return;
|
|
59
179
|
}
|
|
60
|
-
|
|
61
|
-
|
|
180
|
+
const apiState = store.getState()[uploadApi.reducerPath];
|
|
181
|
+
const removeQueryResult = uploadApi.internalActions.removeQueryResult;
|
|
182
|
+
Object.keys(apiState?.queries ?? {}).forEach((cacheKey)=>{
|
|
183
|
+
if (!cacheKey.startsWith('getAssets(')) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let args;
|
|
187
|
+
try {
|
|
188
|
+
args = JSON.parse(cacheKey.slice('getAssets('.length, -1));
|
|
189
|
+
} catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (args.folder === prevFolder) {
|
|
193
|
+
dispatch(removeQueryResult({
|
|
194
|
+
queryCacheKey: cacheKey
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
62
198
|
}, [
|
|
63
199
|
folder,
|
|
64
|
-
|
|
65
|
-
|
|
200
|
+
dispatch,
|
|
201
|
+
store
|
|
66
202
|
]);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
setPage(1);
|
|
75
|
-
}, [
|
|
76
|
-
search
|
|
203
|
+
// Until the new list's first page lands the accumulator still holds the
|
|
204
|
+
// previous folder. Reported as loading so the page shows a spinner instead
|
|
205
|
+
// of the outgoing folder's assets under the incoming folder's header.
|
|
206
|
+
const isChangingList = accumulated.listKey !== listKey;
|
|
207
|
+
const assets = useMemo(()=>isChangingList ? [] : flattenPages(accumulated.pages), [
|
|
208
|
+
isChangingList,
|
|
209
|
+
accumulated.pages
|
|
77
210
|
]);
|
|
78
211
|
// Deliberately the live value, not the stale-tolerant one below: paging must
|
|
79
212
|
// never be driven by a total that belongs to the previous query.
|
|
80
|
-
const hasNextPage =
|
|
213
|
+
const hasNextPage = currentData ? page < currentData.pagination.pageCount : false;
|
|
81
214
|
const isFetchingMore = isFetching && page > 1;
|
|
82
215
|
const fetchNextPage = useCallback(()=>{
|
|
83
|
-
|
|
84
|
-
|
|
216
|
+
setPageState((prev)=>({
|
|
217
|
+
queryKey,
|
|
218
|
+
page: (prev.queryKey === queryKey ? prev.page : 1) + 1
|
|
219
|
+
}));
|
|
220
|
+
}, [
|
|
221
|
+
queryKey
|
|
222
|
+
]);
|
|
85
223
|
if (!enabled) {
|
|
86
224
|
return {
|
|
87
225
|
assets: [],
|
|
226
|
+
// No list to keep fresh while disabled, so nothing to subscribe.
|
|
227
|
+
subscribers: null,
|
|
88
228
|
pagination: undefined,
|
|
89
229
|
isLoading: false,
|
|
90
230
|
isFetchingMore: false,
|
|
@@ -95,10 +235,11 @@ const useInfiniteAssets = ({ folder = null, sort, search, filters, filtersKey =
|
|
|
95
235
|
}
|
|
96
236
|
return {
|
|
97
237
|
assets,
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
238
|
+
subscribers,
|
|
239
|
+
// Falls back to the accumulated total so a consumer showing the count
|
|
240
|
+
// doesn't flash zero mid-transition, matching the possibly stale `assets`.
|
|
241
|
+
pagination: currentData?.pagination ?? accumulated.pagination,
|
|
242
|
+
isLoading: isLoading || isChangingList,
|
|
102
243
|
isFetchingMore,
|
|
103
244
|
hasNextPage,
|
|
104
245
|
fetchNextPage,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useInfiniteAssets.mjs","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteAssets.ts"],"sourcesContent":["import { useState, useCallback, useMemo, useEffect, useRef } from 'react';\n\nimport { useGetAssetsQuery } from '../../../services/assets';\n\nimport type { File, Pagination } from '../../../../../../shared/contracts/files';\n\nconst PAGE_SIZE = 20;\n\ninterface UseInfiniteAssetsOptions {\n folder?: number | null;\n sort?: string;\n search?: string;\n /** Extra `filters[$and]` entries for the files query. */\n filters?: Record<string, unknown>[];\n /**\n * Stable fingerprint of `filters` — a change resets the accumulated pages\n * (the array identity itself changes every render).\n */\n filtersKey?: string;\n /** Structural switch: an `is [folder]`-only type badge means no file can match. */\n enabled?: boolean;\n}\n\nconst useInfiniteAssets = ({\n folder = null,\n sort,\n search,\n filters,\n filtersKey = '',\n enabled = true,\n}: UseInfiniteAssetsOptions = {}) => {\n const [page, setPage] = useState(1);\n const lastResultsRef = useRef<File[]>([]);\n const lastPaginationRef = useRef<Pagination | undefined>(undefined);\n const isMountRef = useRef(true);\n const isSearchMountRef = useRef(true);\n\n const {\n currentData: data,\n isLoading,\n isFetching,\n error,\n } = useGetAssetsQuery(\n {\n folder,\n page,\n pageSize: PAGE_SIZE,\n sort,\n search,\n filters,\n },\n { skip: !enabled }\n );\n\n const pagination = data?.pagination;\n\n if (pagination) {\n lastPaginationRef.current = pagination;\n }\n\n // Accumulate pages. When cache is invalidated the current page is refetched\n // detect this and reset to avoid a gap in the results.\n const assets = useMemo(() => {\n if (!data) {\n return lastResultsRef.current;\n }\n\n const currentPageResults = data.results;\n\n if (page === 1) {\n lastResultsRef.current = currentPageResults;\n } else {\n // If accumulated length doesn't match expectation, cache was cleared\n const expectedPrior = (page - 1) * PAGE_SIZE;\n if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {\n return lastResultsRef.current;\n }\n\n // Only append if these aren't already accumulated\n if (lastResultsRef.current.length < page * PAGE_SIZE) {\n lastResultsRef.current = [...lastResultsRef.current, ...currentPageResults];\n }\n }\n\n return lastResultsRef.current;\n }, [data, page]);\n\n // Reset on filter/sort change — skip the initial mount since the memo\n // already handles page 1 correctly\n useEffect(() => {\n if (isMountRef.current) {\n isMountRef.current = false;\n\n return;\n }\n setPage(1);\n lastResultsRef.current = [];\n }, [folder, sort, filtersKey]);\n\n // A search change resets pagination but keeps the previous results rendered\n // until the new page 1 lands, which replaces them.\n useEffect(() => {\n if (isSearchMountRef.current) {\n isSearchMountRef.current = false;\n\n return;\n }\n setPage(1);\n }, [search]);\n\n // Deliberately the live value, not the stale-tolerant one below: paging must\n // never be driven by a total that belongs to the previous query.\n const hasNextPage = pagination ? page < pagination.pageCount : false;\n const isFetchingMore = isFetching && page > 1;\n\n const fetchNextPage = useCallback(() => {\n setPage((prev) => prev + 1);\n }, []);\n\n if (!enabled) {\n return {\n assets: [] as File[],\n pagination: undefined,\n isLoading: false,\n isFetchingMore: false,\n hasNextPage: false,\n fetchNextPage,\n error: undefined,\n };\n }\n\n return {\n assets,\n // Reported alongside the stale `assets` above so a consumer showing the total\n // doesn't flash zero mid-transition.\n pagination: pagination ?? lastPaginationRef.current,\n isLoading,\n isFetchingMore,\n hasNextPage,\n fetchNextPage,\n error,\n };\n};\n\nexport { useInfiniteAssets };\nexport { PAGE_SIZE };\n"],"names":["PAGE_SIZE","useInfiniteAssets","folder","sort","search","filters","filtersKey","enabled","page","setPage","useState","lastResultsRef","useRef","lastPaginationRef","undefined","isMountRef","isSearchMountRef","currentData","data","isLoading","isFetching","error","useGetAssetsQuery","pageSize","skip","pagination","current","assets","useMemo","currentPageResults","results","expectedPrior","length","useEffect","hasNextPage","pageCount","isFetchingMore","fetchNextPage","useCallback","prev"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useInfiniteAssets.mjs","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteAssets.ts"],"sourcesContent":["import { createElement, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useDispatch, useStore } from 'react-redux';\n\nimport { uploadApi } from '../../../services/api';\nimport { useGetAssetsQuery } from '../../../services/assets';\n\nimport type { File, Pagination } from '../../../../../../shared/contracts/files';\n\nconst PAGE_SIZE = 20;\n\ninterface UseInfiniteAssetsOptions {\n folder?: number | null;\n sort?: string;\n search?: string;\n /** Extra `filters[$and]` entries for the files query. */\n filters?: Record<string, unknown>[];\n /** Structural switch: an `is [folder]`-only type badge means no file can match. */\n enabled?: boolean;\n}\n\ninterface QueryArgs {\n folder: number | null;\n sort?: string;\n search?: string;\n filters?: Record<string, unknown>[];\n}\n\n/**\n * Everything loaded for one query identity.\n *\n * `queryKey` lags the live key while a new query is in flight — that lag is\n * what keeps the previous results on screen across a search change.\n */\ninterface Accumulated {\n queryKey: string;\n listKey: string;\n pages: Record<number, File[]>;\n pagination?: Pagination;\n}\n\ninterface PageState {\n queryKey: string;\n page: number;\n}\n\ntype PageRefreshedHandler = (page: number, results: File[]) => void;\n\n/**\n * Concatenates the pages in ascending order, one entry per asset id.\n *\n * `Map.set` keeps the first insertion position but takes the latest value, so\n * an asset that moved between pages after a mutation holds its place and shows\n * fresh data instead of rendering twice under a duplicate key.\n */\nconst flattenPages = (pages: Record<number, File[]>): File[] => {\n const byId = new Map<number, File>();\n\n for (const page of Object.keys(pages)\n .map(Number)\n .sort((a, b) => a - b)) {\n for (const asset of pages[page]) {\n byId.set(asset.id, asset);\n }\n }\n\n return [...byId.values()];\n};\n\n/**\n * Keeps one earlier page's `getAssets` query subscribed and reports its results\n * back up whenever they change.\n *\n * The hook itself only subscribes the current (latest) page. Without these, a\n * mutation invalidating `{Asset, LIST}` refetches that page alone and earlier\n * pages stay frozen — a rename or delete on an earlier page never reaches the\n * list until a folder/sort/search change. One subscriber per earlier\n * page keeps them all subscribed so the invalidation refetches the whole list.\n * Renders nothing; it exists only for the subscription + `onRefreshed` report.\n */\nconst PageSubscriber = ({\n queryArgs,\n page,\n onRefreshed,\n}: {\n queryArgs: QueryArgs;\n page: number;\n onRefreshed: PageRefreshedHandler;\n}) => {\n const { currentData } = useGetAssetsQuery({ ...queryArgs, page, pageSize: PAGE_SIZE });\n const results = currentData?.results;\n\n useEffect(() => {\n if (results) {\n onRefreshed(page, results);\n }\n // `results` is a stable reference per cache entry, so this only re-reports\n // when a refetch actually replaces the page's data. Reporting the same\n // reference the accumulator already holds is a no-op (see `onRefreshed`).\n }, [results, page, onRefreshed]);\n\n return null;\n};\n\n/**\n * Infinite scroll over `/upload/files`, keeping one accumulated slot per page.\n *\n * The caller MUST render the returned `subscribers` node — that is what keeps\n * every loaded page subscribed, so `{Asset, LIST}` invalidations keep the whole\n * list consistent rather than only the current page.\n */\nconst useInfiniteAssets = ({\n folder = null,\n sort,\n search,\n filters,\n enabled = true,\n}: UseInfiniteAssetsOptions = {}) => {\n // Derived from the request args themselves, so a new filter can't reach the\n // API without also invalidating what was accumulated under the old one.\n const queryArgs: QueryArgs = { folder, sort, search, filters };\n const queryKey = JSON.stringify(queryArgs);\n // Identifies the list being viewed, ignoring the search term. Folder, sort\n // and filter changes are single committed actions, so one clean reset per\n // change is honest. Search is committed per keystroke on a 300ms debounce,\n // and blanking the list on each one would strobe as the user types.\n // The cost: through the search window the previous rows stay rendered and\n // `hasNextPage` is false, so the list reads as complete until page 1 lands.\n const listKey = JSON.stringify({ folder, sort, filters });\n\n const [pageState, setPageState] = useState<PageState>({ queryKey, page: 1 });\n const [accumulated, setAccumulated] = useState<Accumulated>({ queryKey, listKey, pages: {} });\n\n // Derived rather than read straight from state: the render that discovers\n // the key change still reaches the query below, and a list with no page 1\n // must never be requested at page 2.\n const page = pageState.queryKey === queryKey ? pageState.page : 1;\n\n if (pageState.queryKey !== queryKey) {\n setPageState({ queryKey, page: 1 });\n }\n\n const { currentData, isLoading, isFetching, error } = useGetAssetsQuery(\n {\n ...queryArgs,\n page,\n pageSize: PAGE_SIZE,\n },\n { skip: !enabled }\n );\n\n const isSameQuery = accumulated.queryKey === queryKey;\n\n // `currentData` is only ever the payload for the args just passed, so it\n // belongs to this key at this page — nothing to infer. Its reference is\n // stable per cache entry, so this settles after one extra render.\n if (currentData && (!isSameQuery || accumulated.pages[page] !== currentData.results)) {\n setAccumulated(\n isSameQuery\n ? {\n ...accumulated,\n pages: { ...accumulated.pages, [page]: currentData.results },\n pagination: currentData.pagination,\n }\n : {\n queryKey,\n listKey,\n pages: { [page]: currentData.results },\n pagination: currentData.pagination,\n }\n );\n }\n\n // Merges a refetched earlier page back into the accumulator. Ignores reports\n // for a superseded query, and no-ops when the reference is unchanged so a\n // subscriber re-reporting its already-stored page can't loop.\n const handlePageRefreshed = useCallback<PageRefreshedHandler>(\n (refreshedPage, results) => {\n setAccumulated((prev) => {\n if (prev.queryKey !== queryKey || prev.pages[refreshedPage] === results) {\n return prev;\n }\n\n return { ...prev, pages: { ...prev.pages, [refreshedPage]: results } };\n });\n },\n [queryKey]\n );\n\n // One subscriber per earlier page (the current page is already subscribed by\n // the query above). Keyed by queryKey so a folder/sort/search change remounts\n // them against the new args instead of reusing a stale cache entry.\n // `createElement` rather than JSX so this stays a plain `.ts` hook file.\n const subscribers = createElement(\n Fragment,\n null,\n Array.from({ length: Math.max(0, page - 1) }, (_, index) => index + 1).map((earlierPage) =>\n createElement(PageSubscriber, {\n key: `${queryKey}:${earlierPage}`,\n queryArgs,\n page: earlierPage,\n onRefreshed: handlePageRefreshed,\n })\n )\n );\n\n // Drop the left folder's cached `getAssets` pages when the folder changes.\n //\n // RTK Query 1.9.7 leaves a query's store subscription in place when its last\n // subscriber unmounts while a refetch is still in flight — e.g. a delete\n // invalidates `{Asset, LIST}`, every loaded page starts refetching, and the\n // user navigates away before those settle. Those stale subscriptions make the\n // next `{Asset, LIST}` invalidation (an upload in the new folder) refetch the\n // *previous* folder's pages: one bogus `/upload/files` request per loaded\n // page. There is no public per-entry unsubscribe, so evict the left folder's\n // entries outright — returning reloads from page 1, which the accumulator\n // already does on a list change.\n const dispatch = useDispatch();\n const store = useStore();\n const prevFolderRef = useRef(folder);\n useEffect(() => {\n const prevFolder = prevFolderRef.current;\n prevFolderRef.current = folder;\n if (prevFolder === folder) {\n return;\n }\n\n const apiState = (store.getState() as Record<string, { queries?: Record<string, unknown> }>)[\n uploadApi.reducerPath\n ];\n const removeQueryResult = (\n uploadApi as unknown as {\n internalActions: {\n removeQueryResult: (payload: { queryCacheKey: string }) => { type: string };\n };\n }\n ).internalActions.removeQueryResult;\n\n Object.keys(apiState?.queries ?? {}).forEach((cacheKey) => {\n if (!cacheKey.startsWith('getAssets(')) {\n return;\n }\n let args: { folder?: number | null };\n try {\n args = JSON.parse(cacheKey.slice('getAssets('.length, -1));\n } catch {\n return;\n }\n if (args.folder === prevFolder) {\n dispatch(removeQueryResult({ queryCacheKey: cacheKey }));\n }\n });\n }, [folder, dispatch, store]);\n\n // Until the new list's first page lands the accumulator still holds the\n // previous folder. Reported as loading so the page shows a spinner instead\n // of the outgoing folder's assets under the incoming folder's header.\n const isChangingList = accumulated.listKey !== listKey;\n\n const assets = useMemo(\n () => (isChangingList ? [] : flattenPages(accumulated.pages)),\n [isChangingList, accumulated.pages]\n );\n\n // Deliberately the live value, not the stale-tolerant one below: paging must\n // never be driven by a total that belongs to the previous query.\n const hasNextPage = currentData ? page < currentData.pagination.pageCount : false;\n const isFetchingMore = isFetching && page > 1;\n\n const fetchNextPage = useCallback(() => {\n setPageState((prev) => ({\n queryKey,\n page: (prev.queryKey === queryKey ? prev.page : 1) + 1,\n }));\n }, [queryKey]);\n\n if (!enabled) {\n return {\n assets: [] as File[],\n // No list to keep fresh while disabled, so nothing to subscribe.\n subscribers: null,\n pagination: undefined,\n isLoading: false,\n isFetchingMore: false,\n hasNextPage: false,\n fetchNextPage,\n error: undefined,\n };\n }\n\n return {\n assets,\n subscribers,\n // Falls back to the accumulated total so a consumer showing the count\n // doesn't flash zero mid-transition, matching the possibly stale `assets`.\n pagination: currentData?.pagination ?? accumulated.pagination,\n isLoading: isLoading || isChangingList,\n isFetchingMore,\n hasNextPage,\n fetchNextPage,\n error,\n };\n};\n\nexport { useInfiniteAssets };\nexport { PAGE_SIZE };\n"],"names":["PAGE_SIZE","flattenPages","pages","byId","Map","page","Object","keys","map","Number","sort","a","b","asset","set","id","values","PageSubscriber","queryArgs","onRefreshed","currentData","useGetAssetsQuery","pageSize","results","useEffect","useInfiniteAssets","folder","search","filters","enabled","queryKey","JSON","stringify","listKey","pageState","setPageState","useState","accumulated","setAccumulated","isLoading","isFetching","error","skip","isSameQuery","pagination","handlePageRefreshed","useCallback","refreshedPage","prev","subscribers","createElement","Fragment","Array","from","length","Math","max","_","index","earlierPage","key","dispatch","useDispatch","store","useStore","prevFolderRef","useRef","prevFolder","current","apiState","getState","uploadApi","reducerPath","removeQueryResult","internalActions","queries","forEach","cacheKey","startsWith","args","parse","slice","queryCacheKey","isChangingList","assets","useMemo","hasNextPage","pageCount","isFetchingMore","fetchNextPage","undefined"],"mappings":";;;;;AASA,MAAMA,SAAAA,GAAY;AAuClB;;;;;;IAOA,MAAMC,eAAe,CAACC,KAAAA,GAAAA;AACpB,IAAA,MAAMC,OAAO,IAAIC,GAAAA,EAAAA;AAEjB,IAAA,KAAK,MAAMC,IAAAA,IAAQC,MAAAA,CAAOC,IAAI,CAACL,KAAAA,CAAAA,CAC5BM,GAAG,CAACC,MAAAA,CAAAA,CACJC,IAAI,CAAC,CAACC,CAAAA,EAAGC,CAAAA,GAAMD,IAAIC,CAAAA,CAAAA,CAAI;AACxB,QAAA,KAAK,MAAMC,KAAAA,IAASX,KAAK,CAACG,KAAK,CAAE;AAC/BF,YAAAA,IAAAA,CAAKW,GAAG,CAACD,KAAAA,CAAME,EAAE,EAAEF,KAAAA,CAAAA;AACrB,QAAA;AACF,IAAA;IAEA,OAAO;AAAIV,QAAAA,GAAAA,IAAAA,CAAKa,MAAM;AAAG,KAAA;AAC3B,CAAA;AAEA;;;;;;;;;;IAWA,MAAMC,iBAAiB,CAAC,EACtBC,SAAS,EACTb,IAAI,EACJc,WAAW,EAKZ,GAAA;AACC,IAAA,MAAM,EAAEC,WAAW,EAAE,GAAGC,iBAAAA,CAAkB;AAAE,QAAA,GAAGH,SAAS;AAAEb,QAAAA,IAAAA;QAAMiB,QAAAA,EAAUtB;AAAU,KAAA,CAAA;AACpF,IAAA,MAAMuB,UAAUH,WAAAA,EAAaG,OAAAA;IAE7BC,SAAAA,CAAU,IAAA;AACR,QAAA,IAAID,OAAAA,EAAS;AACXJ,YAAAA,WAAAA,CAAYd,IAAAA,EAAMkB,OAAAA,CAAAA;AACpB,QAAA;;;;IAIF,CAAA,EAAG;AAACA,QAAAA,OAAAA;AAASlB,QAAAA,IAAAA;AAAMc,QAAAA;AAAY,KAAA,CAAA;IAE/B,OAAO,IAAA;AACT,CAAA;AAEA;;;;;;AAMC,UACKM,iBAAAA,GAAoB,CAAC,EACzBC,MAAAA,GAAS,IAAI,EACbhB,IAAI,EACJiB,MAAM,EACNC,OAAO,EACPC,OAAAA,GAAU,IAAI,EACW,GAAG,EAAE,GAAA;;;AAG9B,IAAA,MAAMX,SAAAA,GAAuB;AAAEQ,QAAAA,MAAAA;AAAQhB,QAAAA,IAAAA;AAAMiB,QAAAA,MAAAA;AAAQC,QAAAA;AAAQ,KAAA;IAC7D,MAAME,QAAAA,GAAWC,IAAAA,CAAKC,SAAS,CAACd,SAAAA,CAAAA;;;;;;;IAOhC,MAAMe,OAAAA,GAAUF,IAAAA,CAAKC,SAAS,CAAC;AAAEN,QAAAA,MAAAA;AAAQhB,QAAAA,IAAAA;AAAMkB,QAAAA;AAAQ,KAAA,CAAA;AAEvD,IAAA,MAAM,CAACM,SAAAA,EAAWC,YAAAA,CAAa,GAAGC,QAAAA,CAAoB;AAAEN,QAAAA,QAAAA;QAAUzB,IAAAA,EAAM;AAAE,KAAA,CAAA;AAC1E,IAAA,MAAM,CAACgC,WAAAA,EAAaC,cAAAA,CAAe,GAAGF,QAAAA,CAAsB;AAAEN,QAAAA,QAAAA;AAAUG,QAAAA,OAAAA;AAAS/B,QAAAA,KAAAA,EAAO;AAAG,KAAA,CAAA;;;;AAK3F,IAAA,MAAMG,OAAO6B,SAAAA,CAAUJ,QAAQ,KAAKA,QAAAA,GAAWI,SAAAA,CAAU7B,IAAI,GAAG,CAAA;IAEhE,IAAI6B,SAAAA,CAAUJ,QAAQ,KAAKA,QAAAA,EAAU;QACnCK,YAAAA,CAAa;AAAEL,YAAAA,QAAAA;YAAUzB,IAAAA,EAAM;AAAE,SAAA,CAAA;AACnC,IAAA;IAEA,MAAM,EAAEe,WAAW,EAAEmB,SAAS,EAAEC,UAAU,EAAEC,KAAK,EAAE,GAAGpB,iBAAAA,CACpD;AACE,QAAA,GAAGH,SAAS;AACZb,QAAAA,IAAAA;QACAiB,QAAAA,EAAUtB;KACZ,EACA;AAAE0C,QAAAA,IAAAA,EAAM,CAACb;AAAQ,KAAA,CAAA;IAGnB,MAAMc,WAAAA,GAAcN,WAAAA,CAAYP,QAAQ,KAAKA,QAAAA;;;;AAK7C,IAAA,IAAIV,WAAAA,KAAgB,CAACuB,WAAAA,IAAeN,WAAAA,CAAYnC,KAAK,CAACG,IAAAA,CAAK,KAAKe,WAAAA,CAAYG,OAAM,CAAA,EAAI;AACpFe,QAAAA,cAAAA,CACEK,WAAAA,GACI;AACE,YAAA,GAAGN,WAAW;YACdnC,KAAAA,EAAO;AAAE,gBAAA,GAAGmC,YAAYnC,KAAK;gBAAE,CAACG,IAAAA,GAAOe,WAAAA,CAAYG;AAAQ,aAAA;AAC3DqB,YAAAA,UAAAA,EAAYxB,YAAYwB;SAC1B,GACA;AACEd,YAAAA,QAAAA;AACAG,YAAAA,OAAAA;YACA/B,KAAAA,EAAO;gBAAE,CAACG,IAAAA,GAAOe,WAAAA,CAAYG;AAAQ,aAAA;AACrCqB,YAAAA,UAAAA,EAAYxB,YAAYwB;AAC1B,SAAA,CAAA;AAER,IAAA;;;;IAKA,MAAMC,mBAAAA,GAAsBC,WAAAA,CAC1B,CAACC,aAAAA,EAAexB,OAAAA,GAAAA;AACde,QAAAA,cAAAA,CAAe,CAACU,IAAAA,GAAAA;YACd,IAAIA,IAAAA,CAAKlB,QAAQ,KAAKA,QAAAA,IAAYkB,KAAK9C,KAAK,CAAC6C,aAAAA,CAAc,KAAKxB,OAAAA,EAAS;gBACvE,OAAOyB,IAAAA;AACT,YAAA;YAEA,OAAO;AAAE,gBAAA,GAAGA,IAAI;gBAAE9C,KAAAA,EAAO;AAAE,oBAAA,GAAG8C,KAAK9C,KAAK;AAAE,oBAAA,CAAC6C,gBAAgBxB;AAAQ;AAAE,aAAA;AACvE,QAAA,CAAA,CAAA;IACF,CAAA,EACA;AAACO,QAAAA;AAAS,KAAA,CAAA;;;;;AAOZ,IAAA,MAAMmB,cAAcC,aAAAA,CAClBC,QAAAA,EACA,IAAA,EACAC,KAAAA,CAAMC,IAAI,CAAC;AAAEC,QAAAA,MAAAA,EAAQC,IAAAA,CAAKC,GAAG,CAAC,CAAA,EAAGnD,IAAAA,GAAO,CAAA;KAAG,EAAG,CAACoD,CAAAA,EAAGC,KAAAA,GAAUA,KAAAA,GAAQ,CAAA,CAAA,CAAGlD,GAAG,CAAC,CAACmD,WAAAA,GAC1ET,aAAAA,CAAcjC,cAAAA,EAAgB;AAC5B2C,YAAAA,GAAAA,EAAK,CAAA,EAAG9B,QAAAA,CAAS,CAAC,EAAE6B,WAAAA,CAAAA,CAAa;AACjCzC,YAAAA,SAAAA;YACAb,IAAAA,EAAMsD,WAAAA;YACNxC,WAAAA,EAAa0B;AACf,SAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;AAeJ,IAAA,MAAMgB,QAAAA,GAAWC,WAAAA,EAAAA;AACjB,IAAA,MAAMC,KAAAA,GAAQC,QAAAA,EAAAA;AACd,IAAA,MAAMC,gBAAgBC,MAAAA,CAAOxC,MAAAA,CAAAA;IAC7BF,SAAAA,CAAU,IAAA;QACR,MAAM2C,UAAAA,GAAaF,cAAcG,OAAO;AACxCH,QAAAA,aAAAA,CAAcG,OAAO,GAAG1C,MAAAA;AACxB,QAAA,IAAIyC,eAAezC,MAAAA,EAAQ;AACzB,YAAA;AACF,QAAA;QAEA,MAAM2C,QAAAA,GAAW,KAACN,CAAMO,QAAQ,EAA4D,CAC1FC,SAAAA,CAAUC,WAAW,CACtB;AACD,QAAA,MAAMC,iBAAAA,GACJF,SAAAA,CAKAG,eAAe,CAACD,iBAAiB;QAEnCnE,MAAAA,CAAOC,IAAI,CAAC8D,QAAAA,EAAUM,OAAAA,IAAW,EAAC,CAAA,CAAGC,OAAO,CAAC,CAACC,QAAAA,GAAAA;AAC5C,YAAA,IAAI,CAACA,QAAAA,CAASC,UAAU,CAAC,YAAA,CAAA,EAAe;AACtC,gBAAA;AACF,YAAA;YACA,IAAIC,IAAAA;YACJ,IAAI;gBACFA,IAAAA,GAAOhD,IAAAA,CAAKiD,KAAK,CAACH,QAAAA,CAASI,KAAK,CAAC,YAAA,CAAa3B,MAAM,EAAE,CAAC,CAAA,CAAA,CAAA;AACzD,YAAA,CAAA,CAAE,OAAM;AACN,gBAAA;AACF,YAAA;YACA,IAAIyB,IAAAA,CAAKrD,MAAM,KAAKyC,UAAAA,EAAY;AAC9BN,gBAAAA,QAAAA,CAASY,iBAAAA,CAAkB;oBAAES,aAAAA,EAAeL;AAAS,iBAAA,CAAA,CAAA;AACvD,YAAA;AACF,QAAA,CAAA,CAAA;IACF,CAAA,EAAG;AAACnD,QAAAA,MAAAA;AAAQmC,QAAAA,QAAAA;AAAUE,QAAAA;AAAM,KAAA,CAAA;;;;IAK5B,MAAMoB,cAAAA,GAAiB9C,WAAAA,CAAYJ,OAAO,KAAKA,OAAAA;IAE/C,MAAMmD,MAAAA,GAASC,QACb,IAAOF,cAAAA,GAAiB,EAAE,GAAGlF,YAAAA,CAAaoC,WAAAA,CAAYnC,KAAK,CAAA,EAC3D;AAACiF,QAAAA,cAAAA;AAAgB9C,QAAAA,WAAAA,CAAYnC;AAAM,KAAA,CAAA;;;AAKrC,IAAA,MAAMoF,cAAclE,WAAAA,GAAcf,IAAAA,GAAOe,YAAYwB,UAAU,CAAC2C,SAAS,GAAG,KAAA;IAC5E,MAAMC,cAAAA,GAAiBhD,cAAcnC,IAAAA,GAAO,CAAA;AAE5C,IAAA,MAAMoF,gBAAgB3C,WAAAA,CAAY,IAAA;QAChCX,YAAAA,CAAa,CAACa,QAAU;AACtBlB,gBAAAA,QAAAA;gBACAzB,IAAAA,EAAO2C,CAAAA,IAAAA,CAAKlB,QAAQ,KAAKA,WAAWkB,IAAAA,CAAK3C,IAAI,GAAG,CAAA,IAAK;aACvD,CAAA,CAAA;IACF,CAAA,EAAG;AAACyB,QAAAA;AAAS,KAAA,CAAA;AAEb,IAAA,IAAI,CAACD,OAAAA,EAAS;QACZ,OAAO;AACLuD,YAAAA,MAAAA,EAAQ,EAAE;;YAEVnC,WAAAA,EAAa,IAAA;YACbL,UAAAA,EAAY8C,SAAAA;YACZnD,SAAAA,EAAW,KAAA;YACXiD,cAAAA,EAAgB,KAAA;YAChBF,WAAAA,EAAa,KAAA;AACbG,YAAAA,aAAAA;YACAhD,KAAAA,EAAOiD;AACT,SAAA;AACF,IAAA;IAEA,OAAO;AACLN,QAAAA,MAAAA;AACAnC,QAAAA,WAAAA;;;QAGAL,UAAAA,EAAYxB,WAAAA,EAAawB,UAAAA,IAAcP,WAAAA,CAAYO,UAAU;AAC7DL,QAAAA,SAAAA,EAAWA,SAAAA,IAAa4C,cAAAA;AACxBK,QAAAA,cAAAA;AACAF,QAAAA,WAAAA;AACAG,QAAAA,aAAAA;AACAhD,QAAAA;AACF,KAAA;AACF;;;;"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Infinite-scroll trigger for the media library list.
|
|
7
|
+
*
|
|
8
|
+
* Returns a callback ref to place on a sentinel element rendered after the last
|
|
9
|
+
* row. Three deliberate choices:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Loading is driven only by fresh observer readings.** A fetch fires from
|
|
12
|
+
* the `IntersectionObserver` callback — never from a state/prop change — so
|
|
13
|
+
* it can't react to a stale `isIntersecting` from before newly loaded rows
|
|
14
|
+
* rendered (which fetched an extra page on every trigger).
|
|
15
|
+
*
|
|
16
|
+
* 2. **Each settled page re-observes the sentinel.** An `IntersectionObserver`
|
|
17
|
+
* only fires on a *transition*. When the sentinel stays on screen (a page
|
|
18
|
+
* that doesn't fill the viewport — tall viewport, short page, grid fitting
|
|
19
|
+
* the whole page) it would never fire again and loading would stall. So when
|
|
20
|
+
* `isFetchingMore` settles back to false we re-observe, forcing a fresh
|
|
21
|
+
* reading against the new layout: still on screen → load the next page;
|
|
22
|
+
* viewport now filled → stop.
|
|
23
|
+
*
|
|
24
|
+
* 3. **A callback ref, not a stable object ref.** The sentinel only mounts once
|
|
25
|
+
* the first page has rendered (the list shows a loader before that). A
|
|
26
|
+
* callback ref (re)binds the observer exactly when the node mounts/unmounts,
|
|
27
|
+
* so the observer can't miss a sentinel that appears after the hook first ran.
|
|
28
|
+
*/ const useInfiniteScrollSentinel = ({ hasNextPage, isFetchingMore, onLoadMore, options })=>{
|
|
29
|
+
const observerRef = React.useRef(null);
|
|
30
|
+
const nodeRef = React.useRef(null);
|
|
31
|
+
// Read through refs so the callback ref stays stable and the observer callback
|
|
32
|
+
// always sees the latest values. `options` is read once at attach time (see
|
|
33
|
+
// its JSDoc); `onLoadMore` need not be stable.
|
|
34
|
+
const optionsRef = React.useRef(options);
|
|
35
|
+
optionsRef.current = options;
|
|
36
|
+
const onLoadMoreRef = React.useRef(onLoadMore);
|
|
37
|
+
onLoadMoreRef.current = onLoadMore;
|
|
38
|
+
const hasNextPageRef = React.useRef(hasNextPage);
|
|
39
|
+
hasNextPageRef.current = hasNextPage;
|
|
40
|
+
const isFetchingMoreRef = React.useRef(isFetchingMore);
|
|
41
|
+
isFetchingMoreRef.current = isFetchingMore;
|
|
42
|
+
const sentinelRef = React.useCallback((node)=>{
|
|
43
|
+
observerRef.current?.disconnect();
|
|
44
|
+
nodeRef.current = node;
|
|
45
|
+
if (!node) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const observer = new IntersectionObserver(([entry])=>{
|
|
49
|
+
if (entry.isIntersecting && hasNextPageRef.current && !isFetchingMoreRef.current) {
|
|
50
|
+
onLoadMoreRef.current();
|
|
51
|
+
}
|
|
52
|
+
}, optionsRef.current);
|
|
53
|
+
observer.observe(node);
|
|
54
|
+
observerRef.current = observer;
|
|
55
|
+
}, []);
|
|
56
|
+
React.useEffect(()=>()=>observerRef.current?.disconnect(), []);
|
|
57
|
+
// Once a page settles, re-observe so a *fresh* reading against the new layout
|
|
58
|
+
// decides whether to keep loading — rather than a stale `isIntersecting` from
|
|
59
|
+
// before the rows rendered. Re-observing re-emits the current intersection
|
|
60
|
+
// even without a transition, which is what keeps loading a viewport that a
|
|
61
|
+
// single page didn't fill.
|
|
62
|
+
React.useEffect(()=>{
|
|
63
|
+
if (isFetchingMore || !observerRef.current || !nodeRef.current) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
observerRef.current.unobserve(nodeRef.current);
|
|
67
|
+
observerRef.current.observe(nodeRef.current);
|
|
68
|
+
}, [
|
|
69
|
+
isFetchingMore
|
|
70
|
+
]);
|
|
71
|
+
return sentinelRef;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
exports.useInfiniteScrollSentinel = useInfiniteScrollSentinel;
|
|
75
|
+
//# sourceMappingURL=useInfiniteScrollSentinel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInfiniteScrollSentinel.js","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteScrollSentinel.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\n\ninterface UseInfiniteScrollSentinelArgs {\n /** Whether there is a further page to request. */\n hasNextPage: boolean;\n /** Whether a subsequent page is already in flight. */\n isFetchingMore: boolean;\n /** Called to request the next page. Held in a ref, so it need not be stable. */\n onLoadMore: () => void;\n /**\n * Passed to the `IntersectionObserver`. Read once, when the sentinel node\n * attaches — changing it afterwards does NOT rebuild the observer, so a\n * later `threshold`/`rootMargin` is silently ignored. Pass a stable, ideally\n * module-level, object.\n */\n options?: IntersectionObserverInit;\n}\n\n/**\n * Infinite-scroll trigger for the media library list.\n *\n * Returns a callback ref to place on a sentinel element rendered after the last\n * row. Three deliberate choices:\n *\n * 1. **Loading is driven only by fresh observer readings.** A fetch fires from\n * the `IntersectionObserver` callback — never from a state/prop change — so\n * it can't react to a stale `isIntersecting` from before newly loaded rows\n * rendered (which fetched an extra page on every trigger).\n *\n * 2. **Each settled page re-observes the sentinel.** An `IntersectionObserver`\n * only fires on a *transition*. When the sentinel stays on screen (a page\n * that doesn't fill the viewport — tall viewport, short page, grid fitting\n * the whole page) it would never fire again and loading would stall. So when\n * `isFetchingMore` settles back to false we re-observe, forcing a fresh\n * reading against the new layout: still on screen → load the next page;\n * viewport now filled → stop.\n *\n * 3. **A callback ref, not a stable object ref.** The sentinel only mounts once\n * the first page has rendered (the list shows a loader before that). A\n * callback ref (re)binds the observer exactly when the node mounts/unmounts,\n * so the observer can't miss a sentinel that appears after the hook first ran.\n */\nexport const useInfiniteScrollSentinel = ({\n hasNextPage,\n isFetchingMore,\n onLoadMore,\n options,\n}: UseInfiniteScrollSentinelArgs) => {\n const observerRef = useRef<IntersectionObserver | null>(null);\n const nodeRef = useRef<HTMLElement | null>(null);\n\n // Read through refs so the callback ref stays stable and the observer callback\n // always sees the latest values. `options` is read once at attach time (see\n // its JSDoc); `onLoadMore` need not be stable.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const onLoadMoreRef = useRef(onLoadMore);\n onLoadMoreRef.current = onLoadMore;\n const hasNextPageRef = useRef(hasNextPage);\n hasNextPageRef.current = hasNextPage;\n const isFetchingMoreRef = useRef(isFetchingMore);\n isFetchingMoreRef.current = isFetchingMore;\n\n const sentinelRef = useCallback((node: HTMLElement | null) => {\n observerRef.current?.disconnect();\n nodeRef.current = node;\n\n if (!node) {\n return;\n }\n\n const observer = new IntersectionObserver(([entry]) => {\n if (entry.isIntersecting && hasNextPageRef.current && !isFetchingMoreRef.current) {\n onLoadMoreRef.current();\n }\n }, optionsRef.current);\n\n observer.observe(node);\n observerRef.current = observer;\n }, []);\n\n useEffect(() => () => observerRef.current?.disconnect(), []);\n\n // Once a page settles, re-observe so a *fresh* reading against the new layout\n // decides whether to keep loading — rather than a stale `isIntersecting` from\n // before the rows rendered. Re-observing re-emits the current intersection\n // even without a transition, which is what keeps loading a viewport that a\n // single page didn't fill.\n useEffect(() => {\n if (isFetchingMore || !observerRef.current || !nodeRef.current) {\n return;\n }\n\n observerRef.current.unobserve(nodeRef.current);\n observerRef.current.observe(nodeRef.current);\n }, [isFetchingMore]);\n\n return sentinelRef;\n};\n"],"names":["useInfiniteScrollSentinel","hasNextPage","isFetchingMore","onLoadMore","options","observerRef","useRef","nodeRef","optionsRef","current","onLoadMoreRef","hasNextPageRef","isFetchingMoreRef","sentinelRef","useCallback","node","disconnect","observer","IntersectionObserver","entry","isIntersecting","observe","useEffect","unobserve"],"mappings":";;;;AAkBA;;;;;;;;;;;;;;;;;;;;;;;AAuBC,IACM,MAAMA,yBAAAA,GAA4B,CAAC,EACxCC,WAAW,EACXC,cAAc,EACdC,UAAU,EACVC,OAAO,EACuB,GAAA;AAC9B,IAAA,MAAMC,cAAcC,YAAAA,CAAoC,IAAA,CAAA;AACxD,IAAA,MAAMC,UAAUD,YAAAA,CAA2B,IAAA,CAAA;;;;AAK3C,IAAA,MAAME,aAAaF,YAAAA,CAAOF,OAAAA,CAAAA;AAC1BI,IAAAA,UAAAA,CAAWC,OAAO,GAAGL,OAAAA;AACrB,IAAA,MAAMM,gBAAgBJ,YAAAA,CAAOH,UAAAA,CAAAA;AAC7BO,IAAAA,aAAAA,CAAcD,OAAO,GAAGN,UAAAA;AACxB,IAAA,MAAMQ,iBAAiBL,YAAAA,CAAOL,WAAAA,CAAAA;AAC9BU,IAAAA,cAAAA,CAAeF,OAAO,GAAGR,WAAAA;AACzB,IAAA,MAAMW,oBAAoBN,YAAAA,CAAOJ,cAAAA,CAAAA;AACjCU,IAAAA,iBAAAA,CAAkBH,OAAO,GAAGP,cAAAA;IAE5B,MAAMW,WAAAA,GAAcC,kBAAY,CAACC,IAAAA,GAAAA;AAC/BV,QAAAA,WAAAA,CAAYI,OAAO,EAAEO,UAAAA,EAAAA;AACrBT,QAAAA,OAAAA,CAAQE,OAAO,GAAGM,IAAAA;AAElB,QAAA,IAAI,CAACA,IAAAA,EAAM;AACT,YAAA;AACF,QAAA;AAEA,QAAA,MAAME,QAAAA,GAAW,IAAIC,oBAAAA,CAAqB,CAAC,CAACC,KAAAA,CAAM,GAAA;YAChD,IAAIA,KAAAA,CAAMC,cAAc,IAAIT,cAAAA,CAAeF,OAAO,IAAI,CAACG,iBAAAA,CAAkBH,OAAO,EAAE;AAChFC,gBAAAA,aAAAA,CAAcD,OAAO,EAAA;AACvB,YAAA;AACF,QAAA,CAAA,EAAGD,WAAWC,OAAO,CAAA;AAErBQ,QAAAA,QAAAA,CAASI,OAAO,CAACN,IAAAA,CAAAA;AACjBV,QAAAA,WAAAA,CAAYI,OAAO,GAAGQ,QAAAA;AACxB,IAAA,CAAA,EAAG,EAAE,CAAA;AAELK,IAAAA,eAAAA,CAAU,IAAM,IAAMjB,WAAAA,CAAYI,OAAO,EAAEO,cAAc,EAAE,CAAA;;;;;;IAO3DM,eAAAA,CAAU,IAAA;QACR,IAAIpB,cAAAA,IAAkB,CAACG,WAAAA,CAAYI,OAAO,IAAI,CAACF,OAAAA,CAAQE,OAAO,EAAE;AAC9D,YAAA;AACF,QAAA;AAEAJ,QAAAA,WAAAA,CAAYI,OAAO,CAACc,SAAS,CAAChB,QAAQE,OAAO,CAAA;AAC7CJ,QAAAA,WAAAA,CAAYI,OAAO,CAACY,OAAO,CAACd,QAAQE,OAAO,CAAA;IAC7C,CAAA,EAAG;AAACP,QAAAA;AAAe,KAAA,CAAA;IAEnB,OAAOW,WAAAA;AACT;;;;"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Infinite-scroll trigger for the media library list.
|
|
5
|
+
*
|
|
6
|
+
* Returns a callback ref to place on a sentinel element rendered after the last
|
|
7
|
+
* row. Three deliberate choices:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Loading is driven only by fresh observer readings.** A fetch fires from
|
|
10
|
+
* the `IntersectionObserver` callback — never from a state/prop change — so
|
|
11
|
+
* it can't react to a stale `isIntersecting` from before newly loaded rows
|
|
12
|
+
* rendered (which fetched an extra page on every trigger).
|
|
13
|
+
*
|
|
14
|
+
* 2. **Each settled page re-observes the sentinel.** An `IntersectionObserver`
|
|
15
|
+
* only fires on a *transition*. When the sentinel stays on screen (a page
|
|
16
|
+
* that doesn't fill the viewport — tall viewport, short page, grid fitting
|
|
17
|
+
* the whole page) it would never fire again and loading would stall. So when
|
|
18
|
+
* `isFetchingMore` settles back to false we re-observe, forcing a fresh
|
|
19
|
+
* reading against the new layout: still on screen → load the next page;
|
|
20
|
+
* viewport now filled → stop.
|
|
21
|
+
*
|
|
22
|
+
* 3. **A callback ref, not a stable object ref.** The sentinel only mounts once
|
|
23
|
+
* the first page has rendered (the list shows a loader before that). A
|
|
24
|
+
* callback ref (re)binds the observer exactly when the node mounts/unmounts,
|
|
25
|
+
* so the observer can't miss a sentinel that appears after the hook first ran.
|
|
26
|
+
*/ const useInfiniteScrollSentinel = ({ hasNextPage, isFetchingMore, onLoadMore, options })=>{
|
|
27
|
+
const observerRef = useRef(null);
|
|
28
|
+
const nodeRef = useRef(null);
|
|
29
|
+
// Read through refs so the callback ref stays stable and the observer callback
|
|
30
|
+
// always sees the latest values. `options` is read once at attach time (see
|
|
31
|
+
// its JSDoc); `onLoadMore` need not be stable.
|
|
32
|
+
const optionsRef = useRef(options);
|
|
33
|
+
optionsRef.current = options;
|
|
34
|
+
const onLoadMoreRef = useRef(onLoadMore);
|
|
35
|
+
onLoadMoreRef.current = onLoadMore;
|
|
36
|
+
const hasNextPageRef = useRef(hasNextPage);
|
|
37
|
+
hasNextPageRef.current = hasNextPage;
|
|
38
|
+
const isFetchingMoreRef = useRef(isFetchingMore);
|
|
39
|
+
isFetchingMoreRef.current = isFetchingMore;
|
|
40
|
+
const sentinelRef = useCallback((node)=>{
|
|
41
|
+
observerRef.current?.disconnect();
|
|
42
|
+
nodeRef.current = node;
|
|
43
|
+
if (!node) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const observer = new IntersectionObserver(([entry])=>{
|
|
47
|
+
if (entry.isIntersecting && hasNextPageRef.current && !isFetchingMoreRef.current) {
|
|
48
|
+
onLoadMoreRef.current();
|
|
49
|
+
}
|
|
50
|
+
}, optionsRef.current);
|
|
51
|
+
observer.observe(node);
|
|
52
|
+
observerRef.current = observer;
|
|
53
|
+
}, []);
|
|
54
|
+
useEffect(()=>()=>observerRef.current?.disconnect(), []);
|
|
55
|
+
// Once a page settles, re-observe so a *fresh* reading against the new layout
|
|
56
|
+
// decides whether to keep loading — rather than a stale `isIntersecting` from
|
|
57
|
+
// before the rows rendered. Re-observing re-emits the current intersection
|
|
58
|
+
// even without a transition, which is what keeps loading a viewport that a
|
|
59
|
+
// single page didn't fill.
|
|
60
|
+
useEffect(()=>{
|
|
61
|
+
if (isFetchingMore || !observerRef.current || !nodeRef.current) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
observerRef.current.unobserve(nodeRef.current);
|
|
65
|
+
observerRef.current.observe(nodeRef.current);
|
|
66
|
+
}, [
|
|
67
|
+
isFetchingMore
|
|
68
|
+
]);
|
|
69
|
+
return sentinelRef;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { useInfiniteScrollSentinel };
|
|
73
|
+
//# sourceMappingURL=useInfiniteScrollSentinel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInfiniteScrollSentinel.mjs","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteScrollSentinel.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\n\ninterface UseInfiniteScrollSentinelArgs {\n /** Whether there is a further page to request. */\n hasNextPage: boolean;\n /** Whether a subsequent page is already in flight. */\n isFetchingMore: boolean;\n /** Called to request the next page. Held in a ref, so it need not be stable. */\n onLoadMore: () => void;\n /**\n * Passed to the `IntersectionObserver`. Read once, when the sentinel node\n * attaches — changing it afterwards does NOT rebuild the observer, so a\n * later `threshold`/`rootMargin` is silently ignored. Pass a stable, ideally\n * module-level, object.\n */\n options?: IntersectionObserverInit;\n}\n\n/**\n * Infinite-scroll trigger for the media library list.\n *\n * Returns a callback ref to place on a sentinel element rendered after the last\n * row. Three deliberate choices:\n *\n * 1. **Loading is driven only by fresh observer readings.** A fetch fires from\n * the `IntersectionObserver` callback — never from a state/prop change — so\n * it can't react to a stale `isIntersecting` from before newly loaded rows\n * rendered (which fetched an extra page on every trigger).\n *\n * 2. **Each settled page re-observes the sentinel.** An `IntersectionObserver`\n * only fires on a *transition*. When the sentinel stays on screen (a page\n * that doesn't fill the viewport — tall viewport, short page, grid fitting\n * the whole page) it would never fire again and loading would stall. So when\n * `isFetchingMore` settles back to false we re-observe, forcing a fresh\n * reading against the new layout: still on screen → load the next page;\n * viewport now filled → stop.\n *\n * 3. **A callback ref, not a stable object ref.** The sentinel only mounts once\n * the first page has rendered (the list shows a loader before that). A\n * callback ref (re)binds the observer exactly when the node mounts/unmounts,\n * so the observer can't miss a sentinel that appears after the hook first ran.\n */\nexport const useInfiniteScrollSentinel = ({\n hasNextPage,\n isFetchingMore,\n onLoadMore,\n options,\n}: UseInfiniteScrollSentinelArgs) => {\n const observerRef = useRef<IntersectionObserver | null>(null);\n const nodeRef = useRef<HTMLElement | null>(null);\n\n // Read through refs so the callback ref stays stable and the observer callback\n // always sees the latest values. `options` is read once at attach time (see\n // its JSDoc); `onLoadMore` need not be stable.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const onLoadMoreRef = useRef(onLoadMore);\n onLoadMoreRef.current = onLoadMore;\n const hasNextPageRef = useRef(hasNextPage);\n hasNextPageRef.current = hasNextPage;\n const isFetchingMoreRef = useRef(isFetchingMore);\n isFetchingMoreRef.current = isFetchingMore;\n\n const sentinelRef = useCallback((node: HTMLElement | null) => {\n observerRef.current?.disconnect();\n nodeRef.current = node;\n\n if (!node) {\n return;\n }\n\n const observer = new IntersectionObserver(([entry]) => {\n if (entry.isIntersecting && hasNextPageRef.current && !isFetchingMoreRef.current) {\n onLoadMoreRef.current();\n }\n }, optionsRef.current);\n\n observer.observe(node);\n observerRef.current = observer;\n }, []);\n\n useEffect(() => () => observerRef.current?.disconnect(), []);\n\n // Once a page settles, re-observe so a *fresh* reading against the new layout\n // decides whether to keep loading — rather than a stale `isIntersecting` from\n // before the rows rendered. Re-observing re-emits the current intersection\n // even without a transition, which is what keeps loading a viewport that a\n // single page didn't fill.\n useEffect(() => {\n if (isFetchingMore || !observerRef.current || !nodeRef.current) {\n return;\n }\n\n observerRef.current.unobserve(nodeRef.current);\n observerRef.current.observe(nodeRef.current);\n }, [isFetchingMore]);\n\n return sentinelRef;\n};\n"],"names":["useInfiniteScrollSentinel","hasNextPage","isFetchingMore","onLoadMore","options","observerRef","useRef","nodeRef","optionsRef","current","onLoadMoreRef","hasNextPageRef","isFetchingMoreRef","sentinelRef","useCallback","node","disconnect","observer","IntersectionObserver","entry","isIntersecting","observe","useEffect","unobserve"],"mappings":";;AAkBA;;;;;;;;;;;;;;;;;;;;;;;AAuBC,IACM,MAAMA,yBAAAA,GAA4B,CAAC,EACxCC,WAAW,EACXC,cAAc,EACdC,UAAU,EACVC,OAAO,EACuB,GAAA;AAC9B,IAAA,MAAMC,cAAcC,MAAAA,CAAoC,IAAA,CAAA;AACxD,IAAA,MAAMC,UAAUD,MAAAA,CAA2B,IAAA,CAAA;;;;AAK3C,IAAA,MAAME,aAAaF,MAAAA,CAAOF,OAAAA,CAAAA;AAC1BI,IAAAA,UAAAA,CAAWC,OAAO,GAAGL,OAAAA;AACrB,IAAA,MAAMM,gBAAgBJ,MAAAA,CAAOH,UAAAA,CAAAA;AAC7BO,IAAAA,aAAAA,CAAcD,OAAO,GAAGN,UAAAA;AACxB,IAAA,MAAMQ,iBAAiBL,MAAAA,CAAOL,WAAAA,CAAAA;AAC9BU,IAAAA,cAAAA,CAAeF,OAAO,GAAGR,WAAAA;AACzB,IAAA,MAAMW,oBAAoBN,MAAAA,CAAOJ,cAAAA,CAAAA;AACjCU,IAAAA,iBAAAA,CAAkBH,OAAO,GAAGP,cAAAA;IAE5B,MAAMW,WAAAA,GAAcC,YAAY,CAACC,IAAAA,GAAAA;AAC/BV,QAAAA,WAAAA,CAAYI,OAAO,EAAEO,UAAAA,EAAAA;AACrBT,QAAAA,OAAAA,CAAQE,OAAO,GAAGM,IAAAA;AAElB,QAAA,IAAI,CAACA,IAAAA,EAAM;AACT,YAAA;AACF,QAAA;AAEA,QAAA,MAAME,QAAAA,GAAW,IAAIC,oBAAAA,CAAqB,CAAC,CAACC,KAAAA,CAAM,GAAA;YAChD,IAAIA,KAAAA,CAAMC,cAAc,IAAIT,cAAAA,CAAeF,OAAO,IAAI,CAACG,iBAAAA,CAAkBH,OAAO,EAAE;AAChFC,gBAAAA,aAAAA,CAAcD,OAAO,EAAA;AACvB,YAAA;AACF,QAAA,CAAA,EAAGD,WAAWC,OAAO,CAAA;AAErBQ,QAAAA,QAAAA,CAASI,OAAO,CAACN,IAAAA,CAAAA;AACjBV,QAAAA,WAAAA,CAAYI,OAAO,GAAGQ,QAAAA;AACxB,IAAA,CAAA,EAAG,EAAE,CAAA;AAELK,IAAAA,SAAAA,CAAU,IAAM,IAAMjB,WAAAA,CAAYI,OAAO,EAAEO,cAAc,EAAE,CAAA;;;;;;IAO3DM,SAAAA,CAAU,IAAA;QACR,IAAIpB,cAAAA,IAAkB,CAACG,WAAAA,CAAYI,OAAO,IAAI,CAACF,OAAAA,CAAQE,OAAO,EAAE;AAC9D,YAAA;AACF,QAAA;AAEAJ,QAAAA,WAAAA,CAAYI,OAAO,CAACc,SAAS,CAAChB,QAAQE,OAAO,CAAA;AAC7CJ,QAAAA,WAAAA,CAAYI,OAAO,CAACY,OAAO,CAACd,QAAQE,OAAO,CAAA;IAC7C,CAAA,EAAG;AAACP,QAAAA;AAAe,KAAA,CAAA;IAEnB,OAAOW,WAAAA;AACT;;;;"}
|
|
@@ -382,10 +382,16 @@ const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
|
|
|
382
382
|
generateAiMetadata
|
|
383
383
|
});
|
|
384
384
|
},
|
|
385
|
+
// `Folder, LIST` refreshes the folder header count, which changes when
|
|
386
|
+
// files are added to it.
|
|
385
387
|
invalidatesTags: [
|
|
386
388
|
{
|
|
387
389
|
type: 'Asset',
|
|
388
390
|
id: 'LIST'
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
type: 'Folder',
|
|
394
|
+
id: 'LIST'
|
|
389
395
|
}
|
|
390
396
|
]
|
|
391
397
|
}),
|
|
@@ -417,10 +423,16 @@ const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
|
|
|
417
423
|
};
|
|
418
424
|
}
|
|
419
425
|
},
|
|
426
|
+
// `Folder, LIST` refreshes the folder header count, which changes when
|
|
427
|
+
// files are added to it.
|
|
420
428
|
invalidatesTags: [
|
|
421
429
|
{
|
|
422
430
|
type: 'Asset',
|
|
423
431
|
id: 'LIST'
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: 'Folder',
|
|
435
|
+
id: 'LIST'
|
|
424
436
|
}
|
|
425
437
|
]
|
|
426
438
|
}),
|
|
@@ -467,10 +479,16 @@ const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
|
|
|
467
479
|
generateAiMetadata: batch.generateAiMetadata
|
|
468
480
|
});
|
|
469
481
|
},
|
|
482
|
+
// `Folder, LIST` refreshes the folder header count, which changes when
|
|
483
|
+
// files are added to it.
|
|
470
484
|
invalidatesTags: [
|
|
471
485
|
{
|
|
472
486
|
type: 'Asset',
|
|
473
487
|
id: 'LIST'
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
type: 'Folder',
|
|
491
|
+
id: 'LIST'
|
|
474
492
|
}
|
|
475
493
|
]
|
|
476
494
|
}),
|
|
@@ -565,10 +583,16 @@ const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
|
|
|
565
583
|
};
|
|
566
584
|
}
|
|
567
585
|
},
|
|
586
|
+
// `Folder, LIST` refreshes the folder header count, which changes when
|
|
587
|
+
// files are added to it.
|
|
568
588
|
invalidatesTags: [
|
|
569
589
|
{
|
|
570
590
|
type: 'Asset',
|
|
571
591
|
id: 'LIST'
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
type: 'Folder',
|
|
595
|
+
id: 'LIST'
|
|
572
596
|
}
|
|
573
597
|
]
|
|
574
598
|
}),
|