@strapi/upload 5.51.1 → 5.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/dist/admin/components/AssetCard/VideoPreview.js +1 -1
  2. package/dist/admin/components/AssetCard/VideoPreview.js.map +1 -1
  3. package/dist/admin/components/AssetCard/VideoPreview.mjs +1 -1
  4. package/dist/admin/components/AssetCard/VideoPreview.mjs.map +1 -1
  5. package/dist/admin/components/TableList/TableList.js +1 -1
  6. package/dist/admin/components/TableList/TableList.js.map +1 -1
  7. package/dist/admin/components/TableList/TableList.mjs +1 -1
  8. package/dist/admin/components/TableList/TableList.mjs.map +1 -1
  9. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js +21 -4
  10. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js.map +1 -1
  11. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs +21 -4
  12. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs.map +1 -1
  13. package/dist/admin/future/components/Drawer.js +3 -1
  14. package/dist/admin/future/components/Drawer.js.map +1 -1
  15. package/dist/admin/future/components/Drawer.mjs +3 -1
  16. package/dist/admin/future/components/Drawer.mjs.map +1 -1
  17. package/dist/admin/future/components/UploadProgressDialog.js +200 -27
  18. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
  19. package/dist/admin/future/components/UploadProgressDialog.mjs +203 -30
  20. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
  21. package/dist/admin/future/hooks/useMediaLibraryPermissions.js +35 -0
  22. package/dist/admin/future/hooks/useMediaLibraryPermissions.js.map +1 -0
  23. package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs +33 -0
  24. package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs.map +1 -0
  25. package/dist/admin/future/pages/Assets/AssetsPage.js +203 -78
  26. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  27. package/dist/admin/future/pages/Assets/AssetsPage.mjs +200 -75
  28. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  29. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +75 -32
  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 +75 -32
  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 +46 -19
  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 +47 -20
  36. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  37. package/dist/admin/future/pages/Assets/components/AssetsGrid.js +5 -2
  38. package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
  39. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +5 -2
  40. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  41. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +100 -0
  42. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -0
  43. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +98 -0
  44. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -0
  45. package/dist/admin/future/pages/Assets/components/AssetsTable.js +10 -4
  46. package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
  47. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +10 -4
  48. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
  49. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +140 -22
  50. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
  51. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +142 -24
  52. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
  53. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js +193 -0
  54. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js.map +1 -0
  55. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs +191 -0
  56. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs.map +1 -0
  57. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +18 -1
  58. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
  59. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +18 -1
  60. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
  61. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js +13 -3
  62. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
  63. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +13 -3
  64. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
  65. package/dist/admin/future/pages/Assets/components/EmptyState.js +62 -3
  66. package/dist/admin/future/pages/Assets/components/EmptyState.js.map +1 -1
  67. package/dist/admin/future/pages/Assets/components/EmptyState.mjs +62 -4
  68. package/dist/admin/future/pages/Assets/components/EmptyState.mjs.map +1 -1
  69. package/dist/admin/future/pages/Assets/components/FilterBadges.js +408 -0
  70. package/dist/admin/future/pages/Assets/components/FilterBadges.js.map +1 -0
  71. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs +406 -0
  72. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs.map +1 -0
  73. package/dist/admin/future/pages/Assets/components/FilterMenu.js +273 -0
  74. package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -0
  75. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +267 -0
  76. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -0
  77. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +76 -19
  78. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
  79. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +78 -21
  80. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
  81. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js +33 -0
  82. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js.map +1 -0
  83. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs +31 -0
  84. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs.map +1 -0
  85. package/dist/admin/future/pages/Assets/components/SortMenu.js +21 -32
  86. package/dist/admin/future/pages/Assets/components/SortMenu.js.map +1 -1
  87. package/dist/admin/future/pages/Assets/components/SortMenu.mjs +22 -33
  88. package/dist/admin/future/pages/Assets/components/SortMenu.mjs.map +1 -1
  89. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js +40 -0
  90. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js.map +1 -0
  91. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs +38 -0
  92. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs.map +1 -0
  93. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js +21 -5
  94. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js.map +1 -1
  95. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs +21 -5
  96. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs.map +1 -1
  97. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js +33 -12
  98. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js.map +1 -1
  99. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs +33 -12
  100. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs.map +1 -1
  101. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +40 -4
  102. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
  103. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +40 -4
  104. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
  105. package/dist/admin/future/pages/Assets/hooks/useListFilters.js +168 -0
  106. package/dist/admin/future/pages/Assets/hooks/useListFilters.js.map +1 -0
  107. package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs +162 -0
  108. package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs.map +1 -0
  109. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js +201 -0
  110. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js.map +1 -0
  111. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs +198 -0
  112. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs.map +1 -0
  113. package/dist/admin/future/pages/Assets/utils/listQueryKey.js +2 -2
  114. package/dist/admin/future/pages/Assets/utils/listQueryKey.js.map +1 -1
  115. package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs +2 -2
  116. package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs.map +1 -1
  117. package/dist/admin/future/services/api.js +163 -28
  118. package/dist/admin/future/services/api.js.map +1 -1
  119. package/dist/admin/future/services/api.mjs +163 -30
  120. package/dist/admin/future/services/api.mjs.map +1 -1
  121. package/dist/admin/future/services/assets.js +29 -17
  122. package/dist/admin/future/services/assets.js.map +1 -1
  123. package/dist/admin/future/services/assets.mjs +29 -17
  124. package/dist/admin/future/services/assets.mjs.map +1 -1
  125. package/dist/admin/future/services/folders.js +29 -17
  126. package/dist/admin/future/services/folders.js.map +1 -1
  127. package/dist/admin/future/services/folders.mjs +29 -17
  128. package/dist/admin/future/services/folders.mjs.map +1 -1
  129. package/dist/admin/future/services/settings.js +10 -3
  130. package/dist/admin/future/services/settings.js.map +1 -1
  131. package/dist/admin/future/services/settings.mjs +10 -3
  132. package/dist/admin/future/services/settings.mjs.map +1 -1
  133. package/dist/admin/future/store/uploadProgress.js +109 -2
  134. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  135. package/dist/admin/future/store/uploadProgress.mjs +105 -3
  136. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  137. package/dist/admin/future/utils/searchQueryParam.js +11 -0
  138. package/dist/admin/future/utils/searchQueryParam.js.map +1 -0
  139. package/dist/admin/future/utils/searchQueryParam.mjs +9 -0
  140. package/dist/admin/future/utils/searchQueryParam.mjs.map +1 -0
  141. package/dist/admin/hooks/useSelectionState.js +2 -4
  142. package/dist/admin/hooks/useSelectionState.js.map +1 -1
  143. package/dist/admin/hooks/useSelectionState.mjs +2 -4
  144. package/dist/admin/hooks/useSelectionState.mjs.map +1 -1
  145. package/dist/admin/shared/constants.js +43 -0
  146. package/dist/admin/shared/constants.js.map +1 -0
  147. package/dist/admin/shared/constants.mjs +38 -0
  148. package/dist/admin/shared/constants.mjs.map +1 -0
  149. package/dist/admin/src/future/hooks/useMediaLibraryPermissions.d.ts +21 -0
  150. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetCropEditor.d.ts +7 -1
  151. package/dist/admin/src/future/pages/Assets/components/AssetsSearchInput.d.ts +5 -0
  152. package/dist/admin/src/future/pages/Assets/components/BulkActionsBar.d.ts +14 -1
  153. package/dist/admin/src/future/pages/Assets/components/DateRangeCalendar.d.ts +8 -0
  154. package/dist/admin/src/future/pages/Assets/components/DropZone/UploadDropZoneContext.d.ts +8 -1
  155. package/dist/admin/src/future/pages/Assets/components/EmptyState.d.ts +18 -1
  156. package/dist/admin/src/future/pages/Assets/components/FilterBadges.d.ts +6 -0
  157. package/dist/admin/src/future/pages/Assets/components/FilterMenu.d.ts +22 -0
  158. package/dist/admin/src/future/pages/Assets/components/FolderTree/FolderTree.d.ts +13 -4
  159. package/dist/admin/src/future/pages/Assets/components/FolderTree/filterFolderTree.d.ts +16 -0
  160. package/dist/admin/src/future/pages/Assets/components/SortMenu.d.ts +6 -0
  161. package/dist/admin/src/future/pages/Assets/hooks/useAssetSearch.d.ts +12 -0
  162. package/dist/admin/src/future/pages/Assets/hooks/useAssetSelection.d.ts +8 -1
  163. package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +13 -3
  164. package/dist/admin/src/future/pages/Assets/hooks/useListFilters.d.ts +69 -0
  165. package/dist/admin/src/future/pages/Assets/utils/buildAssetFilters.d.ts +46 -0
  166. package/dist/admin/src/future/pages/Assets/utils/listQueryKey.d.ts +3 -4
  167. package/dist/admin/src/future/services/api.d.ts +53 -5
  168. package/dist/admin/src/future/services/assets.d.ts +9 -1
  169. package/dist/admin/src/future/services/folders.d.ts +3 -0
  170. package/dist/admin/src/future/services/settings.d.ts +2 -2
  171. package/dist/admin/src/future/store/uploadProgress.d.ts +83 -1
  172. package/dist/admin/src/future/utils/searchQueryParam.d.ts +7 -0
  173. package/dist/admin/src/hooks/useAiAvailability.d.ts +1 -1
  174. package/dist/admin/src/hooks/useEditAsset.d.ts +4 -4
  175. package/dist/admin/src/hooks/useEditFolder.d.ts +4 -4
  176. package/dist/admin/src/hooks/useSettings.d.ts +1 -0
  177. package/dist/admin/src/hooks/useTracking.d.ts +1 -1
  178. package/dist/admin/src/hooks/useUpload.d.ts +1 -1
  179. package/dist/admin/translations/en.json.js +53 -3
  180. package/dist/admin/translations/en.json.js.map +1 -1
  181. package/dist/admin/translations/en.json.mjs +53 -3
  182. package/dist/admin/translations/en.json.mjs.map +1 -1
  183. package/dist/admin/utils/urlYupSchema.js +33 -9
  184. package/dist/admin/utils/urlYupSchema.js.map +1 -1
  185. package/dist/admin/utils/urlYupSchema.mjs +33 -9
  186. package/dist/admin/utils/urlYupSchema.mjs.map +1 -1
  187. package/dist/server/config.js +13 -5
  188. package/dist/server/config.js.map +1 -1
  189. package/dist/server/config.mjs +13 -5
  190. package/dist/server/config.mjs.map +1 -1
  191. package/dist/server/constants.js.map +1 -1
  192. package/dist/server/constants.mjs.map +1 -1
  193. package/dist/server/controllers/admin-file.js +30 -0
  194. package/dist/server/controllers/admin-file.js.map +1 -1
  195. package/dist/server/controllers/admin-file.mjs +30 -0
  196. package/dist/server/controllers/admin-file.mjs.map +1 -1
  197. package/dist/server/controllers/admin-settings.js +20 -2
  198. package/dist/server/controllers/admin-settings.js.map +1 -1
  199. package/dist/server/controllers/admin-settings.mjs +21 -3
  200. package/dist/server/controllers/admin-settings.mjs.map +1 -1
  201. package/dist/server/controllers/content-api.js +1 -2
  202. package/dist/server/controllers/content-api.js.map +1 -1
  203. package/dist/server/controllers/content-api.mjs +2 -2
  204. package/dist/server/controllers/content-api.mjs.map +1 -1
  205. package/dist/server/controllers/validation/admin/ai-metadata.js +12 -0
  206. package/dist/server/controllers/validation/admin/ai-metadata.js.map +1 -0
  207. package/dist/server/controllers/validation/admin/ai-metadata.mjs +10 -0
  208. package/dist/server/controllers/validation/admin/ai-metadata.mjs.map +1 -0
  209. package/dist/server/controllers/validation/admin/settings.js +4 -1
  210. package/dist/server/controllers/validation/admin/settings.js.map +1 -1
  211. package/dist/server/controllers/validation/admin/settings.mjs +1 -1
  212. package/dist/server/controllers/validation/admin/settings.mjs.map +1 -1
  213. package/dist/server/routes/admin.js +18 -0
  214. package/dist/server/routes/admin.js.map +1 -1
  215. package/dist/server/routes/admin.mjs +18 -0
  216. package/dist/server/routes/admin.mjs.map +1 -1
  217. package/dist/server/services/ai-metadata.js +108 -11
  218. package/dist/server/services/ai-metadata.js.map +1 -1
  219. package/dist/server/services/ai-metadata.mjs +108 -11
  220. package/dist/server/services/ai-metadata.mjs.map +1 -1
  221. package/dist/server/services/file.js +86 -13
  222. package/dist/server/services/file.js.map +1 -1
  223. package/dist/server/services/file.mjs +87 -14
  224. package/dist/server/services/file.mjs.map +1 -1
  225. package/dist/server/services/image-manipulation.js +4 -4
  226. package/dist/server/services/image-manipulation.js.map +1 -1
  227. package/dist/server/services/image-manipulation.mjs +4 -4
  228. package/dist/server/services/image-manipulation.mjs.map +1 -1
  229. package/dist/server/shared/constants.js +43 -0
  230. package/dist/server/shared/constants.js.map +1 -0
  231. package/dist/server/shared/constants.mjs +38 -0
  232. package/dist/server/shared/constants.mjs.map +1 -0
  233. package/dist/server/src/config.d.ts +2 -0
  234. package/dist/server/src/config.d.ts.map +1 -1
  235. package/dist/server/src/constants.d.ts +2 -1
  236. package/dist/server/src/constants.d.ts.map +1 -1
  237. package/dist/server/src/controllers/admin-file.d.ts +10 -0
  238. package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
  239. package/dist/server/src/controllers/admin-settings.d.ts.map +1 -1
  240. package/dist/server/src/controllers/index.d.ts +1 -0
  241. package/dist/server/src/controllers/index.d.ts.map +1 -1
  242. package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts +2 -0
  243. package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts.map +1 -0
  244. package/dist/server/src/controllers/validation/admin/settings.d.ts +1 -1
  245. package/dist/server/src/controllers/validation/admin/settings.d.ts.map +1 -1
  246. package/dist/server/src/index.d.ts +7 -1
  247. package/dist/server/src/index.d.ts.map +1 -1
  248. package/dist/server/src/routes/admin.d.ts.map +1 -1
  249. package/dist/server/src/services/ai-metadata.d.ts +19 -0
  250. package/dist/server/src/services/ai-metadata.d.ts.map +1 -1
  251. package/dist/server/src/services/file.d.ts +12 -2
  252. package/dist/server/src/services/file.d.ts.map +1 -1
  253. package/dist/server/src/services/index.d.ts +4 -1
  254. package/dist/server/src/services/index.d.ts.map +1 -1
  255. package/dist/server/src/types.d.ts +3 -0
  256. package/dist/server/src/types.d.ts.map +1 -1
  257. package/dist/shared/constants.d.ts +35 -0
  258. package/dist/shared/constants.d.ts.map +1 -0
  259. package/dist/shared/contracts/files.d.ts +25 -0
  260. package/dist/shared/contracts/files.d.ts.map +1 -1
  261. package/dist/shared/contracts/settings.d.ts +7 -0
  262. package/package.json +7 -7
