@strapi/upload 5.47.0 → 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 (126) 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 -2
  10. package/dist/admin/future/components/Drawer.js.map +1 -1
  11. package/dist/admin/future/components/Drawer.mjs +7 -2
  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 +733 -148
  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 +737 -155
  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 +88 -1
  34. package/dist/admin/future/services/assets.js.map +1 -1
  35. package/dist/admin/future/services/assets.mjs +86 -2
  36. package/dist/admin/future/services/assets.mjs.map +1 -1
  37. package/dist/admin/future/services/folders.js +33 -1
  38. package/dist/admin/future/services/folders.js.map +1 -1
  39. package/dist/admin/future/services/folders.mjs +33 -2
  40. package/dist/admin/future/services/folders.mjs.map +1 -1
  41. package/dist/admin/future/services/settings.js +18 -0
  42. package/dist/admin/future/services/settings.js.map +1 -0
  43. package/dist/admin/future/services/settings.mjs +16 -0
  44. package/dist/admin/future/services/settings.mjs.map +1 -0
  45. package/dist/admin/future/services/uploadFileViaXHR.js +92 -0
  46. package/dist/admin/future/services/uploadFileViaXHR.js.map +1 -0
  47. package/dist/admin/future/services/uploadFileViaXHR.mjs +88 -0
  48. package/dist/admin/future/services/uploadFileViaXHR.mjs.map +1 -0
  49. package/dist/admin/future/store/uploadProgress.js +32 -26
  50. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  51. package/dist/admin/future/store/uploadProgress.mjs +32 -27
  52. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  53. package/dist/admin/future/utils/createRafBatcher.js +42 -0
  54. package/dist/admin/future/utils/createRafBatcher.js.map +1 -0
  55. package/dist/admin/future/utils/createRafBatcher.mjs +40 -0
  56. package/dist/admin/future/utils/createRafBatcher.mjs.map +1 -0
  57. package/dist/admin/future/utils/downloadFile.js +19 -0
  58. package/dist/admin/future/utils/downloadFile.js.map +1 -0
  59. package/dist/admin/future/utils/downloadFile.mjs +17 -0
  60. package/dist/admin/future/utils/downloadFile.mjs.map +1 -0
  61. package/dist/admin/hooks/useAssets.js +5 -3
  62. package/dist/admin/hooks/useAssets.js.map +1 -1
  63. package/dist/admin/hooks/useAssets.mjs +5 -3
  64. package/dist/admin/hooks/useAssets.mjs.map +1 -1
  65. package/dist/admin/index.js +1 -1
  66. package/dist/admin/index.mjs +1 -1
  67. package/dist/admin/src/components/EditAssetDialog/EditAssetContent.d.ts +2 -1
  68. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.d.ts +22 -0
  69. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetPreview.d.ts +4 -1
  70. package/dist/admin/src/future/services/api.d.ts +9 -8
  71. package/dist/admin/src/future/services/assets.d.ts +11 -2
  72. package/dist/admin/src/future/services/folders.d.ts +1 -1
  73. package/dist/admin/src/future/services/uploadFileViaXHR.d.ts +34 -0
  74. package/dist/admin/src/future/store/uploadProgress.d.ts +17 -4
  75. package/dist/admin/src/future/utils/createRafBatcher.d.ts +23 -0
  76. package/dist/admin/src/future/utils/downloadFile.d.ts +6 -0
  77. package/dist/admin/translations/{dk.json.js → da.json.js} +3 -3
  78. package/dist/admin/translations/{dk.json.js.map → da.json.js.map} +1 -1
  79. package/dist/admin/translations/{dk.json.mjs → da.json.mjs} +3 -3
  80. package/dist/admin/translations/{dk.json.mjs.map → da.json.mjs.map} +1 -1
  81. package/dist/admin/translations/en.json.js +26 -1
  82. package/dist/admin/translations/en.json.js.map +1 -1
  83. package/dist/admin/translations/en.json.mjs +26 -1
  84. package/dist/admin/translations/en.json.mjs.map +1 -1
  85. package/dist/server/bootstrap.js +0 -3
  86. package/dist/server/bootstrap.js.map +1 -1
  87. package/dist/server/bootstrap.mjs +0 -3
  88. package/dist/server/bootstrap.mjs.map +1 -1
  89. package/dist/server/controllers/admin-upload.js +69 -118
  90. package/dist/server/controllers/admin-upload.js.map +1 -1
  91. package/dist/server/controllers/admin-upload.mjs +69 -118
  92. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  93. package/dist/server/routes/admin.js +2 -2
  94. package/dist/server/routes/admin.js.map +1 -1
  95. package/dist/server/routes/admin.mjs +2 -2
  96. package/dist/server/routes/admin.mjs.map +1 -1
  97. package/dist/server/services/ai-metadata-jobs.js +0 -23
  98. package/dist/server/services/ai-metadata-jobs.js.map +1 -1
  99. package/dist/server/services/ai-metadata-jobs.mjs +0 -23
  100. package/dist/server/services/ai-metadata-jobs.mjs.map +1 -1
  101. package/dist/server/services/image-manipulation.js +16 -8
  102. package/dist/server/services/image-manipulation.js.map +1 -1
  103. package/dist/server/services/image-manipulation.mjs +16 -8
  104. package/dist/server/services/image-manipulation.mjs.map +1 -1
  105. package/dist/server/services/upload.js +1 -1
  106. package/dist/server/services/upload.js.map +1 -1
  107. package/dist/server/services/upload.mjs +1 -1
  108. package/dist/server/services/upload.mjs.map +1 -1
  109. package/dist/server/src/bootstrap.d.ts.map +1 -1
  110. package/dist/server/src/controllers/admin-upload.d.ts +6 -8
  111. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  112. package/dist/server/src/controllers/index.d.ts +1 -1
  113. package/dist/server/src/index.d.ts +1 -2
  114. package/dist/server/src/index.d.ts.map +1 -1
  115. package/dist/server/src/services/ai-metadata-jobs.d.ts +0 -1
  116. package/dist/server/src/services/ai-metadata-jobs.d.ts.map +1 -1
  117. package/dist/server/src/services/image-manipulation.d.ts +5 -0
  118. package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
  119. package/dist/server/src/services/index.d.ts +0 -1
  120. package/dist/server/src/services/index.d.ts.map +1 -1
  121. package/dist/server/src/services/upload.d.ts.map +1 -1
  122. package/dist/server/src/types.d.ts +2 -2
  123. package/dist/server/src/types.d.ts.map +1 -1
  124. package/dist/shared/contracts/files.d.ts +19 -2
  125. package/dist/shared/contracts/files.d.ts.map +1 -1
  126. package/package.json +8 -8
