@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
@@ -0,0 +1,323 @@
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
+ import { useRef, useState, useMemo } from 'react';
3
+ import { useClipboard, useNotification } from '@strapi/admin/strapi-admin';
4
+ import { VisuallyHidden, Menu, IconButton, Dialog, Flex, Typography, Button } from '@strapi/design-system';
5
+ import { More, ArrowsCounterClockwise, Link, Download, ArrowRight, Trash } from '@strapi/icons';
6
+ import { useIntl } from 'react-intl';
7
+ import { useAIMetadataEnabled } from '../../../hooks/useAIMetadataEnabled.mjs';
8
+ import { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions.mjs';
9
+ import { useReplaceAssetMutation } from '../../../services/assets.mjs';
10
+ import { downloadFile } from '../../../utils/downloadFile.mjs';
11
+ import { prefixFileUrlWithBackendUrl } from '../../../utils/files.mjs';
12
+ import { getTranslationKey } from '../../../utils/translations.mjs';
13
+ import { useAssetSelection } from '../hooks/useAssetSelection.mjs';
14
+ import { useBusyAssetsOptional } from '../hooks/useBusyAssets.mjs';
15
+ import { ActionsMenuContent } from './ActionsMenuContent.mjs';
16
+ import { BulkMoveDialog } from './BulkMoveDialog.mjs';
17
+ import { DeleteItemsDialog } from './DeleteItemsDialog.mjs';
18
+
19
+ /**
20
+ * The "..." menu on an asset row/card. Always acts on that one asset, whatever
21
+ * the current multi-selection is (the bulk actions bar is the selection-scoped
22
+ * affordance) — the file mirror of `FolderActionsMenu`.
23
+ *
24
+ * Replace / copy link / download duplicate what the details drawer offers as
25
+ * icon buttons, so the same action is one click away without opening the asset
26
+ * first. They are re-implemented rather than lifted out of the drawer: the
27
+ * drawer versions render as `IconButton`s and report through its in-drawer toast
28
+ * slot, neither of which fits a menu item on a row.
29
+ *
30
+ * Any successful move or delete clears the whole selection: a delete only
31
+ * invalidates RTK tags, so without it an `asset:<id>` key for a file that no
32
+ * longer exists would linger in the selection.
33
+ */ const AssetActionsMenu = ({ asset, dragData })=>{
34
+ const { formatMessage } = useIntl();
35
+ const { copy } = useClipboard();
36
+ const { toggleNotification } = useNotification();
37
+ const { clear } = useAssetSelection();
38
+ // Absent in the asset picker and in unit tests: the replace still runs, it
39
+ // just renders no row-level overlay.
40
+ const markBusy = useBusyAssetsOptional()?.markBusy ?? (()=>()=>{});
41
+ const { canUpdate, canDownload, canCopyLink, isLoading: isLoadingPermissions } = useMediaLibraryPermissions();
42
+ const [replaceAsset, { isLoading: isReplacing }] = useReplaceAssetMutation();
43
+ const aiEnabled = useAIMetadataEnabled({
44
+ mime: asset.mime
45
+ });
46
+ const fileInputRef = useRef(null);
47
+ const [isReplaceOpen, setIsReplaceOpen] = useState(false);
48
+ const [isMoveOpen, setIsMoveOpen] = useState(false);
49
+ const [isDeleteOpen, setIsDeleteOpen] = useState(false);
50
+ const [isDownloading, setIsDownloading] = useState(false);
51
+ // Stable identity: the move dialog memoizes its destination walk on it.
52
+ const moveItems = useMemo(()=>[
53
+ dragData
54
+ ], [
55
+ dragData
56
+ ]);
57
+ // Confirm first, then open the native picker, so the user only commits to
58
+ // replacing after acknowledging the warning (same order as the drawer).
59
+ const handleReplaceContinue = ()=>{
60
+ setIsReplaceOpen(false);
61
+ fileInputRef.current?.click();
62
+ };
63
+ const handleFileChange = async (event)=>{
64
+ const file = event.target.files?.[0];
65
+ // Reset the native input so the same file can be picked again later.
66
+ event.target.value = '';
67
+ if (!file) {
68
+ return;
69
+ }
70
+ // The menu has closed by the time the picker returns, so `isReplacing` has
71
+ // nothing left to disable and the row would otherwise sit inert for the
72
+ // whole upload. Marking the asset busy puts the same overlay the drawer
73
+ // uses on the row/card, cleared when the mutation settles either way.
74
+ const releaseBusy = markBusy(asset.id, formatMessage({
75
+ id: getTranslationKey('asset-details.replace.loading'),
76
+ defaultMessage: 'Replacing the file…'
77
+ }));
78
+ let res;
79
+ try {
80
+ res = await replaceAsset({
81
+ id: asset.id,
82
+ file
83
+ });
84
+ } finally{
85
+ releaseBusy();
86
+ }
87
+ if ('error' in res) {
88
+ // `fetchBaseQuery` already unwraps the API envelope, so a server-sent
89
+ // reason (file too large, unsupported type) lands directly on `message`.
90
+ const { message } = res.error;
91
+ toggleNotification({
92
+ type: 'danger',
93
+ message: message ?? formatMessage({
94
+ id: getTranslationKey('asset-details.replace.error'),
95
+ defaultMessage: 'Failed to replace the file.'
96
+ })
97
+ });
98
+ return;
99
+ }
100
+ toggleNotification({
101
+ type: 'success',
102
+ message: formatMessage({
103
+ id: getTranslationKey('asset-details.replace.success'),
104
+ defaultMessage: 'File replaced.'
105
+ })
106
+ });
107
+ };
108
+ const handleCopyLink = async ()=>{
109
+ const url = prefixFileUrlWithBackendUrl(asset.url);
110
+ if (!url) {
111
+ return;
112
+ }
113
+ const didCopy = await copy(url);
114
+ toggleNotification({
115
+ type: didCopy ? 'success' : 'danger',
116
+ message: formatMessage(didCopy ? {
117
+ id: getTranslationKey('asset-details.copy-link.success'),
118
+ defaultMessage: 'Link copied.'
119
+ } : {
120
+ id: getTranslationKey('asset-details.copy-link.error'),
121
+ defaultMessage: 'Failed to copy the link.'
122
+ })
123
+ });
124
+ };
125
+ const handleDownload = async ()=>{
126
+ const url = prefixFileUrlWithBackendUrl(asset.url);
127
+ if (!url) {
128
+ return;
129
+ }
130
+ setIsDownloading(true);
131
+ try {
132
+ await downloadFile(url, asset.name);
133
+ } catch {
134
+ toggleNotification({
135
+ type: 'danger',
136
+ message: formatMessage({
137
+ id: getTranslationKey('asset-details.download.error'),
138
+ defaultMessage: 'Failed to download the file.'
139
+ })
140
+ });
141
+ } finally{
142
+ setIsDownloading(false);
143
+ }
144
+ };
145
+ // Two permission-gated groups: replace/copy-link/download above, move/delete
146
+ // below. The separator earns its place only when both survive the gate —
147
+ // `canUpdate` alone puts Replace in the top group, which is not enough.
148
+ const hasTopGroup = canUpdate || canCopyLink || canDownload;
149
+ const hasBottomGroup = canUpdate;
150
+ const showSeparator = (canCopyLink || canDownload) && hasBottomGroup;
151
+ // A role can read the library while holding none of these, and an empty
152
+ // popup is worse than no trigger at all. Every flag is `false` until the
153
+ // RBAC check settles, so wait for it — otherwise the trigger unmounts and
154
+ // remounts on first paint for everyone.
155
+ if (!isLoadingPermissions && !hasTopGroup && !hasBottomGroup) {
156
+ return null;
157
+ }
158
+ return /*#__PURE__*/ jsxs(Fragment, {
159
+ children: [
160
+ /*#__PURE__*/ jsx(VisuallyHidden, {
161
+ children: /*#__PURE__*/ jsx("input", {
162
+ ref: fileInputRef,
163
+ type: "file",
164
+ multiple: false,
165
+ onChange: handleFileChange,
166
+ "aria-hidden": true,
167
+ tabIndex: -1
168
+ })
169
+ }),
170
+ /*#__PURE__*/ jsxs(Menu.Root, {
171
+ modal: false,
172
+ children: [
173
+ /*#__PURE__*/ jsx(Menu.Trigger, {
174
+ tag: IconButton,
175
+ icon: /*#__PURE__*/ jsx(More, {}),
176
+ variant: "ghost",
177
+ label: formatMessage({
178
+ id: getTranslationKey('control-card.more-actions'),
179
+ defaultMessage: 'More actions'
180
+ })
181
+ }),
182
+ /*#__PURE__*/ jsxs(ActionsMenuContent, {
183
+ popoverPlacement: "bottom-end",
184
+ zIndex: 2,
185
+ minWidth: "22rem",
186
+ children: [
187
+ canUpdate && /*#__PURE__*/ jsx(Menu.Item, {
188
+ startIcon: /*#__PURE__*/ jsx(ArrowsCounterClockwise, {}),
189
+ disabled: isReplacing,
190
+ onSelect: ()=>setIsReplaceOpen(true),
191
+ children: formatMessage({
192
+ id: getTranslationKey('list.assets.actions.replace'),
193
+ defaultMessage: 'Replace media'
194
+ })
195
+ }),
196
+ canCopyLink && /*#__PURE__*/ jsx(Menu.Item, {
197
+ startIcon: /*#__PURE__*/ jsx(Link, {}),
198
+ onSelect: handleCopyLink,
199
+ children: formatMessage({
200
+ id: getTranslationKey('list.assets.actions.copy-link'),
201
+ defaultMessage: 'Copy link to media'
202
+ })
203
+ }),
204
+ canDownload && /*#__PURE__*/ jsx(Menu.Item, {
205
+ startIcon: /*#__PURE__*/ jsx(Download, {}),
206
+ disabled: isDownloading,
207
+ onSelect: handleDownload,
208
+ children: formatMessage({
209
+ id: getTranslationKey('list.assets.actions.download'),
210
+ defaultMessage: 'Download media'
211
+ })
212
+ }),
213
+ showSeparator && /*#__PURE__*/ jsx(Menu.Separator, {}),
214
+ canUpdate && /*#__PURE__*/ jsxs(Fragment, {
215
+ children: [
216
+ /*#__PURE__*/ jsx(Menu.Item, {
217
+ startIcon: /*#__PURE__*/ jsx(ArrowRight, {}),
218
+ onSelect: ()=>setIsMoveOpen(true),
219
+ children: formatMessage({
220
+ id: getTranslationKey('list.assets.actions.move'),
221
+ defaultMessage: 'Move to folder'
222
+ })
223
+ }),
224
+ /*#__PURE__*/ jsx(Menu.Item, {
225
+ startIcon: /*#__PURE__*/ jsx(Trash, {}),
226
+ variant: "danger",
227
+ onSelect: ()=>setIsDeleteOpen(true),
228
+ children: formatMessage({
229
+ id: getTranslationKey('list.assets.actions.delete'),
230
+ defaultMessage: 'Delete'
231
+ })
232
+ })
233
+ ]
234
+ })
235
+ ]
236
+ })
237
+ ]
238
+ }),
239
+ /*#__PURE__*/ jsx(Dialog.Root, {
240
+ open: isReplaceOpen,
241
+ onOpenChange: setIsReplaceOpen,
242
+ children: /*#__PURE__*/ jsxs(Dialog.Content, {
243
+ children: [
244
+ /*#__PURE__*/ jsx(Dialog.Header, {
245
+ children: formatMessage({
246
+ id: getTranslationKey('asset-details.replace.title'),
247
+ defaultMessage: 'Replace this media file?'
248
+ })
249
+ }),
250
+ /*#__PURE__*/ jsx(Dialog.Body, {
251
+ textAlign: "center",
252
+ children: /*#__PURE__*/ jsxs(Flex, {
253
+ direction: "column",
254
+ textAlign: "center",
255
+ children: [
256
+ /*#__PURE__*/ jsx(Typography, {
257
+ variant: "omega",
258
+ children: formatMessage({
259
+ id: getTranslationKey('asset-details.replace.description'),
260
+ defaultMessage: 'Current content will be permanently replaced.'
261
+ })
262
+ }),
263
+ aiEnabled ? /*#__PURE__*/ jsx(Typography, {
264
+ variant: "omega",
265
+ children: formatMessage({
266
+ id: getTranslationKey('asset-details.replace.description.ai'),
267
+ defaultMessage: 'AI will generate new metadata after upload.'
268
+ })
269
+ }) : null
270
+ ]
271
+ })
272
+ }),
273
+ /*#__PURE__*/ jsxs(Dialog.Footer, {
274
+ children: [
275
+ /*#__PURE__*/ jsx(Dialog.Cancel, {
276
+ children: /*#__PURE__*/ jsx(Button, {
277
+ variant: "tertiary",
278
+ fullWidth: true,
279
+ children: formatMessage({
280
+ id: 'app.components.Button.cancel',
281
+ defaultMessage: 'Cancel'
282
+ })
283
+ })
284
+ }),
285
+ /*#__PURE__*/ jsx(Dialog.Action, {
286
+ children: /*#__PURE__*/ jsx(Button, {
287
+ variant: "secondary",
288
+ onClick: handleReplaceContinue,
289
+ fullWidth: true,
290
+ children: formatMessage({
291
+ id: getTranslationKey('asset-details.replace.continue'),
292
+ defaultMessage: 'Continue'
293
+ })
294
+ })
295
+ })
296
+ ]
297
+ })
298
+ ]
299
+ })
300
+ }),
301
+ isMoveOpen && /*#__PURE__*/ jsx(BulkMoveDialog, {
302
+ open: true,
303
+ onClose: ()=>setIsMoveOpen(false),
304
+ items: moveItems,
305
+ onSuccess: clear
306
+ }),
307
+ isDeleteOpen && /*#__PURE__*/ jsx(DeleteItemsDialog, {
308
+ open: true,
309
+ onClose: ()=>setIsDeleteOpen(false),
310
+ target: {
311
+ fileIds: [
312
+ asset.id
313
+ ],
314
+ folderIds: []
315
+ },
316
+ onSuccess: clear
317
+ })
318
+ ]
319
+ });
320
+ };
321
+
322
+ export { AssetActionsMenu };
323
+ //# sourceMappingURL=AssetActionsMenu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssetActionsMenu.mjs","sources":["../../../../../../admin/src/future/pages/Assets/components/AssetActionsMenu.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\n\nimport { useClipboard, useNotification } from '@strapi/admin/strapi-admin';\nimport {\n Button,\n Dialog,\n Flex,\n IconButton,\n Menu,\n Typography,\n VisuallyHidden,\n} from '@strapi/design-system';\nimport { ArrowRight, ArrowsCounterClockwise, Download, Link, More, Trash } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\n\nimport { useAIMetadataEnabled } from '../../../hooks/useAIMetadataEnabled';\nimport { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions';\nimport { useReplaceAssetMutation } from '../../../services/assets';\nimport { downloadFile } from '../../../utils/downloadFile';\nimport { prefixFileUrlWithBackendUrl } from '../../../utils/files';\nimport { getTranslationKey } from '../../../utils/translations';\nimport { useAssetSelection } from '../hooks/useAssetSelection';\nimport { useBusyAssetsOptional } from '../hooks/useBusyAssets';\n\nimport { ActionsMenuContent } from './ActionsMenuContent';\nimport { BulkMoveDialog } from './BulkMoveDialog';\nimport { DeleteItemsDialog } from './DeleteItemsDialog';\n\nimport type { File } from '../../../../../../shared/contracts/files';\nimport type { DragFileData } from '../../../types/dnd';\n\ninterface AssetActionsMenuProps {\n asset: File;\n /** Drag data for this asset, so the move dialog validates against its real folder. */\n dragData: DragFileData;\n}\n\n/**\n * The \"...\" menu on an asset row/card. Always acts on that one asset, whatever\n * the current multi-selection is (the bulk actions bar is the selection-scoped\n * affordance) — the file mirror of `FolderActionsMenu`.\n *\n * Replace / copy link / download duplicate what the details drawer offers as\n * icon buttons, so the same action is one click away without opening the asset\n * first. They are re-implemented rather than lifted out of the drawer: the\n * drawer versions render as `IconButton`s and report through its in-drawer toast\n * slot, neither of which fits a menu item on a row.\n *\n * Any successful move or delete clears the whole selection: a delete only\n * invalidates RTK tags, so without it an `asset:<id>` key for a file that no\n * longer exists would linger in the selection.\n */\nexport const AssetActionsMenu = ({ asset, dragData }: AssetActionsMenuProps) => {\n const { formatMessage } = useIntl();\n const { copy } = useClipboard();\n const { toggleNotification } = useNotification();\n const { clear } = useAssetSelection();\n // Absent in the asset picker and in unit tests: the replace still runs, it\n // just renders no row-level overlay.\n const markBusy = useBusyAssetsOptional()?.markBusy ?? (() => () => {});\n const {\n canUpdate,\n canDownload,\n canCopyLink,\n isLoading: isLoadingPermissions,\n } = useMediaLibraryPermissions();\n const [replaceAsset, { isLoading: isReplacing }] = useReplaceAssetMutation();\n const aiEnabled = useAIMetadataEnabled({ mime: asset.mime });\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [isReplaceOpen, setIsReplaceOpen] = useState(false);\n const [isMoveOpen, setIsMoveOpen] = useState(false);\n const [isDeleteOpen, setIsDeleteOpen] = useState(false);\n const [isDownloading, setIsDownloading] = useState(false);\n\n // Stable identity: the move dialog memoizes its destination walk on it.\n const moveItems = useMemo(() => [dragData], [dragData]);\n\n // Confirm first, then open the native picker, so the user only commits to\n // replacing after acknowledging the warning (same order as the drawer).\n const handleReplaceContinue = () => {\n setIsReplaceOpen(false);\n fileInputRef.current?.click();\n };\n\n const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the native input so the same file can be picked again later.\n event.target.value = '';\n\n if (!file) {\n return;\n }\n\n // The menu has closed by the time the picker returns, so `isReplacing` has\n // nothing left to disable and the row would otherwise sit inert for the\n // whole upload. Marking the asset busy puts the same overlay the drawer\n // uses on the row/card, cleared when the mutation settles either way.\n const releaseBusy = markBusy(\n asset.id,\n formatMessage({\n id: getTranslationKey('asset-details.replace.loading'),\n defaultMessage: 'Replacing the file…',\n })\n );\n\n let res;\n try {\n res = await replaceAsset({ id: asset.id, file });\n } finally {\n releaseBusy();\n }\n\n if ('error' in res) {\n // `fetchBaseQuery` already unwraps the API envelope, so a server-sent\n // reason (file too large, unsupported type) lands directly on `message`.\n const { message } = res.error as { message?: string };\n toggleNotification({\n type: 'danger',\n message:\n message ??\n formatMessage({\n id: getTranslationKey('asset-details.replace.error'),\n defaultMessage: 'Failed to replace the file.',\n }),\n });\n return;\n }\n\n toggleNotification({\n type: 'success',\n message: formatMessage({\n id: getTranslationKey('asset-details.replace.success'),\n defaultMessage: 'File replaced.',\n }),\n });\n };\n\n const handleCopyLink = async () => {\n const url = prefixFileUrlWithBackendUrl(asset.url);\n\n if (!url) {\n return;\n }\n\n const didCopy = await copy(url);\n\n toggleNotification({\n type: didCopy ? 'success' : 'danger',\n message: formatMessage(\n didCopy\n ? {\n id: getTranslationKey('asset-details.copy-link.success'),\n defaultMessage: 'Link copied.',\n }\n : {\n id: getTranslationKey('asset-details.copy-link.error'),\n defaultMessage: 'Failed to copy the link.',\n }\n ),\n });\n };\n\n const handleDownload = async () => {\n const url = prefixFileUrlWithBackendUrl(asset.url);\n\n if (!url) {\n return;\n }\n\n setIsDownloading(true);\n\n try {\n await downloadFile(url, asset.name);\n } catch {\n toggleNotification({\n type: 'danger',\n message: formatMessage({\n id: getTranslationKey('asset-details.download.error'),\n defaultMessage: 'Failed to download the file.',\n }),\n });\n } finally {\n setIsDownloading(false);\n }\n };\n\n // Two permission-gated groups: replace/copy-link/download above, move/delete\n // below. The separator earns its place only when both survive the gate —\n // `canUpdate` alone puts Replace in the top group, which is not enough.\n const hasTopGroup = canUpdate || canCopyLink || canDownload;\n const hasBottomGroup = canUpdate;\n const showSeparator = (canCopyLink || canDownload) && hasBottomGroup;\n\n // A role can read the library while holding none of these, and an empty\n // popup is worse than no trigger at all. Every flag is `false` until the\n // RBAC check settles, so wait for it — otherwise the trigger unmounts and\n // remounts on first paint for everyone.\n if (!isLoadingPermissions && !hasTopGroup && !hasBottomGroup) {\n return null;\n }\n\n return (\n <>\n <VisuallyHidden>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={false}\n onChange={handleFileChange}\n aria-hidden\n tabIndex={-1}\n />\n </VisuallyHidden>\n {/* Non-modal: Radix's default `modal` marks the rest of the document\n `aria-hidden` and blocks pointer events while the menu is open, so\n clicking a sibling row's trigger only dismissed this one — leaving\n every menu you touched to be closed one click at a time. Non-modal\n dismissal treats that click as both \"close this\" and \"open that\". */}\n <Menu.Root modal={false}>\n <Menu.Trigger\n tag={IconButton}\n icon={<More />}\n variant=\"ghost\"\n label={formatMessage({\n id: getTranslationKey('control-card.more-actions'),\n defaultMessage: 'More actions',\n })}\n />\n {/* `ActionsMenuContent` replaces the design system's flat 15rem clamp\n with the height Radix actually measured, and un-hides the scrollbar\n it would otherwise overflow into. See that file for why. */}\n <ActionsMenuContent popoverPlacement=\"bottom-end\" zIndex={2} minWidth=\"22rem\">\n {canUpdate && (\n <Menu.Item\n startIcon={<ArrowsCounterClockwise />}\n disabled={isReplacing}\n onSelect={() => setIsReplaceOpen(true)}\n >\n {formatMessage({\n id: getTranslationKey('list.assets.actions.replace'),\n defaultMessage: 'Replace media',\n })}\n </Menu.Item>\n )}\n {canCopyLink && (\n <Menu.Item startIcon={<Link />} onSelect={handleCopyLink}>\n {formatMessage({\n id: getTranslationKey('list.assets.actions.copy-link'),\n defaultMessage: 'Copy link to media',\n })}\n </Menu.Item>\n )}\n {canDownload && (\n <Menu.Item startIcon={<Download />} disabled={isDownloading} onSelect={handleDownload}>\n {formatMessage({\n id: getTranslationKey('list.assets.actions.download'),\n defaultMessage: 'Download media',\n })}\n </Menu.Item>\n )}\n {showSeparator && <Menu.Separator />}\n {canUpdate && (\n <>\n <Menu.Item startIcon={<ArrowRight />} onSelect={() => setIsMoveOpen(true)}>\n {formatMessage({\n id: getTranslationKey('list.assets.actions.move'),\n defaultMessage: 'Move to folder',\n })}\n </Menu.Item>\n <Menu.Item\n startIcon={<Trash />}\n variant=\"danger\"\n onSelect={() => setIsDeleteOpen(true)}\n >\n {formatMessage({\n id: getTranslationKey('list.assets.actions.delete'),\n defaultMessage: 'Delete',\n })}\n </Menu.Item>\n </>\n )}\n </ActionsMenuContent>\n </Menu.Root>\n <Dialog.Root open={isReplaceOpen} onOpenChange={setIsReplaceOpen}>\n <Dialog.Content>\n <Dialog.Header>\n {formatMessage({\n id: getTranslationKey('asset-details.replace.title'),\n defaultMessage: 'Replace this media file?',\n })}\n </Dialog.Header>\n <Dialog.Body textAlign=\"center\">\n <Flex direction=\"column\" textAlign=\"center\">\n <Typography variant=\"omega\">\n {formatMessage({\n id: getTranslationKey('asset-details.replace.description'),\n defaultMessage: 'Current content will be permanently replaced.',\n })}\n </Typography>\n {aiEnabled ? (\n <Typography variant=\"omega\">\n {formatMessage({\n id: getTranslationKey('asset-details.replace.description.ai'),\n defaultMessage: 'AI will generate new metadata after upload.',\n })}\n </Typography>\n ) : null}\n </Flex>\n </Dialog.Body>\n <Dialog.Footer>\n <Dialog.Cancel>\n <Button variant=\"tertiary\" fullWidth>\n {formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })}\n </Button>\n </Dialog.Cancel>\n <Dialog.Action>\n <Button variant=\"secondary\" onClick={handleReplaceContinue} fullWidth>\n {formatMessage({\n id: getTranslationKey('asset-details.replace.continue'),\n defaultMessage: 'Continue',\n })}\n </Button>\n </Dialog.Action>\n </Dialog.Footer>\n </Dialog.Content>\n </Dialog.Root>\n {isMoveOpen && (\n <BulkMoveDialog\n open\n onClose={() => setIsMoveOpen(false)}\n items={moveItems}\n onSuccess={clear}\n />\n )}\n {/* Both dialogs live inside the row, so a background refetch that drops the\n row would take an open dialog with it. Nothing invalidates until the\n mutation resolves, and the dialog closes in the same tick, so the\n flows themselves can't trigger it. */}\n {isDeleteOpen && (\n <DeleteItemsDialog\n open\n onClose={() => setIsDeleteOpen(false)}\n target={{ fileIds: [asset.id], folderIds: [] }}\n onSuccess={clear}\n />\n )}\n </>\n );\n};\n"],"names":["AssetActionsMenu","asset","dragData","formatMessage","useIntl","copy","useClipboard","toggleNotification","useNotification","clear","useAssetSelection","markBusy","useBusyAssetsOptional","canUpdate","canDownload","canCopyLink","isLoading","isLoadingPermissions","useMediaLibraryPermissions","replaceAsset","isReplacing","useReplaceAssetMutation","aiEnabled","useAIMetadataEnabled","mime","fileInputRef","useRef","isReplaceOpen","setIsReplaceOpen","useState","isMoveOpen","setIsMoveOpen","isDeleteOpen","setIsDeleteOpen","isDownloading","setIsDownloading","moveItems","useMemo","handleReplaceContinue","current","click","handleFileChange","event","file","target","files","value","releaseBusy","id","getTranslationKey","defaultMessage","res","message","error","type","handleCopyLink","url","prefixFileUrlWithBackendUrl","didCopy","handleDownload","downloadFile","name","hasTopGroup","hasBottomGroup","showSeparator","_jsxs","_Fragment","_jsx","VisuallyHidden","input","ref","multiple","onChange","aria-hidden","tabIndex","Menu","Root","modal","Trigger","tag","IconButton","icon","More","variant","label","ActionsMenuContent","popoverPlacement","zIndex","minWidth","Item","startIcon","ArrowsCounterClockwise","disabled","onSelect","Link","Download","Separator","ArrowRight","Trash","Dialog","open","onOpenChange","Content","Header","Body","textAlign","Flex","direction","Typography","Footer","Cancel","Button","fullWidth","Action","onClick","BulkMoveDialog","onClose","items","onSuccess","DeleteItemsDialog","fileIds","folderIds"],"mappings":"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"}
@@ -156,16 +156,24 @@ const FocalPointHandle = styledComponents.styled.button`
156
156
  cursor: grabbing;
157
157
  }
158
158
  `;
159
+ // Hidden on mobile — the panel (title/hint + numeric crop & focal inputs) is
160
+ // too large for a phone; touch users set the crop by dragging the rectangle and
161
+ // focal handle directly on the image. Shown from tablet up.
159
162
  const InfoBox = styledComponents.styled(designSystem.Box)`
160
- position: absolute;
161
- right: ${({ theme })=>theme.spaces[1]};
162
- bottom: ${({ theme })=>theme.spaces[1]};
163
- width: 100%;
164
- max-width: 32rem;
165
- padding: ${({ theme })=>theme.spaces[3]};
166
- border-radius: ${({ theme })=>theme.borderRadius};
167
- background: ${({ theme })=>theme.colorScheme === 'dark' ? theme.colors.neutral150 : theme.colors.neutral900};
168
- z-index: 20;
163
+ display: none;
164
+
165
+ ${({ theme })=>theme.breakpoints.medium} {
166
+ display: block;
167
+ position: absolute;
168
+ right: ${({ theme })=>theme.spaces[1]};
169
+ bottom: ${({ theme })=>theme.spaces[1]};
170
+ width: 100%;
171
+ max-width: 32rem;
172
+ padding: ${({ theme })=>theme.spaces[3]};
173
+ border-radius: ${({ theme })=>theme.borderRadius};
174
+ background: ${({ theme })=>theme.colorScheme === 'dark' ? theme.colors.neutral150 : theme.colors.neutral900};
175
+ z-index: 20;
176
+ }
169
177
  `;
170
178
  const FooterBar = styledComponents.styled(designSystem.Flex)`
171
179
  width: 100%;
@@ -239,12 +247,21 @@ const AssetCropEditor = ({ asset, isBusy = false, onClose, onApply, onSaveAsCopy
239
247
  x: 50,
240
248
  y: 50
241
249
  });
242
- // Append `updatedAt` as a cache-buster: a replaced asset is served at the same
243
- // URL, so without this, reopening the editor shows the browser-cached old image
244
- // (mirrors AssetPreview).
250
+ // The crop editor reads pixels from a canvas (useCropImg -> toBlob), so its
251
+ // <img> must be CORS-clean and always sets crossOrigin="anonymous" (see the
252
+ // img below) — even for unsigned URLs, which AssetPreview leaves unset because
253
+ // it only displays. That difference is why the crop URL is kept DISTINCT from
254
+ // the thumbnail's: identical URL + different crossOrigin collide in the HTTP
255
+ // cache (#26581), and the crop's anonymous request could otherwise reuse the
256
+ // thumbnail's non-CORS cached response and taint the canvas. So the buster
257
+ // uses `updatedAt` where AssetPreview uses `v`, giving each its own entry
258
+ // (mirrors the legacy PreviewBox fix).
259
+ // Signed URLs get no param at all — a presigned S3 URL breaks if you add one,
260
+ // and on the signed side AssetPreview also loads anonymous, so the identical
261
+ // URL there shares one CORS-consistent entry, which is fine.
245
262
  const rawImageUrl = files.prefixFileUrlWithBackendUrl(asset.url);
246
- const cacheKey = asset.updatedAt ? new Date(asset.updatedAt).getTime() : undefined;
247
- const imageUrl = cacheKey !== undefined ? `${rawImageUrl}${rawImageUrl.includes('?') ? '&' : '?'}v=${cacheKey}` : rawImageUrl;
263
+ const cacheKey = asset.updatedAt && !asset.isUrlSigned ? new Date(asset.updatedAt).getTime() : undefined;
264
+ const imageUrl = cacheKey !== undefined ? `${rawImageUrl}${rawImageUrl.includes('?') ? '&' : '?'}updatedAt=${cacheKey}` : rawImageUrl;
248
265
  const handleImageLoad = ()=>{
249
266
  if (imgRef.current) {
250
267
  init(imgRef.current);
@@ -448,6 +465,10 @@ const AssetCropEditor = ({ asset, isBusy = false, onClose, onApply, onSaveAsCopy
448
465
  ref: imgRef,
449
466
  src: imageUrl,
450
467
  alt: asset.name,
468
+ // Always anonymous, unlike AssetPreview/AssetsGrid which gate it:
469
+ // the editor reads pixels via canvas (useCropImg -> toBlob), which
470
+ // taints on a cross-origin image loaded without CORS, so every
471
+ // remote image must be fetched CORS-clean.
451
472
  crossOrigin: "anonymous",
452
473
  onLoad: handleImageLoad,
453
474
  draggable: false