@@ -7,16 +7,17 @@ var designSystem = require('@strapi/design-system');
7
7
  var icons = require('@strapi/icons');
8
8
  var reactIntl = require('react-intl');
9
9
  var styledComponents = require('styled-components');
10
+ var constants = require('../../../../shared/constants.js');
10
11
  var useAiAvailability = require('../../../../hooks/useAiAvailability.js');
12
+ var useMediaLibraryPermissions = require('../../../hooks/useMediaLibraryPermissions.js');
11
13
  var assets = require('../../../services/assets.js');
12
14
  var translations = require('../../../utils/translations.js');
13
15
  var useAssetSelection = require('../hooks/useAssetSelection.js');
14
16
  var BulkMoveDialog = require('./BulkMoveDialog.js');
17
+ var api = require('../../../services/api.js');
15
18
 
16
19
  /**
17
20
  * Floating bulk action bar for the future Media Library.
18
- *
19
- * TODO: create-metadata control is a styled stub (toast on click)
20
21
  */ const Bar = styledComponents.styled(designSystem.Flex)`
21
22
  position: fixed;
22
23
  bottom: ${({ theme })=>theme.spaces[4]};
@@ -42,23 +43,130 @@ const VerticalDivider = styledComponents.styled(designSystem.Box)`
42
43
  background: ${({ theme })=>theme.colors.neutral150};