@@ -1,20 +1,40 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import { useQueryParams, getDisplayName } from '@strapi/admin/strapi-admin';
4
- import { VisuallyHidden, Flex, Loader, Alert, Typography, Box, Field, TextInput } 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
- import { DRAWER_CLOSE_ANIMATION_MS, Drawer } from '../../../../components/Drawer.mjs';
8
+ import { Drawer } from '../../../../components/Drawer.mjs';
9
9
  import { AssetType } from '../../../../enums.mjs';
10
- import { useGetAssetQuery } from '../../../../services/assets.mjs';
11
- import { formatBytes, getFileExtension } from '../../../../utils/files.mjs';
10
+ import { useGetAssetQuery, useUpdateAssetMutation, useReplaceAssetMutation, useDeleteAssetMutation } from '../../../../services/assets.mjs';
11
+ import { useGetAllFoldersQuery } from '../../../../services/folders.mjs';
12
+ import { useGetSettingsQuery } from '../../../../services/settings.mjs';
13
+ import { downloadFile } from '../../../../utils/downloadFile.mjs';
14
+ import { formatBytes, getFileExtension, prefixFileUrlWithBackendUrl } from '../../../../utils/files.mjs';
12
15
  import { getAssetIcon } from '../../../../utils/getAssetIcon.mjs';
13
16
  import { getTranslationKey } from '../../../../utils/translations.mjs';
17
+ import { useFolderInfo } from '../../hooks/useFolderInfo.mjs';
14
18
  import { AssetPreview } from './AssetPreview.mjs';
15
19
 
16
20
  // Name of the parameter to look for in the URL to open the drawer
