@strapi/upload 5.47.1 → 5.48.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 (101) hide show
  1. package/dist/admin/components/EditAssetDialog/EditAssetContent.js +12 -2
  2. package/dist/admin/components/EditAssetDialog/EditAssetContent.js.map +1 -1
  3. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs +12 -2
  4. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
  5. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.js +1 -0
  6. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.js.map +1 -1
  7. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.mjs +1 -0
  8. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.mjs.map +1 -1
  9. package/dist/admin/future/components/Drawer.js +7 -8
  10. package/dist/admin/future/components/Drawer.js.map +1 -1
  11. package/dist/admin/future/components/Drawer.mjs +7 -8
  12. package/dist/admin/future/components/Drawer.mjs.map +1 -1
  13. package/dist/admin/future/components/UploadProgressDialog.js +33 -29
  14. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
  15. package/dist/admin/future/components/UploadProgressDialog.mjs +36 -32
  16. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
  17. package/dist/admin/future/pages/Assets/AssetsPage.js +2 -2
  18. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  19. package/dist/admin/future/pages/Assets/AssetsPage.mjs +3 -3
  20. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  21. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +626 -169
  22. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
  23. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +630 -175
  24. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  25. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js +25 -5
  26. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js.map +1 -1
  27. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs +25 -5
  28. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs.map +1 -1
  29. package/dist/admin/future/services/api.js +124 -200
  30. package/dist/admin/future/services/api.js.map +1 -1
  31. package/dist/admin/future/services/api.mjs +124 -200
  32. package/dist/admin/future/services/api.mjs.map +1 -1
  33. package/dist/admin/future/services/assets.js +57 -1
  34. package/dist/admin/future/services/assets.js.map +1 -1
  35. package/dist/admin/future/services/assets.mjs +56 -2
  36. package/dist/admin/future/services/assets.mjs.map +1 -1
  37. package/dist/admin/future/services/settings.js +18 -0
  38. package/dist/admin/future/services/settings.js.map +1 -0
  39. package/dist/admin/future/services/settings.mjs +16 -0
  40. package/dist/admin/future/services/settings.mjs.map +1 -0
  41. package/dist/admin/future/services/uploadFileViaXHR.js +92 -0
  42. package/dist/admin/future/services/uploadFileViaXHR.js.map +1 -0
  43. package/dist/admin/future/services/uploadFileViaXHR.mjs +88 -0
  44. package/dist/admin/future/services/uploadFileViaXHR.mjs.map +1 -0
  45. package/dist/admin/future/store/uploadProgress.js +32 -26
  46. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  47. package/dist/admin/future/store/uploadProgress.mjs +32 -27
  48. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  49. package/dist/admin/future/utils/createRafBatcher.js +42 -0
  50. package/dist/admin/future/utils/createRafBatcher.js.map +1 -0
  51. package/dist/admin/future/utils/createRafBatcher.mjs +40 -0
  52. package/dist/admin/future/utils/createRafBatcher.mjs.map +1 -0
  53. package/dist/admin/future/utils/downloadFile.js +19 -0
  54. package/dist/admin/future/utils/downloadFile.js.map +1 -0
  55. package/dist/admin/future/utils/downloadFile.mjs +17 -0
  56. package/dist/admin/future/utils/downloadFile.mjs.map +1 -0
  57. package/dist/admin/hooks/useAssets.js +5 -3
  58. package/dist/admin/hooks/useAssets.js.map +1 -1
  59. package/dist/admin/hooks/useAssets.mjs +5 -3
  60. package/dist/admin/hooks/useAssets.mjs.map +1 -1
  61. package/dist/admin/src/components/EditAssetDialog/EditAssetContent.d.ts +2 -1
  62. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.d.ts +15 -1
  63. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetPreview.d.ts +4 -1
  64. package/dist/admin/src/future/services/api.d.ts +9 -8
  65. package/dist/admin/src/future/services/assets.d.ts +6 -1
  66. package/dist/admin/src/future/services/uploadFileViaXHR.d.ts +34 -0
  67. package/dist/admin/src/future/store/uploadProgress.d.ts +17 -4
  68. package/dist/admin/src/future/utils/createRafBatcher.d.ts +23 -0
  69. package/dist/admin/src/future/utils/downloadFile.d.ts +6 -0
  70. package/dist/admin/translations/en.json.js +21 -0
  71. package/dist/admin/translations/en.json.js.map +1 -1
  72. package/dist/admin/translations/en.json.mjs +21 -0
  73. package/dist/admin/translations/en.json.mjs.map +1 -1
  74. package/dist/server/controllers/admin-upload.js +69 -118
  75. package/dist/server/controllers/admin-upload.js.map +1 -1
  76. package/dist/server/controllers/admin-upload.mjs +69 -118
  77. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  78. package/dist/server/routes/admin.js +2 -2
  79. package/dist/server/routes/admin.js.map +1 -1
  80. package/dist/server/routes/admin.mjs +2 -2
  81. package/dist/server/routes/admin.mjs.map +1 -1
  82. package/dist/server/services/image-manipulation.js +16 -8
  83. package/dist/server/services/image-manipulation.js.map +1 -1
  84. package/dist/server/services/image-manipulation.mjs +16 -8
  85. package/dist/server/services/image-manipulation.mjs.map +1 -1
  86. package/dist/server/services/upload.js +1 -1
  87. package/dist/server/services/upload.js.map +1 -1
  88. package/dist/server/services/upload.mjs +1 -1
  89. package/dist/server/services/upload.mjs.map +1 -1
  90. package/dist/server/src/controllers/admin-upload.d.ts +6 -8
  91. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  92. package/dist/server/src/controllers/index.d.ts +1 -1
  93. package/dist/server/src/index.d.ts +1 -1
  94. package/dist/server/src/services/image-manipulation.d.ts +5 -0
  95. package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
  96. package/dist/server/src/services/upload.d.ts.map +1 -1
  97. package/dist/server/src/types.d.ts +2 -2
  98. package/dist/server/src/types.d.ts.map +1 -1
  99. package/dist/shared/contracts/files.d.ts +19 -2
  100. package/dist/shared/contracts/files.d.ts.map +1 -1
  101. package/package.json +7 -7
