@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.
Files changed (193) hide show
  1. package/dist/admin/future/App.js +3 -3
  2. package/dist/admin/future/App.js.map +1 -1
  3. package/dist/admin/future/App.mjs +4 -4
  4. package/dist/admin/future/App.mjs.map +1 -1
  5. package/dist/admin/future/components/BetaNotice.js +72 -0
  6. package/dist/admin/future/components/BetaNotice.js.map +1 -0
  7. package/dist/admin/future/components/BetaNotice.mjs +70 -0
  8. package/dist/admin/future/components/BetaNotice.mjs.map +1 -0
  9. package/dist/admin/future/components/Drawer.js +16 -3
  10. package/dist/admin/future/components/Drawer.js.map +1 -1
  11. package/dist/admin/future/components/Drawer.mjs +16 -3
  12. package/dist/admin/future/components/Drawer.mjs.map +1 -1
  13. package/dist/admin/future/hooks/useAIMetadataEnabled.js +39 -0
  14. package/dist/admin/future/hooks/useAIMetadataEnabled.js.map +1 -0
  15. package/dist/admin/future/hooks/useAIMetadataEnabled.mjs +37 -0
  16. package/dist/admin/future/hooks/useAIMetadataEnabled.mjs.map +1 -0
  17. package/dist/admin/future/pages/Assets/AssetsPage.js +297 -139
  18. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  19. package/dist/admin/future/pages/Assets/AssetsPage.mjs +301 -143
  20. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  21. package/dist/admin/future/pages/Assets/components/ActionsMenuContent.js +46 -0
  22. package/dist/admin/future/pages/Assets/components/ActionsMenuContent.js.map +1 -0
  23. package/dist/admin/future/pages/Assets/components/ActionsMenuContent.mjs +44 -0
  24. package/dist/admin/future/pages/Assets/components/ActionsMenuContent.mjs.map +1 -0
  25. package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js +325 -0
  26. package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js.map +1 -0
  27. package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs +323 -0
  28. package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs.map +1 -0
  29. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +35 -14
  30. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
  31. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +35 -14
  32. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
  33. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +54 -53
  34. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
  35. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +54 -54
  36. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  37. package/dist/admin/future/pages/Assets/components/AssetsGrid.js +89 -24
  38. package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
  39. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +91 -26
  40. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  41. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +15 -3
  42. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -1
  43. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +16 -4
  44. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -1
  45. package/dist/admin/future/pages/Assets/components/AssetsTable.js +168 -58
  46. package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
  47. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +171 -61
  48. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
  49. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +64 -132
  50. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
  51. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +64 -132
  52. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
  53. package/dist/admin/future/pages/Assets/components/BulkMoveDialog.js +123 -76
  54. package/dist/admin/future/pages/Assets/components/BulkMoveDialog.js.map +1 -1
  55. package/dist/admin/future/pages/Assets/components/BulkMoveDialog.mjs +126 -79
  56. package/dist/admin/future/pages/Assets/components/BulkMoveDialog.mjs.map +1 -1
  57. package/dist/admin/future/pages/Assets/components/BusyOverlay.js +36 -0
  58. package/dist/admin/future/pages/Assets/components/BusyOverlay.js.map +1 -0
  59. package/dist/admin/future/pages/Assets/components/BusyOverlay.mjs +34 -0
  60. package/dist/admin/future/pages/Assets/components/BusyOverlay.mjs.map +1 -0
  61. package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.js +131 -0
  62. package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.js.map +1 -0
  63. package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.mjs +129 -0
  64. package/dist/admin/future/pages/Assets/components/DeleteItemsDialog.mjs.map +1 -0
  65. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +27 -17
  66. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
  67. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +28 -18
  68. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
  69. package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.js +28 -31
  70. package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.js.map +1 -1
  71. package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.mjs +28 -31
  72. package/dist/admin/future/pages/Assets/components/Dnd/buildDragSet.mjs.map +1 -1
  73. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js +10 -15
  74. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js.map +1 -1
  75. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs +10 -15
  76. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs.map +1 -1
  77. package/dist/admin/future/pages/Assets/components/FilterBadges.js +13 -4
  78. package/dist/admin/future/pages/Assets/components/FilterBadges.js.map +1 -1
  79. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs +13 -4
  80. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs.map +1 -1
  81. package/dist/admin/future/pages/Assets/components/FilterMenu.js +241 -61
  82. package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -1
  83. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +244 -64
  84. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -1
  85. package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js +144 -0
  86. package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js.map +1 -0
  87. package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs +142 -0
  88. package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs.map +1 -0
  89. package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.js → FolderFormDialog.js} +50 -16
  90. package/dist/admin/future/pages/Assets/components/FolderFormDialog.js.map +1 -0
  91. package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.mjs → FolderFormDialog.mjs} +52 -18
  92. package/dist/admin/future/pages/Assets/components/FolderFormDialog.mjs.map +1 -0
  93. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +111 -191
  94. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
  95. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +114 -194
  96. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
  97. package/dist/admin/future/pages/Assets/hooks/useBusyAssets.js +47 -0
  98. package/dist/admin/future/pages/Assets/hooks/useBusyAssets.js.map +1 -0
  99. package/dist/admin/future/pages/Assets/hooks/useBusyAssets.mjs +44 -0
  100. package/dist/admin/future/pages/Assets/hooks/useBusyAssets.mjs.map +1 -0
  101. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +203 -62
  102. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
  103. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +204 -63
  104. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
  105. package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.js +75 -0
  106. package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.js.map +1 -0
  107. package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.mjs +73 -0
  108. package/dist/admin/future/pages/Assets/hooks/useInfiniteScrollSentinel.mjs.map +1 -0
  109. package/dist/admin/future/services/api.js +24 -0
  110. package/dist/admin/future/services/api.js.map +1 -1
  111. package/dist/admin/future/services/api.mjs +24 -0
  112. package/dist/admin/future/services/api.mjs.map +1 -1
  113. package/dist/admin/future/services/assets.js +11 -0
  114. package/dist/admin/future/services/assets.js.map +1 -1
  115. package/dist/admin/future/services/assets.mjs +11 -0
  116. package/dist/admin/future/services/assets.mjs.map +1 -1
  117. package/dist/admin/future/services/folders.js +37 -2
  118. package/dist/admin/future/services/folders.js.map +1 -1
  119. package/dist/admin/future/services/folders.mjs +37 -3
  120. package/dist/admin/future/services/folders.mjs.map +1 -1
  121. package/dist/admin/future/utils/buildDragSetFromSelection.js +7 -5
  122. package/dist/admin/future/utils/buildDragSetFromSelection.js.map +1 -1
  123. package/dist/admin/future/utils/buildDragSetFromSelection.mjs +7 -5
  124. package/dist/admin/future/utils/buildDragSetFromSelection.mjs.map +1 -1
  125. package/dist/admin/future/utils/formatMoveSuccessMessage.js +12 -1
  126. package/dist/admin/future/utils/formatMoveSuccessMessage.js.map +1 -1
  127. package/dist/admin/future/utils/formatMoveSuccessMessage.mjs +12 -1
  128. package/dist/admin/future/utils/formatMoveSuccessMessage.mjs.map +1 -1
  129. package/dist/admin/future/utils/getRelationId.js +24 -0
  130. package/dist/admin/future/utils/getRelationId.js.map +1 -0
  131. package/dist/admin/future/utils/getRelationId.mjs +22 -0
  132. package/dist/admin/future/utils/getRelationId.mjs.map +1 -0
  133. package/dist/admin/future/utils/isEventFromWithin.js +14 -0
  134. package/dist/admin/future/utils/isEventFromWithin.js.map +1 -0
  135. package/dist/admin/future/utils/isEventFromWithin.mjs +12 -0
  136. package/dist/admin/future/utils/isEventFromWithin.mjs.map +1 -0
  137. package/dist/admin/future/utils/itemLocations.js +40 -0
  138. package/dist/admin/future/utils/itemLocations.js.map +1 -0
  139. package/dist/admin/future/utils/itemLocations.mjs +36 -0
  140. package/dist/admin/future/utils/itemLocations.mjs.map +1 -0
  141. package/dist/admin/future/utils/itemSource.js +36 -0
  142. package/dist/admin/future/utils/itemSource.js.map +1 -0
  143. package/dist/admin/future/utils/itemSource.mjs +32 -0
  144. package/dist/admin/future/utils/itemSource.mjs.map +1 -0
  145. package/dist/admin/index.js +11 -17
  146. package/dist/admin/index.js.map +1 -1
  147. package/dist/admin/index.mjs +12 -18
  148. package/dist/admin/index.mjs.map +1 -1
  149. package/dist/admin/src/future/App.d.ts +1 -1
  150. package/dist/admin/src/future/components/BetaNotice.d.ts +11 -0
  151. package/dist/admin/src/future/components/Drawer.d.ts +5 -1
  152. package/dist/admin/src/future/hooks/useAIMetadataEnabled.d.ts +26 -0
  153. package/dist/admin/src/future/pages/Assets/components/ActionsMenuContent.d.ts +33 -0
  154. package/dist/admin/src/future/pages/Assets/components/AssetActionsMenu.d.ts +24 -0
  155. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.d.ts +16 -0
  156. package/dist/admin/src/future/pages/Assets/components/BulkActionsBar.d.ts +8 -1
  157. package/dist/admin/src/future/pages/Assets/components/BulkMoveDialog.d.ts +17 -6
  158. package/dist/admin/src/future/pages/Assets/components/BusyOverlay.d.ts +9 -0
  159. package/dist/admin/src/future/pages/Assets/components/DeleteItemsDialog.d.ts +27 -0
  160. package/dist/admin/src/future/pages/Assets/components/Dnd/AssetsDndProvider.d.ts +8 -1
  161. package/dist/admin/src/future/pages/Assets/components/Dnd/buildDragSet.d.ts +18 -3
  162. package/dist/admin/src/future/pages/Assets/components/Dnd/useAssetDnd.d.ts +2 -1
  163. package/dist/admin/src/future/pages/Assets/components/FilterBadges.d.ts +3 -1
  164. package/dist/admin/src/future/pages/Assets/components/FilterMenu.d.ts +4 -0
  165. package/dist/admin/src/future/pages/Assets/components/FolderActionsMenu.d.ts +20 -0
  166. package/dist/admin/src/future/pages/Assets/components/FolderFormDialog.d.ts +16 -0
  167. package/dist/admin/src/future/pages/Assets/hooks/useBusyAssets.d.ts +38 -0
  168. package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +20 -6
  169. package/dist/admin/src/future/pages/Assets/hooks/useInfiniteScrollSentinel.d.ts +41 -0
  170. package/dist/admin/src/future/services/folders.d.ts +6 -1
  171. package/dist/admin/src/future/utils/buildDragSetFromSelection.d.ts +4 -3
  172. package/dist/admin/src/future/utils/formatMoveSuccessMessage.d.ts +7 -1
  173. package/dist/admin/src/future/utils/getRelationId.d.ts +11 -0
  174. package/dist/admin/src/future/utils/isEventFromWithin.d.ts +11 -0
  175. package/dist/admin/src/future/utils/itemLocations.d.ts +31 -0
  176. package/dist/admin/src/future/utils/itemSource.d.ts +22 -0
  177. package/dist/admin/translations/en.json.js +25 -4
  178. package/dist/admin/translations/en.json.js.map +1 -1
  179. package/dist/admin/translations/en.json.mjs +25 -4
  180. package/dist/admin/translations/en.json.mjs.map +1 -1
  181. package/dist/server/services/image-manipulation.js.map +1 -1
  182. package/dist/server/services/image-manipulation.mjs.map +1 -1
  183. package/dist/server/src/services/image-manipulation.d.ts +0 -5
  184. package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
  185. package/package.json +9 -9
  186. package/dist/admin/future/pages/Assets/components/CreateFolderDialog.js.map +0 -1
  187. package/dist/admin/future/pages/Assets/components/CreateFolderDialog.mjs.map +0 -1
  188. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js +0 -33
  189. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js.map +0 -1
  190. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs +0 -31
  191. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs.map +0 -1
  192. package/dist/admin/src/future/pages/Assets/components/CreateFolderDialog.d.ts +0 -8
  193. package/dist/admin/src/future/pages/Assets/components/FolderTree/filterFolderTree.d.ts +0 -16