17
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
+ };
18
38
  /* -------------------------------------------------------------------------------------------------
19
39
  * useAssetDetailsParam - sync drawer visibility with URL ?{URL_PARAM}={id}
20
40
  * -----------------------------------------------------------------------------------------------*/ const useAssetDetailsParam = ()=>{
@@ -22,51 +42,49 @@ const URL_PARAM = 'assetId';
22
42
  const detailsId = query?.[URL_PARAM];
23
43
  const assetId = detailsId ? parseInt(detailsId, 10) : null;
24
44
  const hasValidId = assetId !== null && !Number.isNaN(assetId);
25
- const [isClosing, setIsClosing] = React.useState(false);
45
+ // Closing is driven by removing the URL param (a navigation), so navigation
46
+ // guards like <Blocker> can intercept it. `isMounted` keeps the drawer in the
47
+ // tree through the slide-out: it stays true once opened and only flips false
48
+ // when the close animation actually ends (see onCloseAnimationEnd), so the
49
+ // close duration lives entirely in CSS — no JS timer.
50
+ const [isMounted, setIsMounted] = React.useState(hasValidId);
26
51
  const displayAssetId = React.useRef(null);
27
- const isVisible = hasValidId && !isClosing;
28
52
  React.useEffect(()=>{
29
53
  if (hasValidId) {
30
54
  displayAssetId.current = assetId;
55
+ setIsMounted(true);
31
56
  }
32
57
  }, [
33
58
  hasValidId,
34
59
  assetId
35
60
  ]);
61
+ const onCloseAnimationEnd = React.useCallback((event)=>{
62
+ // Ignore animations bubbling up from descendants, and the slide-in.
63
+ if (event.target === event.currentTarget && !hasValidId) {
64
+ setIsMounted(false);
65
+ }
66
+ }, [
67
+ hasValidId
68
+ ]);
36
69
  const openDetails = React.useCallback((id)=>{
37
- setIsClosing(false);
38
70
  setQuery({
39
71
  [URL_PARAM]: String(id)
40
- });
72
+ }, 'push', true);
41
73
  }, [
42
74
  setQuery
43
75
  ]);
44
76
  const closeDetails = React.useCallback(()=>{
45
- if (!hasValidId) return;
46
- setIsClosing(true);
47
- }, [
48
- hasValidId
49
- ]);
50
- React.useEffect(()=>{
51
- if (!isClosing) return;
52
- const timer = window.setTimeout(()=>{
53
- setQuery({
54
- [URL_PARAM]: undefined
55
- }, 'remove');
56
- setIsClosing(false);
57
- displayAssetId.current = null;
58
- }, DRAWER_CLOSE_ANIMATION_MS);
59
- return ()=>window.clearTimeout(timer);
77
+ setQuery({
78
+ [URL_PARAM]: undefined
79
+ }, 'remove', true);
60
80
  }, [
61
- isClosing,
62
81
  setQuery
63
82
  ]);
64
- const shouldRenderDrawer = hasValidId || isClosing;
65
- const drawerAssetId = isClosing ? displayAssetId.current ?? assetId : assetId;
66
83
  return {
67
- assetId: drawerAssetId,
68
- isVisible,
69
- shouldRenderDrawer,
84
+ assetId: hasValidId ? assetId : displayAssetId.current,
85
+ isVisible: hasValidId,
86
+ shouldRenderDrawer: isMounted,
87
+ onCloseAnimationEnd,
70
88
  openDetails,
71
89
  closeDetails
72
90
  };
@@ -96,7 +114,50 @@ const DetailItem = ({ label, value })=>/*#__PURE__*/ jsxs(DetailItemContainer, {
96
114
  });
97
115
  /* -------------------------------------------------------------------------------------------------
98
116
  * DetailField
99
- * -----------------------------------------------------------------------------------------------*/ 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)`
100
161
  width: 1.6rem;
101
162
  height: 1.6rem;
102
163
 
@@ -104,7 +165,18 @@ const DetailItem = ({ label, value })=>/*#__PURE__*/ jsxs(DetailItemContainer, {
104
165
  fill: ${({ theme })=>theme.colors.warning500};
105
166
  }
106
167
  `;