43
44
  margin-left: ${({ theme })=>theme.spaces[1]};
44
45
  `;
45
- const BulkActionsBar = ()=>{
46
+ const BulkActionsBar = ({ assets: assets$1 = [] })=>{
46
47
  const { formatMessage } = reactIntl.useIntl();
47
48
  const { toggleNotification } = strapiAdmin.useNotification();
48
49
  const { isEnabled: isAiMetadataEnabled } = useAiAvailability.useAIAvailability();
50
+ // Every bulk action (move, delete, metadata) is an `assets.update` mutation
51
+ // server-side — one flag gates the whole cluster.
52
+ const { canUpdate } = useMediaLibraryPermissions.useMediaLibraryPermissions();
49
53
  const { selectedIds, selectedFolderIds, clear } = useAssetSelection.useAssetSelection();
50
54
  const [bulkDeleteItems, { isLoading: isDeleting }] = assets.useBulkDeleteItemsMutation();
55
+ const [generateAiMetadata, { isLoading: isGeneratingMetadata }] = api.useGenerateAiMetadataMutation();
51
56
  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
52
57
  const [isMoveDialogOpen, setIsMoveDialogOpen] = React.useState(false);
53
58
  const count = selectedIds.size + selectedFolderIds.size;
54
- const showStubNotification = (translationKey, defaultMessage)=>{
55
- toggleNotification({
56
- type: 'info',
57
- message: formatMessage({
58
- id: translations.getTranslationKey(translationKey),
59
- defaultMessage
60
- })
59
+ const isBusy = isDeleting || isGeneratingMetadata;
60
+ /**
61
+ * The server processes the whole selection inside a single request and
62
+ * rejects anything above the cap with a 400. Disable the action up front so
63
+ * the limit is discoverable before the user commits to a request.
64
+ */ const isOverMetadataLimit = selectedIds.size > constants.AI_METADATA_MAX_FILES;
65
+ /**
66
+ * How many selected assets the AI provider can actually read. The server
67
+ * stays authoritative — it still reports what it skipped — but a selection
68
+ * with nothing eligible (a lone PDF, say) would only ever come back fully
69
+ * skipped, so there is no point sending it.
70
+ */ const eligibleCount = React.useMemo(()=>{
71
+ const mimeById = new Map(assets$1.map(({ id, mime })=>[
72
+ id,
73
+ mime
74
+ ]));
75
+ return [
76
+ ...selectedIds
77
+ ].filter((id)=>constants.isAIMetadataSupportedMime(mimeById.get(id))).length;
78
+ }, [
79
+ assets$1,
80
+ selectedIds
81
+ ]);
82
+ const hasNoEligibleAssets = selectedIds.size > 0 && eligibleCount === 0;
83
+ /**
84
+ * Why the action is unavailable, surfaced on hover. `undefined` leaves the
85
+ * button without a tooltip — either it is usable, or it is disabled for a
86
+ * reason the selection count already makes obvious.
87
+ */ let metadataDisabledReason;
88
+ if (isOverMetadataLimit) {
89
+ metadataDisabledReason = formatMessage({
90
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.too-many'),
91
+ defaultMessage: 'Metadata can be generated for up to {max} assets at a time. Select fewer assets to continue.'
92
+ }, {
93
+ max: constants.AI_METADATA_MAX_FILES
94
+ });
95
+ } else if (hasNoEligibleAssets) {
96
+ metadataDisabledReason = formatMessage({
97
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.no-eligible'),
98
+ defaultMessage: 'Metadata can only be generated for images. None of the selected assets are supported.'
99
+ });
100
+ }
101
+ /**
102
+ * Metadata generation only applies to images — folders and unsupported files
103
+ * in the selection are reported back rather than blocking the action.
104
+ */ const handleCreateMetadata = async ()=>{
105
+ // Guard re-entry while pending, and never send a selection the server
106
+ // would reject outright or skip in full.
107
+ if (isGeneratingMetadata || isOverMetadataLimit || hasNoEligibleAssets) {
108
+ return;
109
+ }
110
+ const fileIds = Array.from(selectedIds);
111
+ const res = await generateAiMetadata({
112
+ fileIds
61
113
  });
114
+ if ('error' in res) {
115
+ // Keep the selection intact so the user can retry.
116
+ toggleNotification({
117
+ type: 'danger',
118
+ message: formatMessage({
119
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.error'),
120
+ defaultMessage: 'An error occurred while generating metadata.'
121
+ })
122
+ });
123
+ return;
124
+ }
125
+ const successCount = res.data.filter(({ status })=>status === 'success').length;
126
+ const skippedCount = res.data.filter(({ status })=>status === 'skipped').length;
127
+ const errorCount = res.data.filter(({ status })=>status === 'error').length;
128
+ // Folders are never sent, so they are not part of the response — but the
129
+ // user selected them, and a silent omission reads as a bug.
130
+ const folderCount = selectedFolderIds.size;
131
+ if (errorCount === res.data.length) {
132
+ // Nothing was written — treat it like a request-level failure and keep
133
+ // the selection so the user can retry.
134
+ toggleNotification({
135
+ type: 'danger',
136
+ message: formatMessage({
137
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.error'),
138
+ defaultMessage: 'An error occurred while generating metadata.'
139
+ })
140
+ });
141
+ return;
142
+ }
143
+ if (skippedCount === 0 && errorCount === 0 && folderCount === 0) {
144
+ toggleNotification({
145
+ type: 'success',
146
+ message: formatMessage({
147
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.success'),
148
+ defaultMessage: '{count, plural, =1 {Metadata generated for # asset} other {Metadata generated for # assets}}'
149
+ }, {
150
+ count: successCount
151
+ })
152
+ });
153
+ } else {
154
+ // Partial outcome: report every bucket so the user knows what was left
155
+ // out — including folders, which were never eligible in the first place.
156
+ toggleNotification({
157
+ type: 'warning',
158
+ message: formatMessage({
159
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata.partial'),
160
+ defaultMessage: '{successCount} generated, {skippedCount} skipped (unsupported file type), {errorCount} failed{folderCount, plural, =0 {} one {, # folder ignored} other {, # folders ignored}}'
161
+ }, {
162
+ successCount,
163
+ skippedCount,
164
+ errorCount,
165
+ folderCount
166
+ })
167
+ });
168
+ }
169
+ clear();
62
170
  };
63
171
  const handleConfirmDelete = async (e)=>{
64
172
  // Radix AlertDialog.Action closes the dialog on click by default; prevent
@@ -97,7 +205,11 @@ const BulkActionsBar = ()=>{
97
205
  });
98
206
  clear();
99
207
  };
100
- if (count === 0) {
208
+ // Every bulk action is behind `assets.update`; with nothing selected, or
209
+ // without the permission, the bar has nothing to offer — drop it entirely
210
+ // rather than show a count + "Clear selection" over no actions (mirrors the
211
+ // drawer footer, which hides when no permitted action survives).
212
+ if (count === 0 || !canUpdate) {
101
213
  return null;
102
214
  }
103
215
  return /*#__PURE__*/ jsxRuntime.jsxs(Bar, {
@@ -121,19 +233,25 @@ const BulkActionsBar = ()=>{
121
233
  }),
122
234
  /*#__PURE__*/ jsxRuntime.jsxs(ActionCluster, {
123
235
  children: [
124
- isAiMetadataEnabled && /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
125
- size: "S",
126
- startIcon: /*#__PURE__*/ jsxRuntime.jsx(icons.Sparkle, {}),
127
- disabled: isDeleting,
128
- onClick: ()=>showStubNotification('list.bulk-actions.create-metadata-not-available', "Generate metadata isn't available yet"),
129
- children: formatMessage({
130
- id: translations.getTranslationKey('list.bulk-actions.create-metadata'),
131
- defaultMessage: 'Create metadata'
236
+ isAiMetadataEnabled && /*#__PURE__*/ jsxRuntime.jsx(designSystem.Tooltip, {
237
+ label: metadataDisabledReason,
238
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
239
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
240
+ size: "S",
241
+ startIcon: /*#__PURE__*/ jsxRuntime.jsx(icons.Sparkle, {}),
242
+ disabled: isBusy || selectedIds.size === 0 || isOverMetadataLimit || hasNoEligibleAssets,
243
+ loading: isGeneratingMetadata,
244
+ onClick: handleCreateMetadata,
245
+ children: formatMessage({
246
+ id: translations.getTranslationKey('list.bulk-actions.create-metadata'),
247
+ defaultMessage: 'Create metadata'
248
+ })
249
+ })
132
250
  })
133
251
  }),
134
252
  /*#__PURE__*/ jsxRuntime.jsx(designSystem.IconButton, {
135
253
  variant: "tertiary",
136
- disabled: isDeleting,
254
+ disabled: isBusy,
137
255
  label: formatMessage({
138
256
  id: translations.getTranslationKey('list.bulk-actions.move'),
139
257
  defaultMessage: 'Move'
@@ -158,7 +276,7 @@ const BulkActionsBar = ()=>{
158
276
  /*#__PURE__*/ jsxRuntime.jsx(designSystem.Dialog.Trigger, {
159
277
  children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.IconButton, {
160
278
  variant: "danger-light",
161
- disabled: isDeleting,
279
+ disabled: isBusy,
162
280
  label: formatMessage({
163
281
  id: translations.getTranslationKey('list.bulk-actions.delete'),
164
282
  defaultMessage: 'Delete'
@@ -241,7 +359,7 @@ const BulkActionsBar = ()=>{
241
359
  defaultMessage: 'Clear selection'
242
360
  }),
243
361
  onClick: clear,
244
- disabled: isDeleting,
362
+ disabled: isBusy,
245
363
  children: /*#__PURE__*/ jsxRuntime.jsx(icons.Cross, {})
246
364
  })
247
365
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"BulkActionsBar.js","sources":["../../../../../../admin/src/future/pages/Assets/components/BulkActionsBar.tsx"],"sourcesContent":["import { useState, type MouseEvent } from 'react';\n\nimport { useNotification } from '@strapi/admin/strapi-admin';\nimport { Box, Button, Dialog, Flex, IconButton, Typography } from '@strapi/design-system';\nimport { ArrowRight, Cross, Sparkle, Trash, WarningCircle } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { styled } from 'styled-components';\n\nimport { useAIAvailability } from '../../../../hooks/useAiAvailability';\nimport { useBulkDeleteItemsMutation } from '../../../services/assets';\nimport { getTranslationKey } from '../../../utils/translations';\nimport { useAssetSelection } from '../hooks/useAssetSelection';\n\nimport { BulkMoveDialog } from './BulkMoveDialog';\n\n/**\n * Floating bulk action bar for the future Media Library.\n *\n * TODO: create-metadata control is a styled stub (toast on click)\n */\nconst Bar = styled(Flex)`\n position: fixed;\n bottom: ${({ theme }) => theme.spaces[4]};\n left: 50%;\n transform: translateX(-50%);\n z-index: ${({ theme }) => theme.zIndices.popover};\n align-items: center;\n gap: ${({ theme }) => theme.spaces[2]};\n padding: ${({ theme }) =>\n `${theme.spaces[3]} ${theme.spaces[2]} ${theme.spaces[3]} ${theme.spaces[6]}`};\n background: ${({ theme }) => theme.colors.neutral0};\n border: 1px solid ${({ theme }) => theme.colors.neutral150};\n border-radius: ${({ theme }) => theme.borderRadius};\n box-shadow: ${({ theme }) => theme.shadows.popupShadow};\n`;\n\nconst ActionCluster = styled(Flex)`\n margin-left: auto;\n align-items: center;\n gap: ${({ theme }) => theme.spaces[2]};\n`;\n\nconst VerticalDivider = styled(Box)`\n width: 1px;\n align-self: stretch;\n background: ${({ theme }) => theme.colors.neutral150};\n margin-left: ${({ theme }) => theme.spaces[1]};\n`;\n\nexport const BulkActionsBar = () => {\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n const { isEnabled: isAiMetadataEnabled } = useAIAvailability();\n const { selectedIds, selectedFolderIds, clear } = useAssetSelection();\n const [bulkDeleteItems, { isLoading: isDeleting }] = useBulkDeleteItemsMutation();\n const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);\n\n const count = selectedIds.size + selectedFolderIds.size;\n\n const showStubNotification = (translationKey: string, defaultMessage: string) => {\n toggleNotification({\n type: 'info',\n message: formatMessage({\n id: getTranslationKey(translationKey),\n defaultMessage,\n }),\n });\n };\n\n const handleConfirmDelete = async (e: MouseEvent) => {\n // Radix AlertDialog.Action closes the dialog on click by default; prevent\n // that so the dialog stays open showing the loader while the request runs\n // (a bulk delete can take a while depending on the number of assets).\n e.preventDefault();\n\n // Guard re-entry while pending.\n if (isDeleting) {\n return;\n }\n\n const res = await bulkDeleteItems({\n fileIds: Array.from(selectedIds),\n folderIds: Array.from(selectedFolderIds),\n });\n\n if ('error' in res) {\n // Keep the dialog open and the selection intact so the user can retry\n // (Confirm again) or Cancel; only surface the error toast.\n toggleNotification({\n type: 'danger',\n message: formatMessage({\n id: getTranslationKey('list.bulk-actions.delete.error'),\n defaultMessage: 'An error occurred while deleting the items.',\n }),\n });\n return;\n }\n\n setIsDeleteDialogOpen(false);\n toggleNotification({\n type: 'success',\n message: formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.delete.success'),\n defaultMessage:\n '{count, plural, =1 {# item has been deleted} other {# items have been deleted}}',\n },\n { count }\n ),\n });\n clear();\n };\n\n if (count === 0) {\n return null;\n }\n\n return (\n <Bar\n tag=\"section\"\n role=\"region\"\n aria-label={formatMessage({\n id: getTranslationKey('list.bulk-actions.label'),\n defaultMessage: 'Bulk actions',\n })}\n >\n <Typography fontWeight=\"bold\" textColor=\"neutral800\" marginRight={4}>\n {formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.selected-count'),\n defaultMessage: '{count, plural, =1 {# item selected} other {# items selected}}',\n },\n { count }\n )}\n </Typography>\n\n <ActionCluster>\n {isAiMetadataEnabled && (\n <Button\n size=\"S\"\n startIcon={<Sparkle />}\n disabled={isDeleting}\n onClick={() =>\n showStubNotification(\n 'list.bulk-actions.create-metadata-not-available',\n \"Generate metadata isn't available yet\"\n )\n }\n >\n {formatMessage({\n id: getTranslationKey('list.bulk-actions.create-metadata'),\n defaultMessage: 'Create metadata',\n })}\n </Button>\n )}\n\n <IconButton\n variant=\"tertiary\"\n disabled={isDeleting}\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.move'),\n defaultMessage: 'Move',\n })}\n onClick={() => setIsMoveDialogOpen(true)}\n >\n <ArrowRight />\n </IconButton>\n <BulkMoveDialog open={isMoveDialogOpen} onClose={() => setIsMoveDialogOpen(false)} />\n\n <Dialog.Root\n open={isDeleteDialogOpen}\n onOpenChange={(open: boolean) => {\n // The dialog must stay open while the request runs — a bulk delete\n // can't be aborted halfway, so the loader is the source of truth.\n if (!isDeleting) {\n setIsDeleteDialogOpen(open);\n }\n }}\n >\n <Dialog.Trigger>\n <IconButton\n variant=\"danger-light\"\n disabled={isDeleting}\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.delete'),\n defaultMessage: 'Delete',\n })}\n >\n <Trash />\n </IconButton>\n </Dialog.Trigger>\n <Dialog.Content>\n <Dialog.Header>\n {formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.delete.confirm.title'),\n defaultMessage: 'Delete {count, plural, =1 {# item} other {# items}}?',\n },\n { count }\n )}\n </Dialog.Header>\n <Dialog.Body\n icon={<WarningCircle width=\"24px\" height=\"24px\" fill=\"danger600\" />}\n textAlign=\"center\"\n >\n <Typography>\n {formatMessage({\n id: getTranslationKey(\n 'list.bulk-actions.delete.confirm.description.are-you-sure'\n ),\n defaultMessage:\n 'These items cannot be recovered once deleted, and deleting a folder also deletes everything inside it. If they are currently in use, linked content will break and image containers will be empty.',\n })}\n </Typography>\n <Typography>\n {formatMessage({\n id: getTranslationKey(\n 'list.bulk-actions.delete.confirm.description.cant-be-undone'\n ),\n defaultMessage:\n 'This action can’t be undone. Deleting a folder also removes everything inside it, and any linked content will break – media asset containers will appear empty.',\n })}\n </Typography>\n </Dialog.Body>\n <Dialog.Footer>\n <Dialog.Cancel>\n <Button variant=\"tertiary\" disabled={isDeleting} fullWidth>\n {formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })}\n </Button>\n </Dialog.Cancel>\n <Dialog.Action>\n <Button\n variant=\"danger-light\"\n loading={isDeleting}\n onClick={handleConfirmDelete}\n fullWidth\n >\n {formatMessage({\n id: 'app.components.Button.confirm',\n defaultMessage: 'Confirm',\n })}\n </Button>\n </Dialog.Action>\n </Dialog.Footer>\n </Dialog.Content>\n </Dialog.Root>\n </ActionCluster>\n\n <VerticalDivider aria-hidden />\n\n <IconButton\n variant=\"ghost\"\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.clear'),\n defaultMessage: 'Clear selection',\n })}\n onClick={clear}\n disabled={isDeleting}\n >\n <Cross />\n </IconButton>\n </Bar>\n );\n};\n"],"names":["Bar","styled","Flex","theme","spaces","zIndices","popover","colors","neutral0","neutral150","borderRadius","shadows","popupShadow","ActionCluster","VerticalDivider","Box","BulkActionsBar","formatMessage","useIntl","toggleNotification","useNotification","isEnabled","isAiMetadataEnabled","useAIAvailability","selectedIds","selectedFolderIds","clear","useAssetSelection","bulkDeleteItems","isLoading","isDeleting","useBulkDeleteItemsMutation","isDeleteDialogOpen","setIsDeleteDialogOpen","useState","isMoveDialogOpen","setIsMoveDialogOpen","count","size","showStubNotification","translationKey","defaultMessage","type","message","id","getTranslationKey","handleConfirmDelete","e","preventDefault","res","fileIds","Array","from","folderIds","_jsxs","tag","role","aria-label","_jsx","Typography","fontWeight","textColor","marginRight","Button","startIcon","Sparkle","disabled","onClick","IconButton","variant","label","ArrowRight","BulkMoveDialog","open","onClose","Dialog","Root","onOpenChange","Trigger","Trash","Content","Header","Body","icon","WarningCircle","width","height","fill","textAlign","Footer","Cancel","fullWidth","Action","loading","aria-hidden","Cross"],"mappings":"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"}
1
+ {"version":3,"file":"BulkActionsBar.js","sources":["../../../../../../admin/src/future/pages/Assets/components/BulkActionsBar.tsx"],"sourcesContent":["import { useMemo, useState, type MouseEvent } from 'react';\n\nimport { useNotification } from '@strapi/admin/strapi-admin';\nimport { Box, Button, Dialog, Flex, IconButton, Tooltip, Typography } from '@strapi/design-system';\nimport { ArrowRight, Cross, Sparkle, Trash, WarningCircle } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { styled } from 'styled-components';\n\nimport {\n AI_METADATA_MAX_FILES,\n isAIMetadataSupportedMime,\n} from '../../../../../../shared/constants';\nimport { useAIAvailability } from '../../../../hooks/useAiAvailability';\nimport { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions';\nimport {\n useBulkDeleteItemsMutation,\n useGenerateAiMetadataMutation,\n} from '../../../services/assets';\nimport { getTranslationKey } from '../../../utils/translations';\nimport { useAssetSelection } from '../hooks/useAssetSelection';\n\nimport { BulkMoveDialog } from './BulkMoveDialog';\n\nimport type { File } from '../../../../../../shared/contracts/files';\n\n/**\n * Floating bulk action bar for the future Media Library.\n */\nconst Bar = styled(Flex)`\n position: fixed;\n bottom: ${({ theme }) => theme.spaces[4]};\n left: 50%;\n transform: translateX(-50%);\n z-index: ${({ theme }) => theme.zIndices.popover};\n align-items: center;\n gap: ${({ theme }) => theme.spaces[2]};\n padding: ${({ theme }) =>\n `${theme.spaces[3]} ${theme.spaces[2]} ${theme.spaces[3]} ${theme.spaces[6]}`};\n background: ${({ theme }) => theme.colors.neutral0};\n border: 1px solid ${({ theme }) => theme.colors.neutral150};\n border-radius: ${({ theme }) => theme.borderRadius};\n box-shadow: ${({ theme }) => theme.shadows.popupShadow};\n`;\n\nconst ActionCluster = styled(Flex)`\n margin-left: auto;\n align-items: center;\n gap: ${({ theme }) => theme.spaces[2]};\n`;\n\nconst VerticalDivider = styled(Box)`\n width: 1px;\n align-self: stretch;\n background: ${({ theme }) => theme.colors.neutral150};\n margin-left: ${({ theme }) => theme.spaces[1]};\n`;\n\ninterface BulkActionsBarProps {\n /**\n * The assets currently rendered in the list, used to resolve the mime types\n * of the selection — the bar needs them to know which selected assets the AI\n * provider can actually read.\n *\n * Selection is page-scoped and cleared whenever the list identity changes, so\n * every selected asset id is guaranteed to appear here.\n */\n assets?: File[];\n}\n\nexport const BulkActionsBar = ({ assets = [] }: BulkActionsBarProps) => {\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n const { isEnabled: isAiMetadataEnabled } = useAIAvailability();\n // Every bulk action (move, delete, metadata) is an `assets.update` mutation\n // server-side — one flag gates the whole cluster.\n const { canUpdate } = useMediaLibraryPermissions();\n const { selectedIds, selectedFolderIds, clear } = useAssetSelection();\n const [bulkDeleteItems, { isLoading: isDeleting }] = useBulkDeleteItemsMutation();\n const [generateAiMetadata, { isLoading: isGeneratingMetadata }] = useGenerateAiMetadataMutation();\n const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);\n\n const count = selectedIds.size + selectedFolderIds.size;\n const isBusy = isDeleting || isGeneratingMetadata;\n\n /**\n * The server processes the whole selection inside a single request and\n * rejects anything above the cap with a 400. Disable the action up front so\n * the limit is discoverable before the user commits to a request.\n */\n const isOverMetadataLimit = selectedIds.size > AI_METADATA_MAX_FILES;\n\n /**\n * How many selected assets the AI provider can actually read. The server\n * stays authoritative — it still reports what it skipped — but a selection\n * with nothing eligible (a lone PDF, say) would only ever come back fully\n * skipped, so there is no point sending it.\n */\n const eligibleCount = useMemo(() => {\n const mimeById = new Map(assets.map(({ id, mime }) => [id, mime]));\n\n return [...selectedIds].filter((id) => isAIMetadataSupportedMime(mimeById.get(id))).length;\n }, [assets, selectedIds]);\n\n const hasNoEligibleAssets = selectedIds.size > 0 && eligibleCount === 0;\n\n /**\n * Why the action is unavailable, surfaced on hover. `undefined` leaves the\n * button without a tooltip — either it is usable, or it is disabled for a\n * reason the selection count already makes obvious.\n */\n let metadataDisabledReason: string | undefined;\n\n if (isOverMetadataLimit) {\n metadataDisabledReason = formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.create-metadata.too-many'),\n defaultMessage:\n 'Metadata can be generated for up to {max} assets at a time. Select fewer assets to continue.',\n },\n { max: AI_METADATA_MAX_FILES }\n );\n } else if (hasNoEligibleAssets) {\n metadataDisabledReason = formatMessage({\n id: getTranslationKey('list.bulk-actions.create-metadata.no-eligible'),\n defaultMessage:\n 'Metadata can only be generated for images. None of the selected assets are supported.',\n });\n }\n\n /**\n * Metadata generation only applies to images — folders and unsupported files\n * in the selection are reported back rather than blocking the action.\n */\n const handleCreateMetadata = async () => {\n // Guard re-entry while pending, and never send a selection the server\n // would reject outright or skip in full.\n if (isGeneratingMetadata || isOverMetadataLimit || hasNoEligibleAssets) {\n return;\n }\n\n const fileIds = Array.from(selectedIds);\n const res = await generateAiMetadata({ fileIds });\n\n if ('error' in res) {\n // Keep the selection intact so the user can retry.\n toggleNotification({\n type: 'danger',\n message: formatMessage({\n id: getTranslationKey('list.bulk-actions.create-metadata.error'),\n defaultMessage: 'An error occurred while generating metadata.',\n }),\n });\n return;\n }\n\n const successCount = res.data.filter(({ status }) => status === 'success').length;\n const skippedCount = res.data.filter(({ status }) => status === 'skipped').length;\n const errorCount = res.data.filter(({ status }) => status === 'error').length;\n // Folders are never sent, so they are not part of the response — but the\n // user selected them, and a silent omission reads as a bug.\n const folderCount = selectedFolderIds.size;\n\n if (errorCount === res.data.length) {\n // Nothing was written — treat it like a request-level failure and keep\n // the selection so the user can retry.\n toggleNotification({\n type: 'danger',\n message: formatMessage({\n id: getTranslationKey('list.bulk-actions.create-metadata.error'),\n defaultMessage: 'An error occurred while generating metadata.',\n }),\n });\n return;\n }\n\n if (skippedCount === 0 && errorCount === 0 && folderCount === 0) {\n toggleNotification({\n type: 'success',\n message: formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.create-metadata.success'),\n defaultMessage:\n '{count, plural, =1 {Metadata generated for # asset} other {Metadata generated for # assets}}',\n },\n { count: successCount }\n ),\n });\n } else {\n // Partial outcome: report every bucket so the user knows what was left\n // out — including folders, which were never eligible in the first place.\n toggleNotification({\n type: 'warning',\n message: formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.create-metadata.partial'),\n defaultMessage:\n '{successCount} generated, {skippedCount} skipped (unsupported file type), {errorCount} failed{folderCount, plural, =0 {} one {, # folder ignored} other {, # folders ignored}}',\n },\n { successCount, skippedCount, errorCount, folderCount }\n ),\n });\n }\n\n clear();\n };\n\n const handleConfirmDelete = async (e: MouseEvent) => {\n // Radix AlertDialog.Action closes the dialog on click by default; prevent\n // that so the dialog stays open showing the loader while the request runs\n // (a bulk delete can take a while depending on the number of assets).\n e.preventDefault();\n\n // Guard re-entry while pending.\n if (isDeleting) {\n return;\n }\n\n const res = await bulkDeleteItems({\n fileIds: Array.from(selectedIds),\n folderIds: Array.from(selectedFolderIds),\n });\n\n if ('error' in res) {\n // Keep the dialog open and the selection intact so the user can retry\n // (Confirm again) or Cancel; only surface the error toast.\n toggleNotification({\n type: 'danger',\n message: formatMessage({\n id: getTranslationKey('list.bulk-actions.delete.error'),\n defaultMessage: 'An error occurred while deleting the items.',\n }),\n });\n return;\n }\n\n setIsDeleteDialogOpen(false);\n toggleNotification({\n type: 'success',\n message: formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.delete.success'),\n defaultMessage:\n '{count, plural, =1 {# item has been deleted} other {# items have been deleted}}',\n },\n { count }\n ),\n });\n clear();\n };\n\n // Every bulk action is behind `assets.update`; with nothing selected, or\n // without the permission, the bar has nothing to offer — drop it entirely\n // rather than show a count + \"Clear selection\" over no actions (mirrors the\n // drawer footer, which hides when no permitted action survives).\n if (count === 0 || !canUpdate) {\n return null;\n }\n\n return (\n <Bar\n tag=\"section\"\n role=\"region\"\n aria-label={formatMessage({\n id: getTranslationKey('list.bulk-actions.label'),\n defaultMessage: 'Bulk actions',\n })}\n >\n <Typography fontWeight=\"bold\" textColor=\"neutral800\" marginRight={4}>\n {formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.selected-count'),\n defaultMessage: '{count, plural, =1 {# item selected} other {# items selected}}',\n },\n { count }\n )}\n </Typography>\n\n {/* Past the early return the user always has `assets.update`, so the\n individual actions no longer re-check it. */}\n <ActionCluster>\n {isAiMetadataEnabled && (\n <Tooltip label={metadataDisabledReason}>\n {/* Wrapped so the tooltip still receives pointer events while the\n button itself is disabled. */}\n <Box>\n <Button\n size=\"S\"\n startIcon={<Sparkle />}\n disabled={\n isBusy || selectedIds.size === 0 || isOverMetadataLimit || hasNoEligibleAssets\n }\n loading={isGeneratingMetadata}\n onClick={handleCreateMetadata}\n >\n {formatMessage({\n id: getTranslationKey('list.bulk-actions.create-metadata'),\n defaultMessage: 'Create metadata',\n })}\n </Button>\n </Box>\n </Tooltip>\n )}\n\n <IconButton\n variant=\"tertiary\"\n disabled={isBusy}\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.move'),\n defaultMessage: 'Move',\n })}\n onClick={() => setIsMoveDialogOpen(true)}\n >\n <ArrowRight />\n </IconButton>\n <BulkMoveDialog open={isMoveDialogOpen} onClose={() => setIsMoveDialogOpen(false)} />\n\n <Dialog.Root\n open={isDeleteDialogOpen}\n onOpenChange={(open: boolean) => {\n // The dialog must stay open while the request runs — a bulk delete\n // can't be aborted halfway, so the loader is the source of truth.\n if (!isDeleting) {\n setIsDeleteDialogOpen(open);\n }\n }}\n >\n <Dialog.Trigger>\n <IconButton\n variant=\"danger-light\"\n disabled={isBusy}\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.delete'),\n defaultMessage: 'Delete',\n })}\n >\n <Trash />\n </IconButton>\n </Dialog.Trigger>\n <Dialog.Content>\n <Dialog.Header>\n {formatMessage(\n {\n id: getTranslationKey('list.bulk-actions.delete.confirm.title'),\n defaultMessage: 'Delete {count, plural, =1 {# item} other {# items}}?',\n },\n { count }\n )}\n </Dialog.Header>\n <Dialog.Body\n icon={<WarningCircle width=\"24px\" height=\"24px\" fill=\"danger600\" />}\n textAlign=\"center\"\n >\n <Typography>\n {formatMessage({\n id: getTranslationKey(\n 'list.bulk-actions.delete.confirm.description.are-you-sure'\n ),\n defaultMessage:\n 'These items cannot be recovered once deleted, and deleting a folder also deletes everything inside it. If they are currently in use, linked content will break and image containers will be empty.',\n })}\n </Typography>\n <Typography>\n {formatMessage({\n id: getTranslationKey(\n 'list.bulk-actions.delete.confirm.description.cant-be-undone'\n ),\n defaultMessage:\n 'This action can’t be undone. Deleting a folder also removes everything inside it, and any linked content will break – media asset containers will appear empty.',\n })}\n </Typography>\n </Dialog.Body>\n <Dialog.Footer>\n <Dialog.Cancel>\n <Button variant=\"tertiary\" disabled={isDeleting} fullWidth>\n {formatMessage({\n id: 'app.components.Button.cancel',\n defaultMessage: 'Cancel',\n })}\n </Button>\n </Dialog.Cancel>\n <Dialog.Action>\n <Button\n variant=\"danger-light\"\n loading={isDeleting}\n onClick={handleConfirmDelete}\n fullWidth\n >\n {formatMessage({\n id: 'app.components.Button.confirm',\n defaultMessage: 'Confirm',\n })}\n </Button>\n </Dialog.Action>\n </Dialog.Footer>\n </Dialog.Content>\n </Dialog.Root>\n </ActionCluster>\n\n <VerticalDivider aria-hidden />\n\n <IconButton\n variant=\"ghost\"\n label={formatMessage({\n id: getTranslationKey('list.bulk-actions.clear'),\n defaultMessage: 'Clear selection',\n })}\n onClick={clear}\n disabled={isBusy}\n >\n <Cross />\n </IconButton>\n </Bar>\n );\n};\n"],"names":["Bar","styled","Flex","theme","spaces","zIndices","popover","colors","neutral0","neutral150","borderRadius","shadows","popupShadow","ActionCluster","VerticalDivider","Box","BulkActionsBar","assets","formatMessage","useIntl","toggleNotification","useNotification","isEnabled","isAiMetadataEnabled","useAIAvailability","canUpdate","useMediaLibraryPermissions","selectedIds","selectedFolderIds","clear","useAssetSelection","bulkDeleteItems","isLoading","isDeleting","useBulkDeleteItemsMutation","generateAiMetadata","isGeneratingMetadata","useGenerateAiMetadataMutation","isDeleteDialogOpen","setIsDeleteDialogOpen","useState","isMoveDialogOpen","setIsMoveDialogOpen","count","size","isBusy","isOverMetadataLimit","AI_METADATA_MAX_FILES","eligibleCount","useMemo","mimeById","Map","map","id","mime","filter","isAIMetadataSupportedMime","get","length","hasNoEligibleAssets","metadataDisabledReason","getTranslationKey","defaultMessage","max","handleCreateMetadata","fileIds","Array","from","res","type","message","successCount","data","status","skippedCount","errorCount","folderCount","handleConfirmDelete","e","preventDefault","folderIds","_jsxs","tag","role","aria-label","_jsx","Typography","fontWeight","textColor","marginRight","Tooltip","label","Button","startIcon","Sparkle","disabled","loading","onClick","IconButton","variant","ArrowRight","BulkMoveDialog","open","onClose","Dialog","Root","onOpenChange","Trigger","Trash","Content","Header","Body","icon","WarningCircle","width","height","fill","textAlign","Footer","Cancel","fullWidth","Action","aria-hidden","Cross"],"mappings":"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"}
@@ -1,20 +1,21 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { useState } from 'react';
2
+ import { useState, useMemo } from 'react';
3
3
  import { useNotification } from '@strapi/admin/strapi-admin';
4
- import { Typography, Button, IconButton, Dialog, Flex, Box } from '@strapi/design-system';
4
+ import { Typography, Tooltip, Box, Button, IconButton, Dialog, Flex } from '@strapi/design-system';
5
5
  import { Sparkle, ArrowRight, Trash, WarningCircle, Cross } from '@strapi/icons';
6
6
  import { useIntl } from 'react-intl';
7
7
  import { styled } from 'styled-components';
8
+ import { AI_METADATA_MAX_FILES, isAIMetadataSupportedMime } from '../../../../shared/constants.mjs';
8
9
  import { useAIAvailability } from '../../../../hooks/useAiAvailability.mjs';
10
+ import { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions.mjs';
9
11
  import { useBulkDeleteItemsMutation } from '../../../services/assets.mjs';
10
12
  import { getTranslationKey } from '../../../utils/translations.mjs';
11
13
  import { useAssetSelection } from '../hooks/useAssetSelection.mjs';
12
14
  import { BulkMoveDialog } from './BulkMoveDialog.mjs';
15
+ import { useGenerateAiMetadataMutation } from '../../../services/api.mjs';
13
16
 
14
17
  /**
15
18
  * Floating bulk action bar for the future Media Library.
16
- *
17
- * TODO: create-metadata control is a styled stub (toast on click)
18
19
  */ const Bar = styled(Flex)`