@@ -1,21 +1,40 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import { useQueryParams, useNotification, Form, Blocker, getDisplayName, useField, useForm } from '@strapi/admin/strapi-admin';
4
- import { VisuallyHidden, Flex, Loader, Alert, Typography, Box, Button, Field, TextInput, Tooltip, SingleSelect, SingleSelectOption } from '@strapi/design-system';
5
- import { FileError, ArrowLineRight, WarningCircle } from '@strapi/icons';
3
+ import { useQueryParams, useNotification, Form, Blocker, getDisplayName, useField, useForm, useClipboard } from '@strapi/admin/strapi-admin';
4
+ import { VisuallyHidden, Flex, Loader, Alert, Typography, Box, Button, IconButton, Dialog, Field, TextInput, Tooltip, SingleSelect, SingleSelectOption } from '@strapi/design-system';
5
+ import { FileError, ArrowLineRight, ArrowsCounterClockwise, Trash, WarningCircle, Link, Download } from '@strapi/icons';
6
6
  import { useIntl } from 'react-intl';
7
7
  import { styled } from 'styled-components';
8
8
  import { Drawer } from '../../../../components/Drawer.mjs';
9
9
  import { AssetType } from '../../../../enums.mjs';
10
- import { useGetAssetQuery, useUpdateAssetMutation } from '../../../../services/assets.mjs';
10
+ import { useGetAssetQuery, useUpdateAssetMutation, useReplaceAssetMutation, useDeleteAssetMutation } from '../../../../services/assets.mjs';
11
11
  import { useGetAllFoldersQuery } from '../../../../services/folders.mjs';
12
- import { formatBytes, getFileExtension } from '../../../../utils/files.mjs';
12
+ import { useGetSettingsQuery } from '../../../../services/settings.mjs';
13
+ import { downloadFile } from '../../../../utils/downloadFile.mjs';
14
+ import { formatBytes, getFileExtension, prefixFileUrlWithBackendUrl } from '../../../../utils/files.mjs';
13
15
  import { getAssetIcon } from '../../../../utils/getAssetIcon.mjs';
14
16
  import { getTranslationKey } from '../../../../utils/translations.mjs';
17
+ import { useFolderInfo } from '../../hooks/useFolderInfo.mjs';
15
18
  import { AssetPreview } from './AssetPreview.mjs';
16
19
 
17
20
  // Name of the parameter to look for in the URL to open the drawer
18
21
  const URL_PARAM = 'assetId';
22
+ const DrawerNotifyContext = /*#__PURE__*/ React.createContext(null);
23
+ const useDrawerNotify = ()=>{
24
+ const ctx = React.useContext(DrawerNotifyContext);
25
+ if (!ctx) {
26
+ throw new Error('useDrawerNotify must be used within AssetDetails');
27
+ }
28
+ return ctx;
29
+ };
30
+ const AssetOperationsContext = /*#__PURE__*/ React.createContext(null);
31
+ const useAssetOperation = ()=>{
32
+ const ctx = React.useContext(AssetOperationsContext);
33
+ if (!ctx) {
34
+ throw new Error('useAssetOperation must be used within AssetDetails');
35
+ }
36
+ return ctx;
37
+ };
19
38
  /* -------------------------------------------------------------------------------------------------
20
39
  * useAssetDetailsParam - sync drawer visibility with URL ?{URL_PARAM}={id}
21
40
  * -----------------------------------------------------------------------------------------------*/ const useAssetDetailsParam = ()=>{
@@ -95,7 +114,50 @@ const DetailItem = ({ label, value })=>/*#__PURE__*/ jsxs(DetailItemContainer, {
95
114
  });
96
115
  /* -------------------------------------------------------------------------------------------------
97
116
  * DetailField
98
- * -----------------------------------------------------------------------------------------------*/ const StyledWarning = styled(WarningCircle)`
117
+ * -----------------------------------------------------------------------------------------------*/ /**
118
+ * Make the asset details Form behave as a flex column inside Drawer.Body so
119
+ * the scrollable area can grow while the footer stays pinned at the bottom.
120
+ * The Form component from `@strapi/admin/strapi-admin` only forwards `width`
121
+ * + `height` to its Box, so we target the rendered `<form>` element via a
122
+ * styled-components descendant rule.
123
+ */ const FormShell = styled(Box)`
124
+ display: flex;
125
+ flex-direction: column;
126
+ flex: 1;
127
+ min-height: 0;
128
+
129
+ > form {
130
+ display: flex;
131
+ flex-direction: column;
132
+ flex: 1;
133
+ min-height: 0;
134
+ position: relative;
135
+ }
136
+ `;
137
+ /**
138
+ * In-drawer toast container
139
+ */ const DrawerToastSlot = styled(Box)`
140
+ position: absolute;
141
+ top: ${({ theme })=>theme.spaces[2]};
142
+ left: 50%;
143
+ transform: translateX(-50%);
144
+ z-index: 10;
145
+ width: calc(100% - ${({ theme })=>theme.spaces[2]});
146
+ `;
147
+ /**
148
+ * Full-form overlay rendered during long-running drawer-scoped mutations
149
+ * (e.g. replacing the binary). Sits above the toast slot (z-index 10) and
150
+ * the in-drawer Alert so the user can't interact with the form mid-flight.
151
+ */ const DrawerBusyOverlay = styled(Flex)`
152
+ position: absolute;
153
+ inset: 0;
154
+ z-index: 20;
155
+ align-items: center;
156
+ justify-content: center;
157
+ background: ${({ theme })=>theme.colors.neutral0};
158
+ opacity: 0.7;
159
+ `;
160
+ const StyledWarning = styled(WarningCircle)`
99
161
  width: 1.6rem;
100
162
  height: 1.6rem;
101
163
 
@@ -168,11 +230,283 @@ const LocationField = ({ label, rootLabel, folders })=>{
168
230
  ]
169
231
  });
