@strapi/upload 5.51.2 → 5.52.1
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/components/TruncatedText.js +76 -0
- package/dist/admin/future/components/TruncatedText.js.map +1 -0
- package/dist/admin/future/components/TruncatedText.mjs +74 -0
- package/dist/admin/future/components/TruncatedText.mjs.map +1 -0
- 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/hooks/useTracking.js +57 -0
- package/dist/admin/future/hooks/useTracking.js.map +1 -0
- package/dist/admin/future/hooks/useTracking.mjs +53 -0
- package/dist/admin/future/hooks/useTracking.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/AssetsPage.js +328 -139
- package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.mjs +332 -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 +328 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs +326 -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 +74 -53
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +74 -54
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js +92 -28
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +94 -30
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +26 -4
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +27 -5
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.js +172 -60
- package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +175 -63
- 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 +259 -63
- package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +262 -66
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js +146 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs +144 -0
- package/dist/admin/future/pages/Assets/components/FolderActionsMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.js → FolderFormDialog.js} +62 -16
- package/dist/admin/future/pages/Assets/components/FolderFormDialog.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/{CreateFolderDialog.mjs → FolderFormDialog.mjs} +64 -18
- package/dist/admin/future/pages/Assets/components/FolderFormDialog.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +132 -229
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +135 -232
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/SortMenu.js +14 -0
- package/dist/admin/future/pages/Assets/components/SortMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/SortMenu.mjs +14 -0
- package/dist/admin/future/pages/Assets/components/SortMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js +5 -0
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs +6 -1
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.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/pages/Assets/utils/selection.js +17 -0
- package/dist/admin/future/pages/Assets/utils/selection.js.map +1 -1
- package/dist/admin/future/pages/Assets/utils/selection.mjs +17 -1
- package/dist/admin/future/pages/Assets/utils/selection.mjs.map +1 -1
- package/dist/admin/future/services/api.js +31 -8
- package/dist/admin/future/services/api.js.map +1 -1
- package/dist/admin/future/services/api.mjs +31 -8
- package/dist/admin/future/services/api.mjs.map +1 -1
- package/dist/admin/future/services/assets.js +22 -23
- package/dist/admin/future/services/assets.js.map +1 -1
- package/dist/admin/future/services/assets.mjs +22 -23
- 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/hooks/useAIMetadataJob.js +1 -1
- package/dist/admin/hooks/useAIMetadataJob.js.map +1 -1
- package/dist/admin/hooks/useAIMetadataJob.mjs +1 -1
- package/dist/admin/hooks/useAIMetadataJob.mjs.map +1 -1
- 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/pages/SettingsPage/SettingsPage.js +2 -2
- package/dist/admin/pages/SettingsPage/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage/SettingsPage.mjs +2 -2
- package/dist/admin/pages/SettingsPage/SettingsPage.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/components/TruncatedText.d.ts +32 -0
- package/dist/admin/src/future/hooks/useAIMetadataEnabled.d.ts +26 -0
- package/dist/admin/src/future/hooks/useTracking.d.ts +29 -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 +26 -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 +21 -0
- package/dist/admin/src/future/pages/Assets/components/FolderFormDialog.d.ts +16 -0
- package/dist/admin/src/future/pages/Assets/hooks/useAssetSelection.d.ts +2 -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/pages/Assets/utils/selection.d.ts +10 -0
- package/dist/admin/src/future/services/api.d.ts +6 -7
- 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/controllers/admin-file.js +27 -9
- package/dist/server/controllers/admin-file.js.map +1 -1
- package/dist/server/controllers/admin-file.mjs +27 -9
- package/dist/server/controllers/admin-file.mjs.map +1 -1
- package/dist/server/controllers/admin-upload.js +24 -8
- package/dist/server/controllers/admin-upload.js.map +1 -1
- package/dist/server/controllers/admin-upload.mjs +24 -8
- package/dist/server/controllers/admin-upload.mjs.map +1 -1
- package/dist/server/routes/admin.js +41 -21
- package/dist/server/routes/admin.js.map +1 -1
- package/dist/server/routes/admin.mjs +41 -21
- package/dist/server/routes/admin.mjs.map +1 -1
- package/dist/server/services/ai-metadata.js.map +1 -1
- package/dist/server/services/ai-metadata.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/controllers/admin-file.d.ts +29 -8
- package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-upload.d.ts +20 -4
- package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
- package/dist/server/src/controllers/index.d.ts +4 -4
- package/dist/server/src/index.d.ts +4 -4
- package/dist/server/src/routes/admin.d.ts.map +1 -1
- package/dist/server/src/services/ai-metadata.d.ts +2 -2
- package/dist/server/src/services/ai-metadata.d.ts.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/dist/shared/contracts/ai-metadata-jobs.d.ts +10 -32
- package/dist/shared/contracts/ai-metadata-jobs.d.ts.map +1 -1
- package/dist/shared/contracts/files.d.ts +10 -10
- package/dist/shared/contracts/files.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":"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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;;;;"}
|
|
@@ -39,6 +39,22 @@ const createEmptySelection = ()=>({
|
|
|
39
39
|
anchorKey: key
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Subtractive counterpart to {@link toggleSelection} — removes a key without the
|
|
44
|
+
* add-if-absent ambiguity, leaving the rest of the selection alone. Used when a
|
|
45
|
+
* single-item affordance (a row's "..." menu) makes exactly one key stale.
|
|
46
|
+
*
|
|
47
|
+
* Idempotent: removing a key that was never selected is a no-op. The anchor is
|
|
48
|
+
* dropped only when it is the removed key, so a Shift+click range built around
|
|
49
|
+
* other rows survives.
|
|
50
|
+
*/ const deselect = (state, key)=>{
|
|
51
|
+
const selectedKeys = new Set(state.selectedKeys);
|
|
52
|
+
selectedKeys.delete(key);
|
|
53
|
+
return {
|
|
54
|
+
selectedKeys,
|
|
55
|
+
anchorKey: state.anchorKey === key ? null : state.anchorKey
|
|
56
|
+
};
|
|
57
|
+
};
|
|
42
58
|
/**
|
|
43
59
|
* Shift+click — selects the contiguous range between the current anchor and the
|
|
44
60
|
* target in render order, replacing the current selection. The anchor stays put.
|
|
@@ -95,6 +111,7 @@ const clearSelection = ()=>createEmptySelection();
|
|
|
95
111
|
exports.assetKey = assetKey;
|
|
96
112
|
exports.clearSelection = clearSelection;
|
|
97
113
|
exports.createEmptySelection = createEmptySelection;
|
|
114
|
+
exports.deselect = deselect;
|
|
98
115
|
exports.folderKey = folderKey;
|
|
99
116
|
exports.getIdsOfKind = getIdsOfKind;
|
|
100
117
|
exports.getSelectAllState = getSelectAllState;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection.js","sources":["../../../../../../admin/src/future/pages/Assets/utils/selection.ts"],"sourcesContent":["/**\n * Pure, view-agnostic selection logic for the future Media Library multi-select.\n *\n * Selection covers both assets and folders. Since their numeric ids live in\n * different tables (and can collide), items are tracked as namespaced keys\n * (`asset:1` / `folder:1`). Views build their rendered order as a key array so\n * range / select-all work across the folder and asset sections seamlessly.\n */\n\nexport type SelectableKind = 'asset' | 'folder';\n\nexport type ItemKey = `${SelectableKind}:${number}`;\n\nexport const assetKey = (id: number): ItemKey => `asset:${id}`;\n\nexport const folderKey = (id: number): ItemKey => `folder:${id}`;\n\n/** Extracts the numeric ids of one kind from a set of item keys. */\nexport const getIdsOfKind = (keys: Set<ItemKey>, kind: SelectableKind): Set<number> => {\n const ids = new Set<number>();\n\n keys.forEach((key) => {\n const [keyKind, id] = key.split(':');\n if (keyKind === kind) {\n ids.add(Number(id));\n }\n });\n\n return ids;\n};\n\nexport interface SelectionState {\n /** Currently selected item keys (assets and folders). */\n selectedKeys: Set<ItemKey>;\n /** Anchor used as the start of a Shift+click range. */\n anchorKey: ItemKey | null;\n}\n\nexport const createEmptySelection = (): SelectionState => ({\n selectedKeys: new Set<ItemKey>(),\n anchorKey: null,\n});\n\n/**\n * Additive toggle — adds the key if absent, removes it otherwise, without\n * touching the rest of the selection. Used by the item checkbox and Cmd/Ctrl+click.\n * The toggled key becomes the new anchor.\n */\nexport const toggleSelection = (state: SelectionState, key: ItemKey): SelectionState => {\n const selectedKeys = new Set(state.selectedKeys);\n\n if (selectedKeys.has(key)) {\n selectedKeys.delete(key);\n } else {\n selectedKeys.add(key);\n }\n\n return { selectedKeys, anchorKey: key };\n};\n\n/**\n * Shift+click — selects the contiguous range between the current anchor and the\n * target in render order, replacing the current selection. The anchor stays put.\n *\n * Falls back to a single selection when there is no usable anchor (e.g. first\n * click was a Shift+click, or the anchor is no longer in the list).\n */\nexport const selectRange = (\n state: SelectionState,\n orderedKeys: ItemKey[],\n targetKey: ItemKey\n): SelectionState => {\n const targetIndex = orderedKeys.indexOf(targetKey);\n\n if (targetIndex === -1) {\n return state;\n }\n\n const anchorIndex = state.anchorKey === null ? -1 : orderedKeys.indexOf(state.anchorKey);\n\n if (anchorIndex === -1) {\n return { selectedKeys: new Set<ItemKey>([targetKey]), anchorKey: targetKey };\n }\n\n const start = Math.min(anchorIndex, targetIndex);\n const end = Math.max(anchorIndex, targetIndex);\n\n return {\n selectedKeys: new Set(orderedKeys.slice(start, end + 1)),\n anchorKey: state.anchorKey,\n };\n};\n\n/**\n * Select-all — selects every key in the provided ordered array. Used by the header\n * checkbox when not everything is already selected.\n */\nexport const selectAll = (orderedKeys: ItemKey[]): SelectionState => ({\n selectedKeys: new Set(orderedKeys),\n anchorKey: orderedKeys.length > 0 ? orderedKeys[orderedKeys.length - 1] : null,\n});\n\nexport const clearSelection = (): SelectionState => createEmptySelection();\n\nexport interface SelectAllState {\n allSelected: boolean;\n isIndeterminate: boolean;\n}\n\n/**\n * Derives the header checkbox state: everything selected, some (indeterminate),\n * or none. Only considers the currently-rendered items (orderedKeys).\n */\nexport const getSelectAllState = (\n selectedKeys: Set<ItemKey>,\n orderedKeys: ItemKey[]\n): SelectAllState => {\n if (orderedKeys.length === 0) {\n return { allSelected: false, isIndeterminate: false };\n }\n\n const selectedCount = orderedKeys.reduce(\n (count, key) => (selectedKeys.has(key) ? count + 1 : count),\n 0\n );\n const allSelected = selectedCount === orderedKeys.length;\n\n return {\n allSelected,\n isIndeterminate: selectedCount > 0 && !allSelected,\n };\n};\n"],"names":["assetKey","id","folderKey","getIdsOfKind","keys","kind","ids","Set","forEach","key","keyKind","split","add","Number","createEmptySelection","selectedKeys","anchorKey","toggleSelection","state","has","delete","selectRange","orderedKeys","targetKey","targetIndex","indexOf","anchorIndex","start","Math","min","end","max","slice","selectAll","length","clearSelection","getSelectAllState","allSelected","isIndeterminate","selectedCount","reduce","count"],"mappings":";;AAAA;;;;;;;UAaaA,QAAAA,GAAW,CAACC,KAAwB,CAAC,MAAM,EAAEA,EAAAA,CAAAA;AAEnD,MAAMC,YAAY,CAACD,EAAAA,GAAwB,CAAC,OAAO,EAAEA;AAE5D,qEACO,MAAME,YAAAA,GAAe,CAACC,IAAAA,EAAoBC,IAAAA,GAAAA;AAC/C,IAAA,MAAMC,MAAM,IAAIC,GAAAA,EAAAA;IAEhBH,IAAAA,CAAKI,OAAO,CAAC,CAACC,GAAAA,GAAAA;AACZ,QAAA,MAAM,CAACC,OAAAA,EAAST,EAAAA,CAAG,GAAGQ,GAAAA,CAAIE,KAAK,CAAC,GAAA,CAAA;AAChC,QAAA,IAAID,YAAYL,IAAAA,EAAM;YACpBC,GAAAA,CAAIM,GAAG,CAACC,MAAAA,CAAOZ,EAAAA,CAAAA,CAAAA;AACjB,QAAA;AACF,IAAA,CAAA,CAAA;IAEA,OAAOK,GAAAA;AACT;AASO,MAAMQ,oBAAAA,GAAuB,KAAuB;AACzDC,QAAAA,YAAAA,EAAc,IAAIR,GAAAA,EAAAA;QAClBS,SAAAA,EAAW;AACb,KAAA;AAEA;;;;AAIC,IACM,MAAMC,eAAAA,GAAkB,CAACC,KAAAA,EAAuBT,GAAAA,GAAAA;AACrD,IAAA,MAAMM,YAAAA,GAAe,IAAIR,GAAAA,CAAIW,KAAAA,CAAMH,YAAY,CAAA;IAE/C,IAAIA,YAAAA,CAAaI,GAAG,CAACV,GAAAA,CAAAA,EAAM;AACzBM,QAAAA,YAAAA,CAAaK,MAAM,CAACX,GAAAA,CAAAA;IACtB,CAAA,MAAO;AACLM,QAAAA,YAAAA,CAAaH,GAAG,CAACH,GAAAA,CAAAA;AACnB,IAAA;IAEA,OAAO;AAAEM,QAAAA,YAAAA;QAAcC,SAAAA,EAAWP;AAAI,KAAA;AACxC;AAEA;;;;;;AAMC,IACM,
|
|
1
|
+
{"version":3,"file":"selection.js","sources":["../../../../../../admin/src/future/pages/Assets/utils/selection.ts"],"sourcesContent":["/**\n * Pure, view-agnostic selection logic for the future Media Library multi-select.\n *\n * Selection covers both assets and folders. Since their numeric ids live in\n * different tables (and can collide), items are tracked as namespaced keys\n * (`asset:1` / `folder:1`). Views build their rendered order as a key array so\n * range / select-all work across the folder and asset sections seamlessly.\n */\n\nexport type SelectableKind = 'asset' | 'folder';\n\nexport type ItemKey = `${SelectableKind}:${number}`;\n\nexport const assetKey = (id: number): ItemKey => `asset:${id}`;\n\nexport const folderKey = (id: number): ItemKey => `folder:${id}`;\n\n/** Extracts the numeric ids of one kind from a set of item keys. */\nexport const getIdsOfKind = (keys: Set<ItemKey>, kind: SelectableKind): Set<number> => {\n const ids = new Set<number>();\n\n keys.forEach((key) => {\n const [keyKind, id] = key.split(':');\n if (keyKind === kind) {\n ids.add(Number(id));\n }\n });\n\n return ids;\n};\n\nexport interface SelectionState {\n /** Currently selected item keys (assets and folders). */\n selectedKeys: Set<ItemKey>;\n /** Anchor used as the start of a Shift+click range. */\n anchorKey: ItemKey | null;\n}\n\nexport const createEmptySelection = (): SelectionState => ({\n selectedKeys: new Set<ItemKey>(),\n anchorKey: null,\n});\n\n/**\n * Additive toggle — adds the key if absent, removes it otherwise, without\n * touching the rest of the selection. Used by the item checkbox and Cmd/Ctrl+click.\n * The toggled key becomes the new anchor.\n */\nexport const toggleSelection = (state: SelectionState, key: ItemKey): SelectionState => {\n const selectedKeys = new Set(state.selectedKeys);\n\n if (selectedKeys.has(key)) {\n selectedKeys.delete(key);\n } else {\n selectedKeys.add(key);\n }\n\n return { selectedKeys, anchorKey: key };\n};\n\n/**\n * Subtractive counterpart to {@link toggleSelection} — removes a key without the\n * add-if-absent ambiguity, leaving the rest of the selection alone. Used when a\n * single-item affordance (a row's \"...\" menu) makes exactly one key stale.\n *\n * Idempotent: removing a key that was never selected is a no-op. The anchor is\n * dropped only when it is the removed key, so a Shift+click range built around\n * other rows survives.\n */\nexport const deselect = (state: SelectionState, key: ItemKey): SelectionState => {\n const selectedKeys = new Set(state.selectedKeys);\n selectedKeys.delete(key);\n\n return {\n selectedKeys,\n anchorKey: state.anchorKey === key ? null : state.anchorKey,\n };\n};\n\n/**\n * Shift+click — selects the contiguous range between the current anchor and the\n * target in render order, replacing the current selection. The anchor stays put.\n *\n * Falls back to a single selection when there is no usable anchor (e.g. first\n * click was a Shift+click, or the anchor is no longer in the list).\n */\nexport const selectRange = (\n state: SelectionState,\n orderedKeys: ItemKey[],\n targetKey: ItemKey\n): SelectionState => {\n const targetIndex = orderedKeys.indexOf(targetKey);\n\n if (targetIndex === -1) {\n return state;\n }\n\n const anchorIndex = state.anchorKey === null ? -1 : orderedKeys.indexOf(state.anchorKey);\n\n if (anchorIndex === -1) {\n return { selectedKeys: new Set<ItemKey>([targetKey]), anchorKey: targetKey };\n }\n\n const start = Math.min(anchorIndex, targetIndex);\n const end = Math.max(anchorIndex, targetIndex);\n\n return {\n selectedKeys: new Set(orderedKeys.slice(start, end + 1)),\n anchorKey: state.anchorKey,\n };\n};\n\n/**\n * Select-all — selects every key in the provided ordered array. Used by the header\n * checkbox when not everything is already selected.\n */\nexport const selectAll = (orderedKeys: ItemKey[]): SelectionState => ({\n selectedKeys: new Set(orderedKeys),\n anchorKey: orderedKeys.length > 0 ? orderedKeys[orderedKeys.length - 1] : null,\n});\n\nexport const clearSelection = (): SelectionState => createEmptySelection();\n\nexport interface SelectAllState {\n allSelected: boolean;\n isIndeterminate: boolean;\n}\n\n/**\n * Derives the header checkbox state: everything selected, some (indeterminate),\n * or none. Only considers the currently-rendered items (orderedKeys).\n */\nexport const getSelectAllState = (\n selectedKeys: Set<ItemKey>,\n orderedKeys: ItemKey[]\n): SelectAllState => {\n if (orderedKeys.length === 0) {\n return { allSelected: false, isIndeterminate: false };\n }\n\n const selectedCount = orderedKeys.reduce(\n (count, key) => (selectedKeys.has(key) ? count + 1 : count),\n 0\n );\n const allSelected = selectedCount === orderedKeys.length;\n\n return {\n allSelected,\n isIndeterminate: selectedCount > 0 && !allSelected,\n };\n};\n"],"names":["assetKey","id","folderKey","getIdsOfKind","keys","kind","ids","Set","forEach","key","keyKind","split","add","Number","createEmptySelection","selectedKeys","anchorKey","toggleSelection","state","has","delete","deselect","selectRange","orderedKeys","targetKey","targetIndex","indexOf","anchorIndex","start","Math","min","end","max","slice","selectAll","length","clearSelection","getSelectAllState","allSelected","isIndeterminate","selectedCount","reduce","count"],"mappings":"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"}
|