19
20
  position: fixed;
20
21
  bottom: ${({ theme })=>theme.spaces[4]};
@@ -40,23 +41,130 @@ const VerticalDivider = styled(Box)`
40
41
  background: ${({ theme })=>theme.colors.neutral150};
41
42
  margin-left: ${({ theme })=>theme.spaces[1]};
42
43
  `;
43
- const BulkActionsBar = ()=>{
44
+ const BulkActionsBar = ({ assets = [] })=>{
44
45
  const { formatMessage } = useIntl();
45
46
  const { toggleNotification } = useNotification();
46
47
  const { isEnabled: isAiMetadataEnabled } = useAIAvailability();
48
+ // Every bulk action (move, delete, metadata) is an `assets.update` mutation
49
+ // server-side — one flag gates the whole cluster.
50
+ const { canUpdate } = useMediaLibraryPermissions();
47
51
  const { selectedIds, selectedFolderIds, clear } = useAssetSelection();
48
52
  const [bulkDeleteItems, { isLoading: isDeleting }] = useBulkDeleteItemsMutation();
53
+ const [generateAiMetadata, { isLoading: isGeneratingMetadata }] = useGenerateAiMetadataMutation();
49
54
  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
50
55
  const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
51
56
  const count = selectedIds.size + selectedFolderIds.size;
52
- const showStubNotification = (translationKey, defaultMessage)=>{
53
- toggleNotification({
54
- type: 'info',
55
- message: formatMessage({
56
- id: getTranslationKey(translationKey),
57
- defaultMessage
58
- })
57
+ const isBusy = isDeleting || isGeneratingMetadata;
58
+ /**
59
+ * The server processes the whole selection inside a single request and
60
+ * rejects anything above the cap with a 400. Disable the action up front so
61
+ * the limit is discoverable before the user commits to a request.
62
+ */ const isOverMetadataLimit = selectedIds.size > AI_METADATA_MAX_FILES;
63
+ /**
64
+ * How many selected assets the AI provider can actually read. The server
65
+ * stays authoritative — it still reports what it skipped — but a selection
66
+ * with nothing eligible (a lone PDF, say) would only ever come back fully
67
+ * skipped, so there is no point sending it.
68
+ */ const eligibleCount = useMemo(()=>{
69
+ const mimeById = new Map(assets.map(({ id, mime })=>[
70
+ id,
71
+ mime
72
+ ]));
73
+ return [
74
+ ...selectedIds
75
+ ].filter((id)=>isAIMetadataSupportedMime(mimeById.get(id))).length;
76
+ }, [
77
+ assets,
78
+ selectedIds
79
+ ]);
80
+ const hasNoEligibleAssets = selectedIds.size > 0 && eligibleCount === 0;
81
+ /**
82
+ * Why the action is unavailable, surfaced on hover. `undefined` leaves the
83
+ * button without a tooltip — either it is usable, or it is disabled for a
84
+ * reason the selection count already makes obvious.
85
+ */ let metadataDisabledReason;
86
+ if (isOverMetadataLimit) {
87
+ metadataDisabledReason = formatMessage({
88
+ id: getTranslationKey('list.bulk-actions.create-metadata.too-many'),
89
+ defaultMessage: 'Metadata can be generated for up to {max} assets at a time. Select fewer assets to continue.'
90
+ }, {
91
+ max: AI_METADATA_MAX_FILES
92
+ });
93
+ } else if (hasNoEligibleAssets) {
94
+ metadataDisabledReason = formatMessage({
95
+ id: getTranslationKey('list.bulk-actions.create-metadata.no-eligible'),
96
+ defaultMessage: 'Metadata can only be generated for images. None of the selected assets are supported.'
97
+ });
98
+ }
99
+ /**
100
+ * Metadata generation only applies to images — folders and unsupported files
101
+ * in the selection are reported back rather than blocking the action.
102
+ */ const handleCreateMetadata = async ()=>{
103
+ // Guard re-entry while pending, and never send a selection the server
104
+ // would reject outright or skip in full.
105
+ if (isGeneratingMetadata || isOverMetadataLimit || hasNoEligibleAssets) {
106
+ return;
107
+ }
108
+ const fileIds = Array.from(selectedIds);
109
+ const res = await generateAiMetadata({
110
+ fileIds
59
111
  });
112
+ if ('error' in res) {
113
+ // Keep the selection intact so the user can retry.
114
+ toggleNotification({
115
+ type: 'danger',
116
+ message: formatMessage({
117
+ id: getTranslationKey('list.bulk-actions.create-metadata.error'),
118
+ defaultMessage: 'An error occurred while generating metadata.'
119
+ })
120
+ });
121
+ return;
122
+ }
123
+ const successCount = res.data.filter(({ status })=>status === 'success').length;
124
+ const skippedCount = res.data.filter(({ status })=>status === 'skipped').length;
125
+ const errorCount = res.data.filter(({ status })=>status === 'error').length;
126
+ // Folders are never sent, so they are not part of the response — but the
127
+ // user selected them, and a silent omission reads as a bug.
128
+ const folderCount = selectedFolderIds.size;
129
+ if (errorCount === res.data.length) {
130
+ // Nothing was written — treat it like a request-level failure and keep
131
+ // the selection so the user can retry.
132
+ toggleNotification({
133
+ type: 'danger',
134
+ message: formatMessage({
135
+ id: getTranslationKey('list.bulk-actions.create-metadata.error'),
136
+ defaultMessage: 'An error occurred while generating metadata.'
137
+ })
138
+ });
139
+ return;
140
+ }
141
+ if (skippedCount === 0 && errorCount === 0 && folderCount === 0) {
142
+ toggleNotification({
143
+ type: 'success',
144
+ message: formatMessage({
145
+ id: getTranslationKey('list.bulk-actions.create-metadata.success'),
146
+ defaultMessage: '{count, plural, =1 {Metadata generated for # asset} other {Metadata generated for # assets}}'
147
+ }, {
148
+ count: successCount
149
+ })
150
+ });
151
+ } else {
152
+ // Partial outcome: report every bucket so the user knows what was left
153
+ // out — including folders, which were never eligible in the first place.
154
+ toggleNotification({
155
+ type: 'warning',
156
+ message: formatMessage({
157
+ id: getTranslationKey('list.bulk-actions.create-metadata.partial'),
158
+ defaultMessage: '{successCount} generated, {skippedCount} skipped (unsupported file type), {errorCount} failed{folderCount, plural, =0 {} one {, # folder ignored} other {, # folders ignored}}'
159
+ }, {
160
+ successCount,
161
+ skippedCount,
162
+ errorCount,
163
+ folderCount
164
+ })
165
+ });
166
+ }
167
+ clear();
60
168
  };
61
169
  const handleConfirmDelete = async (e)=>{
62
170
  // Radix AlertDialog.Action closes the dialog on click by default; prevent
@@ -95,7 +203,11 @@ const BulkActionsBar = ()=>{
95
203
  });
96
204
  clear();
97
205
  };
98
- if (count === 0) {
206
+ // Every bulk action is behind `assets.update`; with nothing selected, or
207
+ // without the permission, the bar has nothing to offer — drop it entirely
208
+ // rather than show a count + "Clear selection" over no actions (mirrors the
209
+ // drawer footer, which hides when no permitted action survives).
210
+ if (count === 0 || !canUpdate) {
99
211
  return null;
100
212
  }
101
213
  return /*#__PURE__*/ jsxs(Bar, {
@@ -119,19 +231,25 @@ const BulkActionsBar = ()=>{
119
231
  }),
120
232
  /*#__PURE__*/ jsxs(ActionCluster, {
121
233
  children: [
122
- isAiMetadataEnabled && /*#__PURE__*/ jsx(Button, {
123
- size: "S",
124
- startIcon: /*#__PURE__*/ jsx(Sparkle, {}),
125
- disabled: isDeleting,
126
- onClick: ()=>showStubNotification('list.bulk-actions.create-metadata-not-available', "Generate metadata isn't available yet"),
127
- children: formatMessage({
128
- id: getTranslationKey('list.bulk-actions.create-metadata'),
129
- defaultMessage: 'Create metadata'
234
+ isAiMetadataEnabled && /*#__PURE__*/ jsx(Tooltip, {
235
+ label: metadataDisabledReason,
236
+ children: /*#__PURE__*/ jsx(Box, {
237
+ children: /*#__PURE__*/ jsx(Button, {
238
+ size: "S",
239
+ startIcon: /*#__PURE__*/ jsx(Sparkle, {}),
240
+ disabled: isBusy || selectedIds.size === 0 || isOverMetadataLimit || hasNoEligibleAssets,
241
+ loading: isGeneratingMetadata,
242
+ onClick: handleCreateMetadata,
243
+ children: formatMessage({
244
+ id: getTranslationKey('list.bulk-actions.create-metadata'),
245
+ defaultMessage: 'Create metadata'
246
+ })
247
+ })
130
248
  })
131
249
  }),
132
250
  /*#__PURE__*/ jsx(IconButton, {
133
251
  variant: "tertiary",
134
- disabled: isDeleting,
252
+ disabled: isBusy,
135
253
  label: formatMessage({
136
254
  id: getTranslationKey('list.bulk-actions.move'),
137
255
  defaultMessage: 'Move'
@@ -156,7 +274,7 @@ const BulkActionsBar = ()=>{
156
274
  /*#__PURE__*/ jsx(Dialog.Trigger, {
157
275
  children: /*#__PURE__*/ jsx(IconButton, {
158
276
  variant: "danger-light",
159
- disabled: isDeleting,
277
+ disabled: isBusy,
160
278
  label: formatMessage({
161
279
  id: getTranslationKey('list.bulk-actions.delete'),
162
280
  defaultMessage: 'Delete'
@@ -239,7 +357,7 @@ const BulkActionsBar = ()=>{
239
357
  defaultMessage: 'Clear selection'
240
358
  }),
241
359
  onClick: clear,
242
- disabled: isDeleting,
360
+ disabled: isBusy,
243
361
  children: /*#__PURE__*/ jsx(Cross, {})
244
362
  })
245
363
  ]