107
- const DetailField = ({ name, label, value, required })=>/*#__PURE__*/ jsxs(Field.Root, {
168
+ const DetailField = ({ name, label, required })=>{
169
+ const { formatMessage } = useIntl();
170
+ const field = useField(name);
171
+ const isSubmitting = useForm('DetailField', (state)=>state.isSubmitting);
172
+ const value = field.value ?? '';
173
+ const emptyTooltipLabel = formatMessage({
174
+ id: getTranslationKey('asset-details.field.empty'),
175
+ defaultMessage: '{label} is currently empty.'
176
+ }, {
177
+ label
178
+ });
179
+ return /*#__PURE__*/ jsxs(Field.Root, {
108
180
  name: name,
109
181
  required: required,
110
182
  children: [
@@ -112,139 +184,651 @@ const DetailField = ({ name, label, value, required })=>/*#__PURE__*/ jsxs(Field
112
184
  children: label
113
185
  }),
114
186
  /*#__PURE__*/ jsx(TextInput, {
115
- value: value ?? '',
116
- // TODO: handle onChange
117
- onChange: ()=>{},
118
- endAction: !value ? /*#__PURE__*/ jsx(StyledWarning, {}) : undefined,
119
- type: "text"
187
+ value: value,
188
+ onChange: (event)=>field.onChange(name, event.target.value),
189
+ endAction: !value ? /*#__PURE__*/ jsx(Tooltip, {
190
+ label: emptyTooltipLabel,
191
+ children: /*#__PURE__*/ jsx(StyledWarning, {
192
+ "aria-label": emptyTooltipLabel,
193
+ role: "img"
194
+ })
195
+ }) : undefined,
196
+ type: "text",
197
+ disabled: isSubmitting
120
198
  })
121
199
  ]
122
200
  });
123
- const AssetDetails = ({ asset })=>{
124
- const { formatMessage, formatDate } = useIntl();
125
- const isImage = asset.mime?.includes(AssetType.Image);
126
- return /*#__PURE__*/ jsxs(Flex, {
127
- direction: "column",
128
- alignItems: "stretch",
129
- gap: 4,
130
- paddingTop: 4,
131
- paddingBottom: 4,
132
- paddingLeft: 5,
133
- paddingRight: 5,
201
+ };
202
+ const LocationField = ({ label, rootLabel, folders })=>{
203
+ const field = useField('folder');
204
+ const isSubmitting = useForm('LocationField', (state)=>state.isSubmitting);
205
+ return /*#__PURE__*/ jsxs(Field.Root, {
206
+ name: "folder",
207
+ required: true,
134
208
  children: [
135
- /*#__PURE__*/ jsx(Typography, {
136
- variant: "beta",
137
- fontWeight: "semiBold",
138
- tag: "h3",
139
- children: formatMessage({
140
- id: getTranslationKey('asset-details.fileInfo'),
141
- defaultMessage: 'File info'
142
- })
209
+ /*#__PURE__*/ jsx(Field.Label, {
210
+ children: label
143
211
  }),
144
- /*#__PURE__*/ jsxs(Flex, {
145
- wrap: "wrap",
146
- gap: 4,
147
- background: "neutral100",
148
- paddingTop: 4,
149
- paddingBottom: 4,
150
- paddingLeft: 6,
151
- paddingRight: 6,
152
- alignItems: "flex-start",
212
+ /*#__PURE__*/ jsxs(SingleSelect, {
213
+ value: field.value == null ? '' : String(field.value),
214
+ onChange: (value)=>{
215
+ const next = value === '' ? null : Number(value);
216
+ field.onChange('folder', next);
217
+ },
218
+ disabled: isSubmitting,
153
219
  children: [
154
- /*#__PURE__*/ jsx(DetailItem, {
155
- label: formatMessage({
156
- id: getTranslationKey('asset-details.creationDate'),
157
- defaultMessage: 'Creation date'
158
- }),
159
- value: asset.createdAt ? formatDate(new Date(asset.createdAt), {
160
- dateStyle: 'long',
161
- timeStyle: 'short'
162
- }) : null
163
- }),
164
- /*#__PURE__*/ jsx(DetailItem, {
165
- label: formatMessage({
166
- id: getTranslationKey('asset-details.lastUpdated'),
167
- defaultMessage: 'Last updated'
168
- }),
169
- value: asset.updatedAt ? formatDate(new Date(asset.updatedAt), {
170
- dateStyle: 'long',
171
- timeStyle: 'short'
172
- }) : null
173
- }),
174
- /*#__PURE__*/ jsx(DetailItem, {
175
- label: formatMessage({
176
- id: getTranslationKey('asset-details.createdBy'),
177
- defaultMessage: 'Created by'
178
- }),
179
- value: asset.createdBy ? getDisplayName({
180
- firstname: asset.createdBy.firstname ?? undefined,
181
- lastname: asset.createdBy.lastname ?? undefined,
182
- username: asset.createdBy.username ?? undefined,
183
- email: asset.createdBy.email ?? undefined
184
- }) ?? '-' : null
220
+ /*#__PURE__*/ jsx(SingleSelectOption, {
221
+ value: "",
222
+ children: rootLabel
185
223
  }),
186
- /*#__PURE__*/ jsx(DetailItem, {
187
- label: formatMessage({
188
- id: getTranslationKey('asset-details.size'),
189
- defaultMessage: 'Size'
190
- }),
191
- value: asset.size ? formatBytes(asset.size, 1) : null
192
- }),
193
- isImage && (asset.width != null || asset.height != null) && /*#__PURE__*/ jsx(DetailItem, {
194
- label: formatMessage({
195
- id: getTranslationKey('asset-details.dimensions'),
196
- defaultMessage: 'Dimensions'
197
- }),
198
- value: asset.width != null && asset.height != null ? `${asset.width} × ${asset.height}` : null
224
+ folders.map((folder)=>/*#__PURE__*/ jsx(SingleSelectOption, {
225
+ value: String(folder.id),
226
+ children: folder.name
227
+ }, folder.id))
228
+ ]
229
+ })
230
+ ]
231
+ });
232
+ };
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
+ })
199
266
  }),
200
- /*#__PURE__*/ jsx(DetailItem, {
201
- label: formatMessage({
202
- id: getTranslationKey('asset-details.extension'),
203
- defaultMessage: 'Extension'
267
+ /*#__PURE__*/ jsx(Dialog.Body, {
268
+ icon: /*#__PURE__*/ jsx(WarningCircle, {
269
+ width: "24px",
270
+ height: "24px",
271
+ fill: "danger600"
204
272
  }),
205
- value: getFileExtension(asset.ext)
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
+ })
206
278
  }),
207
- /*#__PURE__*/ jsx(DetailItem, {
208
- label: formatMessage({
209
- id: getTranslationKey('asset-details.assetId'),
210
- defaultMessage: 'Asset ID'
211
- }),
212
- value: String(asset.id)
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
+ ]
213
305
  })
214
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
+ })
215
414
  }),
216
- /*#__PURE__*/ jsx(DetailField, {
217
- name: "fileName",
415
+ /*#__PURE__*/ jsx(IconButton, {
416
+ withTooltip: false,
218
417
  label: formatMessage({
219
- id: getTranslationKey('asset-details.fileName'),
220
- defaultMessage: 'File name'
418
+ id: getTranslationKey('asset-details.replace.trigger'),
419
+ defaultMessage: 'Replace this file'
221
420
  }),
222
- value: asset.name,
223
- required: true
421
+ variant: "tertiary",
422
+ onClick: handleTriggerClick,
423
+ disabled: isReplacing,
424
+ children: /*#__PURE__*/ jsx(ArrowsCounterClockwise, {})
224
425
  }),
225
- isImage && /*#__PURE__*/ jsxs(Fragment, {
226
- children: [
227
- /*#__PURE__*/ jsx(DetailField, {
228
- name: "caption",
229
- label: formatMessage({
230
- id: getTranslationKey('asset-details.caption'),
231
- defaultMessage: 'Caption'
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
+ })
232
436
  }),
233
- value: asset.caption
234
- }),
235
- /*#__PURE__*/ jsx(DetailField, {
236
- name: "alternativeText",
237
- label: formatMessage({
238
- id: getTranslationKey('asset-details.alternativeText'),
239
- defaultMessage: 'Alternative text'
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
+ })
240
459
  }),
241
- value: asset.alternativeText
242
- })
243
- ]
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
+ })
244
487
  })
245
488
  ]