170
232
  };
171
- const AssetDetails = ({ asset })=>{
233
+ /* -------------------------------------------------------------------------------------------------
234
+ * DeleteAssetButton
235
+ * -----------------------------------------------------------------------------------------------*/ const DeleteAssetButton = ()=>{
236
+ const { formatMessage } = useIntl();
237
+ const { deleteAsset, isDeleting } = useAssetOperation();
238
+ const [isOpen, setIsOpen] = React.useState(false);
239
+ const handleConfirm = async ()=>{
240
+ await deleteAsset();
241
+ setIsOpen(false);
242
+ };
243
+ const triggerLabel = formatMessage({
244
+ id: getTranslationKey('asset-details.delete.trigger'),
245
+ defaultMessage: 'Delete this file'
246
+ });
247
+ return /*#__PURE__*/ jsxs(Dialog.Root, {
248
+ open: isOpen,
249
+ onOpenChange: setIsOpen,
250
+ children: [
251
+ /*#__PURE__*/ jsx(Dialog.Trigger, {
252
+ children: /*#__PURE__*/ jsx(IconButton, {
253
+ withTooltip: false,
254
+ label: triggerLabel,
255
+ variant: "danger-light",
256
+ children: /*#__PURE__*/ jsx(Trash, {})
257
+ })
258
+ }),
259
+ /*#__PURE__*/ jsxs(Dialog.Content, {
260
+ children: [
261
+ /*#__PURE__*/ jsx(Dialog.Header, {
262
+ children: formatMessage({
263
+ id: getTranslationKey('asset-details.delete.title'),
264
+ defaultMessage: 'Delete this media file?'
265
+ })
266
+ }),
267
+ /*#__PURE__*/ jsx(Dialog.Body, {
268
+ icon: /*#__PURE__*/ jsx(WarningCircle, {
269
+ width: "24px",
270
+ height: "24px",
271
+ fill: "danger600"
272
+ }),
273
+ textAlign: "center",
274
+ children: formatMessage({
275
+ id: getTranslationKey('asset-details.delete.description'),
276
+ defaultMessage: 'This file cannot be recovered once deleted. If it is currently in use, linked content will break and image containers will be empty.'
277
+ })
278
+ }),
279
+ /*#__PURE__*/ jsxs(Dialog.Footer, {
280
+ children: [
281
+ /*#__PURE__*/ jsx(Dialog.Cancel, {
282
+ children: /*#__PURE__*/ jsx(Button, {
283
+ variant: "tertiary",
284
+ disabled: isDeleting,
285
+ fullWidth: true,
286
+ children: formatMessage({
287
+ id: 'app.components.Button.cancel',
288
+ defaultMessage: 'Cancel'
289
+ })
290
+ })
291
+ }),
292
+ /*#__PURE__*/ jsx(Dialog.Action, {
293
+ children: /*#__PURE__*/ jsx(Button, {
294
+ variant: "danger-light",
295
+ loading: isDeleting,
296
+ onClick: handleConfirm,
297
+ fullWidth: true,
298
+ children: formatMessage({
299
+ id: 'app.components.Button.confirm',
300
+ defaultMessage: 'Confirm'
301
+ })
302
+ })
303
+ })
304
+ ]
305
+ })
306
+ ]
307
+ })
308
+ ]
309
+ });
310
+ };
311
+ const CopyLinkButton = ({ asset })=>{
312
+ const { formatMessage } = useIntl();
313
+ const { copy } = useClipboard();
314
+ const notify = useDrawerNotify();
315
+ const handleCopy = async ()=>{
316
+ const url = prefixFileUrlWithBackendUrl(asset.url);
317
+ if (!url) return;
318
+ const didCopy = await copy(url);
319
+ notify({
320
+ type: didCopy ? 'success' : 'danger',
321
+ message: didCopy ? formatMessage({
322
+ id: getTranslationKey('asset-details.copy-link.success'),
323
+ defaultMessage: 'Link copied.'
324
+ }) : formatMessage({
325
+ id: getTranslationKey('asset-details.copy-link.error'),
326
+ defaultMessage: 'Failed to copy the link.'
327
+ })
328
+ });
329
+ };
330
+ return /*#__PURE__*/ jsx(IconButton, {
331
+ withTooltip: false,
332
+ label: formatMessage({
333
+ id: getTranslationKey('asset-details.copy-link.trigger'),
334
+ defaultMessage: 'Copy link'
335
+ }),
336
+ variant: "tertiary",
337
+ onClick: handleCopy,
338
+ children: /*#__PURE__*/ jsx(Link, {})
339
+ });
340
+ };
341
+ const DownloadAssetButton = ({ asset })=>{
342
+ const { formatMessage } = useIntl();
343
+ const notify = useDrawerNotify();
344
+ const [isDownloading, setIsDownloading] = React.useState(false);
345
+ const handleDownload = async ()=>{
346
+ const url = prefixFileUrlWithBackendUrl(asset.url);
347
+ if (!url) return;
348
+ setIsDownloading(true);
349
+ try {
350
+ await downloadFile(url, asset.name);
351
+ } catch {
352
+ notify({
353
+ type: 'danger',
354
+ message: formatMessage({
355
+ id: getTranslationKey('asset-details.download.error'),
356
+ defaultMessage: 'Failed to download the file.'
357
+ })
358
+ });
359
+ } finally{
360
+ setIsDownloading(false);
361
+ }
362
+ };
363
+ return /*#__PURE__*/ jsx(IconButton, {
364
+ withTooltip: false,
365
+ label: formatMessage({
366
+ id: getTranslationKey('asset-details.download.trigger'),
367
+ defaultMessage: 'Download'
368
+ }),
369
+ variant: "tertiary",
370
+ onClick: handleDownload,
371
+ disabled: isDownloading,
372
+ children: /*#__PURE__*/ jsx(Download, {})
373
+ });
374
+ };
375
+ /* -------------------------------------------------------------------------------------------------
376
+ * ReplaceAssetButton
377
+ * -----------------------------------------------------------------------------------------------*/ const ReplaceAssetButton = ()=>{
378
+ const { formatMessage } = useIntl();
379
+ const { replaceAsset, isReplacing } = useAssetOperation();
380
+ const fileInputRef = React.useRef(null);
381
+ const [isDialogOpen, setIsDialogOpen] = React.useState(false);
382
+ const { data: settings } = useGetSettingsQuery();
383
+ const aiEnabled = settings?.data?.aiMetadata ?? false;
384
+ const handleTriggerClick = ()=>{
385
+ setIsDialogOpen(true);
386
+ };
387
+ const handleContinue = ()=>{
388
+ // Confirm first, then open the native picker so the user only commits to
389
+ // replacing after acknowledging the warning. The actual POST is delegated
390
+ // to the parent (which owns the mutation + loading state).
391
+ setIsDialogOpen(false);
392
+ fileInputRef.current?.click();
393
+ };
394
+ const handleFileChange = async (event)=>{
395
+ const file = event.target.files?.[0];
396
+ // Reset the native input so the same file can be picked again later.
397
+ event.target.value = '';
398
+ if (!file) {
399
+ return;
400
+ }
401
+ await replaceAsset(file);
402
+ };
403
+ return /*#__PURE__*/ jsxs(Fragment, {
404
+ children: [
405
+ /*#__PURE__*/ jsx(VisuallyHidden, {
406
+ children: /*#__PURE__*/ jsx("input", {
407
+ ref: fileInputRef,
408
+ type: "file",
409
+ multiple: false,
410
+ onChange: handleFileChange,
411
+ "aria-hidden": true,
412
+ tabIndex: -1
413
+ })
414
+ }),
415
+ /*#__PURE__*/ jsx(IconButton, {
416
+ withTooltip: false,
417
+ label: formatMessage({
418
+ id: getTranslationKey('asset-details.replace.trigger'),
419
+ defaultMessage: 'Replace this file'
420
+ }),
421
+ variant: "tertiary",
422
+ onClick: handleTriggerClick,
423
+ disabled: isReplacing,
424
+ children: /*#__PURE__*/ jsx(ArrowsCounterClockwise, {})
425
+ }),
426
+ /*#__PURE__*/ jsx(Dialog.Root, {
427
+ open: isDialogOpen,
428
+ onOpenChange: setIsDialogOpen,
429
+ children: /*#__PURE__*/ jsxs(Dialog.Content, {
430
+ children: [
431
+ /*#__PURE__*/ jsx(Dialog.Header, {
432
+ children: formatMessage({
433
+ id: getTranslationKey('asset-details.replace.title'),
434
+ defaultMessage: 'Replace this media file?'
435
+ })
436
+ }),
437
+ /*#__PURE__*/ jsx(Dialog.Body, {
438
+ textAlign: "center",
439
+ children: /*#__PURE__*/ jsxs(Flex, {
440
+ direction: "column",
441
+ textAlign: "center",
442
+ children: [
443
+ /*#__PURE__*/ jsx(Typography, {
444
+ variant: "omega",
445
+ children: formatMessage({
446
+ id: getTranslationKey('asset-details.replace.description'),
447
+ defaultMessage: 'Current content will be permanently replaced.'
448
+ })
449
+ }),
450
+ aiEnabled ? /*#__PURE__*/ jsx(Typography, {
451
+ variant: "omega",
452
+ children: formatMessage({
453
+ id: getTranslationKey('asset-details.replace.description.ai'),
454
+ defaultMessage: 'AI will generate new metadata after upload.'
455
+ })
456
+ }) : null
457
+ ]
458
+ })
459
+ }),
460
+ /*#__PURE__*/ jsxs(Dialog.Footer, {
461
+ children: [
462
+ /*#__PURE__*/ jsx(Dialog.Cancel, {
463
+ children: /*#__PURE__*/ jsx(Button, {
464
+ variant: "tertiary",
465
+ fullWidth: true,
466
+ children: formatMessage({
467
+ id: 'app.components.Button.cancel',
468
+ defaultMessage: 'Cancel'
469
+ })
470
+ })
471
+ }),
472
+ /*#__PURE__*/ jsx(Dialog.Action, {
473
+ children: /*#__PURE__*/ jsx(Button, {
474
+ variant: "secondary",
475
+ onClick: handleContinue,
476
+ fullWidth: true,
477
+ children: formatMessage({
478
+ id: getTranslationKey('asset-details.replace.continue'),
479
+ defaultMessage: 'Continue'
480
+ })
481
+ })
482
+ })
483
+ ]
484
+ })
485
+ ]
486
+ })
487
+ })
488
+ ]
489
+ });
490
+ };
491
+ const AssetDetails = ({ asset, closeDetails })=>{
172
492
  const { formatMessage, formatDate } = useIntl();
173
- const { toggleNotification } = useNotification();
174
493
  const { data: folders = [] } = useGetAllFoldersQuery();
494
+ const { toggleNotification } = useNotification();
175
495
  const [updateAsset] = useUpdateAssetMutation();
496
+ const [replaceMutation, { isLoading: isReplacing }] = useReplaceAssetMutation();
497
+ const [deleteMutation, { isLoading: isDeleting }] = useDeleteAssetMutation();
498
+ // In-drawer toast slot
499
+ const [drawerToast, setDrawerToast] = React.useState(null);
500
+ React.useEffect(()=>{
501
+ if (!drawerToast) return;
502
+ const timer = window.setTimeout(()=>setDrawerToast(null), 5000);
503
+ return ()=>window.clearTimeout(timer);
504
+ }, [
505
+ drawerToast
506
+ ]);
507
+ // Local alias matching the DrawerNotifyContext signature, so the drawer's
508
+ // own handlers (replace, update) read like the consumers do.
509
+ const notify = React.useCallback((toast)=>setDrawerToast(toast), []);
176
510
  const isImage = asset.mime?.includes(AssetType.Image);
177
511
  const initialValues = {
178
512
  name: asset.name ?? '',
@@ -191,7 +525,7 @@ const AssetDetails = ({ asset })=>{
191
525
  }
192
526
  });
193
527
  if ('error' in res) {
194
- toggleNotification({
528
+ notify({
195
529
  type: 'danger',
196
530
  message: formatMessage({
197
531
  id: getTranslationKey('asset-details.update.error'),
@@ -200,7 +534,7 @@ const AssetDetails = ({ asset })=>{
200
534
  });
201
535
  return;
202
536
  }
203
- toggleNotification({
537
+ notify({
204
538
  type: 'success',
205
539
  message: formatMessage({
206
540
  id: getTranslationKey('asset-details.update.success'),
@@ -208,168 +542,292 @@ const AssetDetails = ({ asset })=>{
208
542
  })
209
543
  });
210
544
  };
545
+ const { title: folderName } = useFolderInfo(typeof asset.folder === 'object' && asset.folder !== null ? asset.folder.id ?? null : asset.folder ?? null);
546
+ // Owns the replace upload so isReplacing can drive the busy overlay.
547
+ const handleReplace = async (file)=>{
548
+ const res = await replaceMutation({
549
+ id: asset.id,
550
+ file
551
+ });
552
+ if ('error' in res) {
553
+ const error = res.error;
554
+ const message = error?.data?.error?.message ?? error?.data?.message ?? formatMessage({
555
+ id: getTranslationKey('asset-details.replace.error'),
556
+ defaultMessage: 'Failed to replace the file.'
557
+ });
558
+ notify({
559
+ type: 'danger',
560
+ message
561
+ });
562
+ return;
563
+ }
564
+ notify({
565
+ type: 'success',
566
+ message: formatMessage({
567
+ id: getTranslationKey('asset-details.replace.success'),
568
+ defaultMessage: 'File replaced.'
569
+ })
570
+ });
571
+ };
572
+ // Owns the delete: on error notify in-drawer (drawer stays), on success fire
573
+ // a persistent global notification then close the drawer.
574
+ const handleDelete = async ()=>{
575
+ const res = await deleteMutation(asset.id);
576
+ if ('error' in res) {
577
+ const error = res.error;
578
+ const message = error?.data?.error?.message ?? error?.data?.message ?? formatMessage({
579
+ id: getTranslationKey('asset-details.delete.error'),
580
+ defaultMessage: 'Failed to delete the asset.'
581
+ });
582
+ notify({
583
+ type: 'danger',
584
+ message
585
+ });
586
+ return;
587
+ }
588
+ toggleNotification({
589
+ type: 'success',
590
+ message: formatMessage({
591
+ id: getTranslationKey('asset-details.delete.success'),
592
+ defaultMessage: '1 element have been deleted from {folderName}'
593
+ }, {
594
+ folderName
595
+ })
596
+ });
597
+ closeDetails();
598
+ };
599
+ const operations = React.useMemo(()=>({
600
+ replaceAsset: handleReplace,
601
+ deleteAsset: handleDelete,
602
+ isReplacing,
603
+ isDeleting
604
+ }), // handleReplace / handleDelete close over asset+mutations and don't need a
605
+ // stable identity here; the consumers re-render with the new context value.
606
+ // eslint-disable-next-line react-hooks/exhaustive-deps
607
+ [
608
+ isReplacing,
609
+ isDeleting
610
+ ]);
211
611
  return(// `key={asset.id}` resets the form when the drawer switches to a different
212
612
  // asset so cached values from the previous asset don't bleed in.
213
- /*#__PURE__*/ jsx(Form, {
214
- method: "POST",
215
- initialValues: initialValues,
216
- onSubmit: handleSubmit,
217
- children: ({ modified, isSubmitting, values, resetForm })=>{
218
- const nameIsEmpty = (values.name ?? '').trim() === '';
219
- return /*#__PURE__*/ jsxs(Fragment, {
220
- children: [
221
- /*#__PURE__*/ jsx(Blocker, {
222
- onProceed: resetForm
223
- }),
224
- /*#__PURE__*/ jsxs(Flex, {
225
- direction: "column",
226
- alignItems: "stretch",
227
- gap: 4,
228
- paddingTop: 4,
229
- paddingBottom: 4,
230
- paddingLeft: 5,
231
- paddingRight: 5,
232
- children: [
233
- /*#__PURE__*/ jsx(Typography, {
234
- variant: "beta",
235
- fontWeight: "semiBold",
236
- tag: "h3",
237
- children: formatMessage({
238
- id: getTranslationKey('asset-details.fileInfo'),
239
- defaultMessage: 'File info'
240
- })
241
- }),
242
- /*#__PURE__*/ jsxs(Flex, {
243
- wrap: "wrap",
244
- gap: 4,
245
- background: "neutral100",
246
- paddingTop: 4,
247
- paddingBottom: 4,
248
- paddingLeft: 6,
249
- paddingRight: 6,
250
- alignItems: "flex-start",
251
- children: [
252
- /*#__PURE__*/ jsx(DetailItem, {
253
- label: formatMessage({
254
- id: getTranslationKey('asset-details.creationDate'),
255
- defaultMessage: 'Creation date'
256
- }),
257
- value: asset.createdAt ? formatDate(new Date(asset.createdAt), {
258
- dateStyle: 'long',
259
- timeStyle: 'short'
260
- }) : null
261
- }),
262
- /*#__PURE__*/ jsx(DetailItem, {
263
- label: formatMessage({
264
- id: getTranslationKey('asset-details.lastUpdated'),
265
- defaultMessage: 'Last updated'
266
- }),
267
- value: asset.updatedAt ? formatDate(new Date(asset.updatedAt), {
268
- dateStyle: 'long',
269
- timeStyle: 'short'
270
- }) : null
271
- }),
272
- /*#__PURE__*/ jsx(DetailItem, {
273
- label: formatMessage({
274
- id: getTranslationKey('asset-details.createdBy'),
275
- defaultMessage: 'Created by'
276
- }),
277
- value: asset.createdBy ? getDisplayName({
278
- firstname: asset.createdBy.firstname ?? undefined,
279
- lastname: asset.createdBy.lastname ?? undefined,
280
- username: asset.createdBy.username ?? undefined,
281
- email: asset.createdBy.email ?? undefined
282
- }) ?? '-' : null
283
- }),
284
- /*#__PURE__*/ jsx(DetailItem, {
285
- label: formatMessage({
286
- id: getTranslationKey('asset-details.size'),
287
- defaultMessage: 'Size'
288
- }),
289
- value: asset.size ? formatBytes(asset.size, 1) : null
290
- }),
291
- isImage && (asset.width != null || asset.height != null) && /*#__PURE__*/ jsx(DetailItem, {
292
- label: formatMessage({
293
- id: getTranslationKey('asset-details.dimensions'),
294
- defaultMessage: 'Dimensions'
295
- }),
296
- value: asset.width != null && asset.height != null ? `${asset.width} × ${asset.height}` : null
297
- }),
298
- /*#__PURE__*/ jsx(DetailItem, {
299
- label: formatMessage({
300
- id: getTranslationKey('asset-details.extension'),
301
- defaultMessage: 'Extension'
302
- }),
303
- value: getFileExtension(asset.ext)
304
- }),
305
- /*#__PURE__*/ jsx(DetailItem, {
306
- label: formatMessage({
307
- id: getTranslationKey('asset-details.assetId'),
308
- defaultMessage: 'Asset ID'
309
- }),
310
- value: String(asset.id)
311
- })
312
- ]
313
- }),
314
- /*#__PURE__*/ jsx(DetailField, {
315
- name: "name",
316
- label: formatMessage({
317
- id: getTranslationKey('asset-details.fileName'),
318
- defaultMessage: 'File name'
319
- }),
320
- required: true
321
- }),
322
- /*#__PURE__*/ jsx(LocationField, {
323
- label: formatMessage({
324
- id: getTranslationKey('asset-details.location'),
325
- defaultMessage: 'Location'
326
- }),
327
- rootLabel: formatMessage({
328
- id: getTranslationKey('plugin.home'),
329
- defaultMessage: 'Home'
613
+ /*#__PURE__*/ jsx(DrawerNotifyContext.Provider, {
614
+ value: notify,
615
+ children: /*#__PURE__*/ jsx(AssetOperationsContext.Provider, {
616
+ value: operations,
617
+ children: /*#__PURE__*/ jsx(FormShell, {
618
+ children: /*#__PURE__*/ jsx(Form, {
619
+ method: "POST",
620
+ initialValues: initialValues,
621
+ onSubmit: handleSubmit,
622
+ children: ({ modified, isSubmitting, values, resetForm })=>{
623
+ const nameIsEmpty = (values.name ?? '').trim() === '';
624
+ return /*#__PURE__*/ jsxs(Fragment, {
625
+ children: [
626
+ /*#__PURE__*/ jsx(Blocker, {
627
+ onProceed: resetForm
330
628
  }),
331
- folders: folders
332
- }),
333
- isImage && /*#__PURE__*/ jsxs(Fragment, {
334
- children: [
335
- /*#__PURE__*/ jsx(DetailField, {
336
- name: "caption",
337
- label: formatMessage({
338
- id: getTranslationKey('asset-details.caption'),
339
- defaultMessage: 'Caption'
340
- })
341
- }),
342
- /*#__PURE__*/ jsx(DetailField, {
343
- name: "alternativeText",
344
- label: formatMessage({
345
- id: getTranslationKey('asset-details.alternativeText'),
346
- defaultMessage: 'Alternative text'
629
+ isReplacing || isDeleting ? /*#__PURE__*/ jsx(DrawerBusyOverlay, {
630
+ children: /*#__PURE__*/ jsx(Loader, {
631
+ children: formatMessage({
632
+ id: getTranslationKey(isDeleting ? 'asset-details.delete.loading' : 'asset-details.replace.loading'),
633
+ defaultMessage: isDeleting ? 'Deleting the file…' : 'Replacing the file…'
347
634
  })
348
635
  })
349
- ]
350
- }),
351
- /*#__PURE__*/ jsx(Flex, {
352
- justifyContent: "flex-end",
353
- gap: 2,
354
- paddingTop: 2,
355
- children: /*#__PURE__*/ jsx(Button, {
356
- type: "submit",
357
- variant: "default",
358
- loading: isSubmitting,
359
- // File name is required; block submit when it's empty or whitespace so the API can't 400 on a blank value.
360
- disabled: !modified || isSubmitting || nameIsEmpty,
361
- children: formatMessage({
362
- id: getTranslationKey('asset-details.save'),
363
- defaultMessage: 'Save changes'
636
+ }) : null,
637
+ drawerToast ? /*#__PURE__*/ jsx(DrawerToastSlot, {
638
+ children: /*#__PURE__*/ jsx(Alert, {
639
+ variant: drawerToast.type === 'success' ? 'success' : 'danger',
640
+ closeLabel: formatMessage({
641
+ id: 'global.close',
642
+ defaultMessage: 'Close'
643
+ }),
644
+ onClose: ()=>setDrawerToast(null),
645
+ children: drawerToast.message
364
646
  })
647
+ }) : null,
648
+ /*#__PURE__*/ jsxs(Drawer.ScrollableContent, {
649
+ children: [
650
+ /*#__PURE__*/ jsx(AssetPreview, {
651
+ asset: asset,
652
+ actions: isImage ? /*#__PURE__*/ jsx(Flex, {
653
+ direction: "column",
654
+ gap: 2,
655
+ children: /*#__PURE__*/ jsx(ReplaceAssetButton, {})
656
+ }) : null
657
+ }),
658
+ /*#__PURE__*/ jsxs(Flex, {
659
+ direction: "column",
660
+ alignItems: "stretch",
661
+ gap: 4,
662
+ paddingTop: 4,
663
+ paddingBottom: 4,
664
+ paddingLeft: 5,
665
+ paddingRight: 5,
666
+ children: [
667
+ /*#__PURE__*/ jsx(Typography, {
668
+ variant: "beta",
669
+ fontWeight: "semiBold",
670
+ tag: "h3",
671
+ children: formatMessage({
672
+ id: getTranslationKey('asset-details.fileInfo'),
673
+ defaultMessage: 'File info'
674
+ })
675
+ }),
676
+ /*#__PURE__*/ jsxs(Flex, {
677
+ wrap: "wrap",
678
+ gap: 4,
679
+ background: "neutral100",
680
+ paddingTop: 4,
681
+ paddingBottom: 4,
682
+ paddingLeft: 6,
683
+ paddingRight: 6,
684
+ alignItems: "flex-start",
685
+ children: [
686
+ /*#__PURE__*/ jsx(DetailItem, {
687
+ label: formatMessage({
688
+ id: getTranslationKey('asset-details.creationDate'),
689
+ defaultMessage: 'Creation date'
690
+ }),
691
+ value: asset.createdAt ? formatDate(new Date(asset.createdAt), {
692
+ dateStyle: 'long',
693
+ timeStyle: 'short'
694
+ }) : null
695
+ }),
696
+ /*#__PURE__*/ jsx(DetailItem, {
697
+ label: formatMessage({
698
+ id: getTranslationKey('asset-details.lastUpdated'),
699
+ defaultMessage: 'Last updated'
700
+ }),
701
+ value: asset.updatedAt ? formatDate(new Date(asset.updatedAt), {
702
+ dateStyle: 'long',
703
+ timeStyle: 'short'
704
+ }) : null
705
+ }),
706
+ /*#__PURE__*/ jsx(DetailItem, {
707
+ label: formatMessage({
708
+ id: getTranslationKey('asset-details.createdBy'),
709
+ defaultMessage: 'Created by'
710
+ }),
711
+ value: asset.createdBy ? getDisplayName({
712
+ firstname: asset.createdBy.firstname ?? undefined,
713
+ lastname: asset.createdBy.lastname ?? undefined,
714
+ username: asset.createdBy.username ?? undefined,
715
+ email: asset.createdBy.email ?? undefined
716
+ }) ?? '-' : null
717
+ }),
718
+ /*#__PURE__*/ jsx(DetailItem, {
719
+ label: formatMessage({
720
+ id: getTranslationKey('asset-details.size'),
721
+ defaultMessage: 'Size'
722
+ }),
723
+ value: asset.size ? formatBytes(asset.size, 1) : null
724
+ }),
725
+ isImage && (asset.width != null || asset.height != null) && /*#__PURE__*/ jsx(DetailItem, {
726
+ label: formatMessage({
727
+ id: getTranslationKey('asset-details.dimensions'),
728
+ defaultMessage: 'Dimensions'
729
+ }),
730
+ value: asset.width != null && asset.height != null ? `${asset.width} × ${asset.height}` : null
731
+ }),
732
+ /*#__PURE__*/ jsx(DetailItem, {
733
+ label: formatMessage({
734
+ id: getTranslationKey('asset-details.extension'),
735
+ defaultMessage: 'Extension'
736
+ }),
737
+ value: getFileExtension(asset.ext)
738
+ }),
739
+ /*#__PURE__*/ jsx(DetailItem, {
740
+ label: formatMessage({
741
+ id: getTranslationKey('asset-details.assetId'),
742
+ defaultMessage: 'Asset ID'
743
+ }),
744
+ value: String(asset.id)
745
+ })
746
+ ]
747
+ }),
748
+ /*#__PURE__*/ jsx(DetailField, {
749
+ name: "name",
750
+ label: formatMessage({
751
+ id: getTranslationKey('asset-details.fileName'),
752
+ defaultMessage: 'File name'
753
+ }),
754
+ required: true
755
+ }),
756
+ /*#__PURE__*/ jsx(LocationField, {
757
+ label: formatMessage({
758
+ id: getTranslationKey('asset-details.location'),
759
+ defaultMessage: 'Location'
760
+ }),
761
+ rootLabel: formatMessage({
762
+ id: getTranslationKey('plugin.home'),
763
+ defaultMessage: 'Home'
764
+ }),
765
+ folders: folders
766
+ }),
767
+ isImage && /*#__PURE__*/ jsxs(Fragment, {
768
+ children: [
769
+ /*#__PURE__*/ jsx(DetailField, {
770
+ name: "caption",
771
+ label: formatMessage({
772
+ id: getTranslationKey('asset-details.caption'),
773
+ defaultMessage: 'Caption'
774
+ })
775
+ }),
776
+ /*#__PURE__*/ jsx(DetailField, {
777
+ name: "alternativeText",
778
+ label: formatMessage({
779
+ id: getTranslationKey('asset-details.alternativeText'),
780
+ defaultMessage: 'Alternative text'
781
+ })
782
+ })
783
+ ]
784
+ })
785
+ ]
786
+ })
787
+ ]
788
+ }),
789
+ /*#__PURE__*/ jsxs(Flex, {
790
+ justifyContent: "space-between",
791
+ alignItems: "center",
792
+ gap: 2,
793
+ padding: 3,
794
+ borderColor: "neutral150",
795
+ borderStyle: "solid",
796
+ borderWidth: "1px 0 0 0",
797
+ background: "neutral0",
798
+ children: [
799
+ /*#__PURE__*/ jsxs(Flex, {
800
+ gap: 2,
801
+ children: [
802
+ /*#__PURE__*/ jsx(DeleteAssetButton, {}),
803
+ /*#__PURE__*/ jsx(CopyLinkButton, {
804
+ asset: asset
805
+ }),
806
+ /*#__PURE__*/ jsx(DownloadAssetButton, {
807
+ asset: asset
808
+ })
809
+ ]
810
+ }),
811
+ /*#__PURE__*/ jsx(Button, {
812
+ type: "submit",
813
+ variant: "default",
814
+ loading: isSubmitting,
815
+ // File name is required; block submit when it's empty or whitespace so the API can't 400 on a blank value.
816
+ disabled: !modified || isSubmitting || nameIsEmpty,
817
+ children: formatMessage({
818
+ id: getTranslationKey('asset-details.save'),
819
+ defaultMessage: 'Save changes'
820
+ })
821
+ })
822
+ ]
365
823
  })
366
- })
367
- ]
368
- })
369
- ]
370
- });
371
- }
372
- }, asset.id));
824
+ ]
825
+ });
826
+ }
827
+ }, asset.id)
828
+ })
829
+ })
830
+ }));
373
831
  };
374
832
  const DrawerHeader = ({ asset, closeDetails })=>{
375
833
  const DocIcon = asset ? getAssetIcon(asset.mime, asset.ext) : FileError;
@@ -379,6 +837,9 @@ const DrawerHeader = ({ asset, closeDetails })=>{
379
837
  paddingTop: 3,
380
838
  paddingBottom: 3,
381
839
  paddingRight: 3,
840
+ borderColor: "neutral150",
841
+ borderStyle: "solid",
842
+ borderWidth: "0 0 1px 0",
382
843
  children: [
383
844
  /*#__PURE__*/ jsx(DocIcon, {
384
845
  width: 20,
@@ -450,15 +911,9 @@ const DrawerContent = ({ assetId, closeDetails })=>{
450
911
  asset: asset,
451
912
  closeDetails: closeDetails
452
913
  }),
453
- /*#__PURE__*/ jsxs(Drawer.ScrollableContent, {
454
- children: [
455
- /*#__PURE__*/ jsx(AssetPreview, {
456
- asset: asset
457
- }),
458
- /*#__PURE__*/ jsx(AssetDetails, {
459
- asset: asset
460
- })
461
- ]
914
+ /*#__PURE__*/ jsx(AssetDetails, {
915
+ asset: asset,
916
+ closeDetails: closeDetails
462
917
  })
463
918
  ]
464
919
  });
@@ -507,5 +962,5 @@ const DrawerContent = ({ assetId, closeDetails })=>{
507
962
  });
508
963
  };
509
964
 
510
- export { AssetDetails, AssetDetailsDrawer, useAssetDetailsParam };
965
+ export { AssetDetails, AssetDetailsDrawer, AssetOperationsContext, DrawerNotifyContext, useAssetDetailsParam };
511
966
  //# sourceMappingURL=AssetDetailsDrawer.mjs.map