@@ -1,90 +1,230 @@
1
- import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
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
- const useInfiniteAssets = ({ folder = null, sort, search, filters, filtersKey = '', enabled = true } = {})=>{
6
- const [page, setPage] = useState(1);
7
- const lastResultsRef = useRef([]);
8
- const lastPaginationRef = useRef(undefined);
9
- const isMountRef = useRef(true);
10
- const isSearchMountRef = useRef(true);
11
- const { currentData: data, isLoading, isFetching, error } = useGetAssetsQuery({
12
- folder,
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
- pageSize: PAGE_SIZE,
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 pagination = data?.pagination;
22
- if (pagination) {
23
- lastPaginationRef.current = pagination;
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
- // Accumulate pages. When cache is invalidated the current page is refetched
26
- // detect this and reset to avoid a gap in the results.
27
- const assets = useMemo(()=>{
28
- if (!data) {
29
- return lastResultsRef.current;
30
- }
31
- const currentPageResults = data.results;
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
- // Only append if these aren't already accumulated
41
- if (lastResultsRef.current.length < page * PAGE_SIZE) {
42
- lastResultsRef.current = [
43
- ...lastResultsRef.current,
44
- ...currentPageResults
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
- data,
51
- page
146
+ queryKey
52
147
  ]);
53
- // Reset on filter/sort change skip the initial mount since the memo
54
- // already handles page 1 correctly
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
- if (isMountRef.current) {
57
- isMountRef.current = false;
175
+ const prevFolder = prevFolderRef.current;
176
+ prevFolderRef.current = folder;
177
+ if (prevFolder === folder) {
58
178
  return;
59
179
  }
60
- setPage(1);
61
- lastResultsRef.current = [];
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
- sort,
65
- filtersKey
200
+ dispatch,
201
+ store
66
202
  ]);
67
- // A search change resets pagination but keeps the previous results rendered
68
- // until the new page 1 lands, which replaces them.
69
- useEffect(()=>{
70
- if (isSearchMountRef.current) {
71
- isSearchMountRef.current = false;
72
- return;
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 = pagination ? page < pagination.pageCount : false;
213
+ const hasNextPage = currentData ? page < currentData.pagination.pageCount : false;
81
214
  const isFetchingMore = isFetching && page > 1;
82
215
  const fetchNextPage = useCallback(()=>{
83
- setPage((prev)=>prev + 1);
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
- // Reported alongside the stale `assets` above so a consumer showing the total
99
- // doesn't flash zero mid-transition.
100
- pagination: pagination ?? lastPaginationRef.current,
101
- isLoading,
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":"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"}
@@ -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
  }),