246
489
  });
247
490
  };
491
+ const AssetDetails = ({ asset, closeDetails })=>{
492
+ const { formatMessage, formatDate } = useIntl();
493
+ const { data: folders = [] } = useGetAllFoldersQuery();
494
+ const { toggleNotification } = useNotification();
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), []);
510
+ const isImage = asset.mime?.includes(AssetType.Image);
511
+ const initialValues = {
512
+ name: asset.name ?? '',
513
+ caption: asset.caption ?? '',
514
+ alternativeText: asset.alternativeText ?? '',
515
+ folder: typeof asset.folder === 'object' && asset.folder !== null ? asset.folder.id ?? null : asset.folder ?? null
516
+ };
517
+ const handleSubmit = async (values)=>{
518
+ const res = await updateAsset({
519
+ id: asset.id,
520
+ fileInfo: {
521
+ name: values.name,
522
+ caption: values.caption,
523
+ alternativeText: values.alternativeText,
524
+ folder: values.folder
525
+ }
526
+ });
527
+ if ('error' in res) {
528
+ notify({
529
+ type: 'danger',
530
+ message: formatMessage({
531
+ id: getTranslationKey('asset-details.update.error'),
532
+ defaultMessage: 'Failed to update the file.'
533
+ })
534
+ });
535
+ return;
536
+ }
537
+ notify({
538
+ type: 'success',
539
+ message: formatMessage({
540
+ id: getTranslationKey('asset-details.update.success'),
541
+ defaultMessage: 'File updated'
542
+ })
543
+ });
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
+ ]);
611
+ return(// `key={asset.id}` resets the form when the drawer switches to a different
612
+ // asset so cached values from the previous asset don't bleed in.
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
628
+ }),
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…'
634
+ })
635
+ })
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
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
+ ]
823
+ })
824
+ ]
825
+ });
826
+ }
827
+ }, asset.id)
828
+ })
829
+ })
830
+ }));
831
+ };
248
832
  const DrawerHeader = ({ asset, closeDetails })=>{
249
833
  const DocIcon = asset ? getAssetIcon(asset.mime, asset.ext) : FileError;
250
834
  return /*#__PURE__*/ jsxs(Flex, {
@@ -253,6 +837,9 @@ const DrawerHeader = ({ asset, closeDetails })=>{
253
837
  paddingTop: 3,
254
838
  paddingBottom: 3,
255
839
  paddingRight: 3,
840
+ borderColor: "neutral150",
841
+ borderStyle: "solid",
842
+ borderWidth: "0 0 1px 0",
256
843
  children: [
257
844
  /*#__PURE__*/ jsx(DocIcon, {
258
845
  width: 20,
@@ -324,15 +911,9 @@ const DrawerContent = ({ assetId, closeDetails })=>{
324
911
  asset: asset,
325
912
  closeDetails: closeDetails
326
913
  }),
327
- /*#__PURE__*/ jsxs(Drawer.ScrollableContent, {
328
- children: [
329
- /*#__PURE__*/ jsx(AssetPreview, {
330
- asset: asset
331
- }),
332
- /*#__PURE__*/ jsx(AssetDetails, {
333
- asset: asset
334
- })
335
- ]
914
+ /*#__PURE__*/ jsx(AssetDetails, {
915
+ asset: asset,
916
+ closeDetails: closeDetails
336
917
  })
337
918
  ]
338
919
  });
@@ -341,7 +922,7 @@ const DrawerContent = ({ assetId, closeDetails })=>{
341
922
  * AssetDetailsDrawer
342
923
  * -----------------------------------------------------------------------------------------------*/ const AssetDetailsDrawer = ()=>{
343
924
  const { formatMessage } = useIntl();
344
- const { assetId, isVisible, shouldRenderDrawer, closeDetails } = useAssetDetailsParam();
925
+ const { assetId, isVisible, shouldRenderDrawer, onCloseAnimationEnd, closeDetails } = useAssetDetailsParam();
345
926
  if (!shouldRenderDrawer || assetId === null) {
346
927
  return null;
347
928
  }
@@ -371,6 +952,7 @@ const DrawerContent = ({ assetId, closeDetails })=>{
371
952
  animationDirection: "left",
372
953
  width: "41.6rem",
373
954
  height: "100vh",
955
+ onAnimationEnd: onCloseAnimationEnd,
374
956
  children: /*#__PURE__*/ jsx(DrawerContent, {
375
957
  assetId: assetId,
376
958
  closeDetails: closeDetails
@@ -380,5 +962,5 @@ const DrawerContent = ({ assetId, closeDetails })=>{
380
962
  });
381
963
  };
382
964
 
383
- export { AssetDetailsDrawer, useAssetDetailsParam };
965
+ export { AssetDetails, AssetDetailsDrawer, AssetOperationsContext, DrawerNotifyContext, useAssetDetailsParam };
384
966
  //# sourceMappingURL=AssetDetailsDrawer.mjs.map