@orthacms/media-admin 0.0.0-reserve.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/lib/components/AssetActionsMenu/index.d.ts +37 -0
  7. package/dist/lib/components/AssetActionsMenu/index.d.ts.map +1 -0
  8. package/dist/lib/components/AssetActionsMenu/index.js +57 -0
  9. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts +11 -0
  10. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts.map +1 -0
  11. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.js +9 -0
  12. package/dist/lib/components/AssetDetailDrawer/index.d.ts +28 -0
  13. package/dist/lib/components/AssetDetailDrawer/index.d.ts.map +1 -0
  14. package/dist/lib/components/AssetDetailDrawer/index.js +93 -0
  15. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts +28 -0
  16. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts.map +1 -0
  17. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.js +52 -0
  18. package/dist/lib/components/EntryMediaTab/index.d.ts +11 -0
  19. package/dist/lib/components/EntryMediaTab/index.d.ts.map +1 -0
  20. package/dist/lib/components/EntryMediaTab/index.js +31 -0
  21. package/dist/lib/components/FolderActionsMenu/index.d.ts +21 -0
  22. package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -0
  23. package/dist/lib/components/FolderActionsMenu/index.js +25 -0
  24. package/dist/lib/components/MediaAltTextWidget/index.d.ts +9 -0
  25. package/dist/lib/components/MediaAltTextWidget/index.d.ts.map +1 -0
  26. package/dist/lib/components/MediaAltTextWidget/index.js +66 -0
  27. package/dist/lib/components/MediaBreadcrumbs/index.d.ts +11 -0
  28. package/dist/lib/components/MediaBreadcrumbs/index.d.ts.map +1 -0
  29. package/dist/lib/components/MediaEmptyState/index.d.ts +13 -0
  30. package/dist/lib/components/MediaEmptyState/index.d.ts.map +1 -0
  31. package/dist/lib/components/MediaEmptyState/index.js +38 -0
  32. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts +34 -0
  33. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -0
  34. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +117 -0
  35. package/dist/lib/components/MediaFieldControl/index.d.ts +61 -0
  36. package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -0
  37. package/dist/lib/components/MediaFieldControl/index.js +422 -0
  38. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -0
  39. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -0
  40. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +37 -0
  41. package/dist/lib/components/MediaFoldersNav/index.d.ts +22 -0
  42. package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -0
  43. package/dist/lib/components/MediaFoldersNav/index.js +40 -0
  44. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +18 -0
  45. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -0
  46. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +29 -0
  47. package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts +16 -0
  48. package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts.map +1 -0
  49. package/dist/lib/components/MediaGrid/MediaFolderCard/index.js +25 -0
  50. package/dist/lib/components/MediaGrid/index.d.ts +24 -0
  51. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -0
  52. package/dist/lib/components/MediaGrid/index.js +20 -0
  53. package/dist/lib/components/MediaKindBadge/index.d.ts +10 -0
  54. package/dist/lib/components/MediaKindBadge/index.d.ts.map +1 -0
  55. package/dist/lib/components/MediaKindBadge/index.js +30 -0
  56. package/dist/lib/components/MediaKindIcon/index.d.ts +11 -0
  57. package/dist/lib/components/MediaKindIcon/index.d.ts.map +1 -0
  58. package/dist/lib/components/MediaKindIcon/index.js +20 -0
  59. package/dist/lib/components/MediaLibrarySkeleton/index.d.ts +18 -0
  60. package/dist/lib/components/MediaLibrarySkeleton/index.d.ts.map +1 -0
  61. package/dist/lib/components/MediaLibrarySkeleton/index.js +43 -0
  62. package/dist/lib/components/MediaPagination/index.d.ts +26 -0
  63. package/dist/lib/components/MediaPagination/index.d.ts.map +1 -0
  64. package/dist/lib/components/MediaPagination/index.js +52 -0
  65. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts +17 -0
  66. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts.map +1 -0
  67. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.js +43 -0
  68. package/dist/lib/components/MediaPickerDialog/index.d.ts +21 -0
  69. package/dist/lib/components/MediaPickerDialog/index.d.ts.map +1 -0
  70. package/dist/lib/components/MediaPickerDialog/index.js +208 -0
  71. package/dist/lib/components/MediaSelectionBar/index.d.ts +18 -0
  72. package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -0
  73. package/dist/lib/components/MediaSelectionBar/index.js +26 -0
  74. package/dist/lib/components/MediaStorageStat/index.d.ts +9 -0
  75. package/dist/lib/components/MediaStorageStat/index.d.ts.map +1 -0
  76. package/dist/lib/components/MediaStorageStat/index.js +31 -0
  77. package/dist/lib/components/MediaStorageWidget/index.d.ts +10 -0
  78. package/dist/lib/components/MediaStorageWidget/index.d.ts.map +1 -0
  79. package/dist/lib/components/MediaStorageWidget/index.js +93 -0
  80. package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts +17 -0
  81. package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts.map +1 -0
  82. package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts +16 -0
  83. package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts.map +1 -0
  84. package/dist/lib/components/MediaTable/index.d.ts +26 -0
  85. package/dist/lib/components/MediaTable/index.d.ts.map +1 -0
  86. package/dist/lib/components/MediaThumbnail/index.d.ts +19 -0
  87. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -0
  88. package/dist/lib/components/MediaThumbnail/index.js +26 -0
  89. package/dist/lib/components/MediaToolbar/index.d.ts +19 -0
  90. package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -0
  91. package/dist/lib/components/MediaToolbar/index.js +62 -0
  92. package/dist/lib/components/MediaTopBar/index.d.ts +20 -0
  93. package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -0
  94. package/dist/lib/components/MediaTopBar/index.js +35 -0
  95. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts +14 -0
  96. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts.map +1 -0
  97. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.js +51 -0
  98. package/dist/lib/components/MediaUploadBanner/index.d.ts +24 -0
  99. package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -0
  100. package/dist/lib/components/MediaUploadBanner/index.js +99 -0
  101. package/dist/lib/components/MediaUploadsWidget/index.d.ts +7 -0
  102. package/dist/lib/components/MediaUploadsWidget/index.d.ts.map +1 -0
  103. package/dist/lib/components/MediaUploadsWidget/index.js +70 -0
  104. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts +19 -0
  105. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts.map +1 -0
  106. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.js +10 -0
  107. package/dist/lib/components/MoveAssetsDialog/index.d.ts +18 -0
  108. package/dist/lib/components/MoveAssetsDialog/index.d.ts.map +1 -0
  109. package/dist/lib/components/MoveAssetsDialog/index.js +41 -0
  110. package/dist/lib/components/NewFolderDialog/index.d.ts +13 -0
  111. package/dist/lib/components/NewFolderDialog/index.d.ts.map +1 -0
  112. package/dist/lib/components/NewFolderDialog/index.js +45 -0
  113. package/dist/lib/components/RenameDialog/index.d.ts +13 -0
  114. package/dist/lib/components/RenameDialog/index.d.ts.map +1 -0
  115. package/dist/lib/components/RenameDialog/index.js +37 -0
  116. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts +25 -0
  117. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -0
  118. package/dist/lib/components/UploadDialog/StagedFileRow/index.js +82 -0
  119. package/dist/lib/components/UploadDialog/index.d.ts +50 -0
  120. package/dist/lib/components/UploadDialog/index.d.ts.map +1 -0
  121. package/dist/lib/components/UploadDialog/index.js +120 -0
  122. package/dist/lib/components/WysiwygLibrarySource/index.d.ts +14 -0
  123. package/dist/lib/components/WysiwygLibrarySource/index.d.ts.map +1 -0
  124. package/dist/lib/components/WysiwygLibrarySource/index.js +28 -0
  125. package/dist/lib/components/WysiwygUploadSource/index.d.ts +25 -0
  126. package/dist/lib/components/WysiwygUploadSource/index.d.ts.map +1 -0
  127. package/dist/lib/components/WysiwygUploadSource/index.js +102 -0
  128. package/dist/lib/constants/index.d.ts +82 -0
  129. package/dist/lib/constants/index.d.ts.map +1 -0
  130. package/dist/lib/constants/index.js +75 -0
  131. package/dist/lib/hooks/useMediaInsights/index.d.ts +32 -0
  132. package/dist/lib/hooks/useMediaInsights/index.d.ts.map +1 -0
  133. package/dist/lib/hooks/useMediaInsights/index.js +85 -0
  134. package/dist/lib/hooks/useMediaLibrary/index.d.ts +83 -0
  135. package/dist/lib/hooks/useMediaLibrary/index.d.ts.map +1 -0
  136. package/dist/lib/hooks/useMediaLibrary/index.js +341 -0
  137. package/dist/lib/hooks/usePendingMediaUploads/index.d.ts +26 -0
  138. package/dist/lib/hooks/usePendingMediaUploads/index.d.ts.map +1 -0
  139. package/dist/lib/hooks/usePendingMediaUploads/index.js +217 -0
  140. package/dist/lib/hooks/useUploadQueue/index.d.ts +33 -0
  141. package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -0
  142. package/dist/lib/hooks/useUploadQueue/index.js +218 -0
  143. package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts +23 -0
  144. package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts.map +1 -0
  145. package/dist/lib/infrastructure/apiMessage/index.d.ts +17 -0
  146. package/dist/lib/infrastructure/apiMessage/index.d.ts.map +1 -0
  147. package/dist/lib/infrastructure/apiMessage/index.js +30 -0
  148. package/dist/lib/infrastructure/httpMediaGateway/index.d.ts +9 -0
  149. package/dist/lib/infrastructure/httpMediaGateway/index.d.ts.map +1 -0
  150. package/dist/lib/infrastructure/httpMediaGateway/index.js +157 -0
  151. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts +8 -0
  152. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts.map +1 -0
  153. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.js +35 -0
  154. package/dist/lib/infrastructure/mediaGateway/index.d.ts +120 -0
  155. package/dist/lib/infrastructure/mediaGateway/index.d.ts.map +1 -0
  156. package/dist/lib/infrastructure/mediaGateway/index.js +1 -0
  157. package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts +46 -0
  158. package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts.map +1 -0
  159. package/dist/lib/infrastructure/mediaInsightsGateway/index.js +6 -0
  160. package/dist/lib/infrastructure/mediaKeys/index.d.ts +23 -0
  161. package/dist/lib/infrastructure/mediaKeys/index.d.ts.map +1 -0
  162. package/dist/lib/infrastructure/mediaKeys/index.js +11 -0
  163. package/dist/lib/infrastructure/mediaMapper/index.d.ts +50 -0
  164. package/dist/lib/infrastructure/mediaMapper/index.d.ts.map +1 -0
  165. package/dist/lib/infrastructure/mediaMapper/index.js +39 -0
  166. package/dist/lib/pages/MediaLibraryPage/index.d.ts +23 -0
  167. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -0
  168. package/dist/lib/pages/MediaLibraryPage/index.js +425 -0
  169. package/dist/lib/types/mediaAsset/index.d.ts +44 -0
  170. package/dist/lib/types/mediaAsset/index.d.ts.map +1 -0
  171. package/dist/lib/types/mediaAsset/index.js +1 -0
  172. package/dist/lib/types/mediaFieldDisplay/index.d.ts +48 -0
  173. package/dist/lib/types/mediaFieldDisplay/index.d.ts.map +1 -0
  174. package/dist/lib/types/mediaFieldDisplay/index.js +1 -0
  175. package/dist/lib/types/mediaFolder/index.d.ts +16 -0
  176. package/dist/lib/types/mediaFolder/index.d.ts.map +1 -0
  177. package/dist/lib/types/mediaFolder/index.js +1 -0
  178. package/dist/lib/types/pendingUpload/index.d.ts +38 -0
  179. package/dist/lib/types/pendingUpload/index.d.ts.map +1 -0
  180. package/dist/lib/types/pendingUpload/index.js +1 -0
  181. package/dist/lib/types/upload/index.d.ts +37 -0
  182. package/dist/lib/types/upload/index.d.ts.map +1 -0
  183. package/dist/lib/types/upload/index.js +1 -0
  184. package/dist/lib/utils/assetGradient/index.d.ts +8 -0
  185. package/dist/lib/utils/assetGradient/index.d.ts.map +1 -0
  186. package/dist/lib/utils/assetGradient/index.js +16 -0
  187. package/dist/lib/utils/assetHandlers/index.d.ts +11 -0
  188. package/dist/lib/utils/assetHandlers/index.d.ts.map +1 -0
  189. package/dist/lib/utils/assetHandlers/index.js +17 -0
  190. package/dist/lib/utils/folderContents/index.d.ts +17 -0
  191. package/dist/lib/utils/folderContents/index.d.ts.map +1 -0
  192. package/dist/lib/utils/folderContents/index.js +35 -0
  193. package/dist/lib/utils/folderTree/index.d.ts +13 -0
  194. package/dist/lib/utils/folderTree/index.d.ts.map +1 -0
  195. package/dist/lib/utils/folderTree/index.js +15 -0
  196. package/dist/lib/utils/formatBytes/index.d.ts +6 -0
  197. package/dist/lib/utils/formatBytes/index.d.ts.map +1 -0
  198. package/dist/lib/utils/formatBytes/index.js +17 -0
  199. package/dist/lib/utils/formatDuration/index.d.ts +6 -0
  200. package/dist/lib/utils/formatDuration/index.d.ts.map +1 -0
  201. package/dist/lib/utils/formatDuration/index.js +13 -0
  202. package/dist/lib/utils/kindFromMime/index.d.ts +10 -0
  203. package/dist/lib/utils/kindFromMime/index.d.ts.map +1 -0
  204. package/dist/lib/utils/kindFromMime/index.js +27 -0
  205. package/dist/lib/utils/mediaAccept/index.d.ts +30 -0
  206. package/dist/lib/utils/mediaAccept/index.d.ts.map +1 -0
  207. package/dist/lib/utils/mediaAccept/index.js +63 -0
  208. package/dist/lib/utils/mediaPlugin/index.d.ts +15 -0
  209. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -0
  210. package/dist/lib/utils/mediaPlugin/index.js +177 -0
  211. package/dist/lib/utils/toWysiwygEmbed/index.d.ts +23 -0
  212. package/dist/lib/utils/toWysiwygEmbed/index.d.ts.map +1 -0
  213. package/dist/lib/utils/toWysiwygEmbed/index.js +44 -0
  214. package/package.json +50 -0
@@ -0,0 +1,83 @@
1
+ import { KIND_FILTER_ALL, type MediaKind, type MediaSort } from '../../constants';
2
+ import type { MediaFolder } from '../../types/mediaFolder';
3
+ /** The selected kind filter — a {@link MediaKind} or the "all" sentinel. */
4
+ export type KindFilter = MediaKind | typeof KIND_FILTER_ALL;
5
+ /** Per-surface tuning for {@link useMediaLibrary}. */
6
+ export type MediaLibraryOptions = {
7
+ /**
8
+ * Assets per request. The library page lets the user choose; a surface with
9
+ * no pager of its own (the picker dialog) passes a large one and browses by
10
+ * searching instead.
11
+ */
12
+ pageSize?: number;
13
+ };
14
+ /**
15
+ * The Media Library's data layer and every action the UI can take. Folders and
16
+ * the open folder's assets are fetched from `@orthacms/media-server` via the
17
+ * gateway (TanStack Query); mutations post to the API and invalidate the cache.
18
+ * View/search/sort/filter/selection/navigation stay local UI state. The returned
19
+ * shape mirrors the former mock store exactly, so no component changed.
20
+ *
21
+ * @param enabled - gates the reads (false until the caller confirms `media:read`).
22
+ */
23
+ export declare function useMediaLibrary(enabled?: boolean, { pageSize: initialPageSize }?: MediaLibraryOptions): {
24
+ folders: MediaFolder[];
25
+ assets: import("../../types/mediaAsset").MediaAsset[];
26
+ currentFolderId: string;
27
+ currentFolder: MediaFolder | null;
28
+ breadcrumbs: MediaFolder[];
29
+ childFolders: MediaFolder[];
30
+ visibleAssets: import("../../types/mediaAsset").MediaAsset[];
31
+ folderCounts: Map<string, number>;
32
+ total: number;
33
+ page: number;
34
+ pageCount: number;
35
+ pageSize: number;
36
+ setPage: (next: number) => void;
37
+ setPageSize: (next: number) => void;
38
+ isLoading: boolean;
39
+ /**
40
+ * True while the grid on screen is not yet the answer to the controls
41
+ * as they now read — the debounce window, then the request.
42
+ *
43
+ * `isFetching` alone is a poor cue for a search box: it only goes true
44
+ * *after* the debounce commits, so for the first 300ms of typing
45
+ * nothing visible happens at all and the old results sit there looking
46
+ * like the new ones. Or-ing in the un-committed keystrokes is what
47
+ * makes the grid say "not this, yet" from the first letter.
48
+ */
49
+ isRefreshing: boolean;
50
+ isError: boolean;
51
+ reload: () => void;
52
+ selectedIds: Set<string>;
53
+ selectedAssets: import("../../types/mediaAsset").MediaAsset[];
54
+ toggleSelect: (id: string) => void;
55
+ clearSelection: () => void;
56
+ detailAsset: import("../../types/mediaAsset").MediaAsset | null;
57
+ openDetail: (id: string) => void;
58
+ closeDetail: () => void;
59
+ search: string;
60
+ setSearch: (next: string) => void;
61
+ sort: MediaSort;
62
+ setSort: (next: MediaSort) => void;
63
+ kindFilter: KindFilter;
64
+ setKindFilter: (next: KindFilter) => void;
65
+ navigateTo: (folderId: string) => void;
66
+ createFolder: (name: string, parentId?: string) => Promise<boolean>;
67
+ renameFolder: (id: string, name: string) => Promise<boolean>;
68
+ renameAsset: (id: string, name: string) => Promise<boolean>;
69
+ setAssetAlt: (id: string, alt: string) => Promise<boolean>;
70
+ deleteAssets: (ids: string[]) => Promise<boolean>;
71
+ deleteFolder: (id: string) => Promise<boolean>;
72
+ duplicateAssets: (ids: string[]) => Promise<boolean>;
73
+ moveAssets: (ids: string[], folderId: string) => Promise<boolean>;
74
+ uploadFiles: (uploads: import("../../infrastructure/mediaGateway").StagedUpload[]) => void;
75
+ uploadItems: import("../../types/upload").UploadItem[];
76
+ uploadSummary: import("../../types/upload").UploadSummary;
77
+ retryUpload: (id: string) => void;
78
+ cancelUpload: (id: string) => void;
79
+ dismissUploads: () => void;
80
+ };
81
+ /** The full store + actions returned by {@link useMediaLibrary}. */
82
+ export type MediaLibraryStore = ReturnType<typeof useMediaLibrary>;
83
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/hooks/useMediaLibrary/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAEH,eAAe,EAIf,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAM3D,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,eAAe,CAAC;AAE5D,sDAAsD;AACtD,MAAM,MAAM,mBAAmB,GAAG;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA0BF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,OAAO,UAAO,EACd,EACI,QAAQ,EAAE,eAA0C,EACvD,GAAE,mBAAwB;;;;;;;;;;;;;oBA8JhB,MAAM;wBASN,MAAM;;IA6Lb;;;;;;;;;OASG;;;;;;uBAvI+B,MAAM;;;qBASR,MAAM;;;sBA1D/B,MAAM;;oBAiBN,SAAS;;0BATT,UAAU;2BAmBN,MAAM;yBAiFV,MAAM,aAAY,MAAM;uBAK1B,MAAM,QAAQ,MAAM;sBASpB,MAAM,QAAQ,MAAM;sBAKpB,MAAM,OAAO,MAAM;wBAclB,MAAM,EAAE;uBAvBT,MAAM;2BAmBL,MAAM,EAAE;sBALR,MAAM,EAAE,YAAY,MAAM;;;;;;;EA4FvC;AAED,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC"}
@@ -0,0 +1,341 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { toast } from '@orthacms/design-system';
5
+ import { useDebouncedValue } from '@orthacms/utils-admin';
6
+ import { useCurrentWorkspace } from '@orthacms/workspaces-admin';
7
+ import { DEFAULT_ASSETS_PAGE_SIZE, KIND_FILTER_ALL, MEDIA_SORT, ROOT_FOLDER_ID, SEARCH_DEBOUNCE_MS } from '../../constants';
8
+ import { mediaKeys } from '../../infrastructure/mediaKeys';
9
+ import { httpMediaGateway } from '../../infrastructure/httpMediaGateway';
10
+ import { apiMessage } from '../../infrastructure/apiMessage';
11
+ import { useUploadQueue } from '../useUploadQueue';
12
+ /**
13
+ * Resolves a mutation to whether it **succeeded**, so a caller can hold its
14
+ * confirmation until the server has agreed.
15
+ *
16
+ * Every action below returns this. Firing a success toast at dispatch time is
17
+ * what produced "Renamed to “a/b.txt”" beside "Request failed with status code
18
+ * 400" on the same screen: the rejection is already handled by `onError`, so
19
+ * the failure branch here only has to swallow it.
20
+ */
21
+ function settle(promise) {
22
+ return promise.then(() => true, () => false);
23
+ }
24
+ /** Intl descriptor for a failed media mutation. */
25
+ const messages = defineMessages({
26
+ error: {
27
+ id: 'media.error.mutation',
28
+ defaultMessage: 'Something went wrong. Please try again.'
29
+ }
30
+ });
31
+ /**
32
+ * The Media Library's data layer and every action the UI can take. Folders and
33
+ * the open folder's assets are fetched from `@orthacms/media-server` via the
34
+ * gateway (TanStack Query); mutations post to the API and invalidate the cache.
35
+ * View/search/sort/filter/selection/navigation stay local UI state. The returned
36
+ * shape mirrors the former mock store exactly, so no component changed.
37
+ *
38
+ * @param enabled - gates the reads (false until the caller confirms `media:read`).
39
+ */
40
+ export function useMediaLibrary(enabled = true, { pageSize: initialPageSize = DEFAULT_ASSETS_PAGE_SIZE } = {}) {
41
+ const intl = useIntl();
42
+ const workspaceId = useCurrentWorkspace().id;
43
+ const queryClient = useQueryClient();
44
+ const [currentFolderId, setCurrentFolderId] = useState(ROOT_FOLDER_ID);
45
+ const [selectedIds, setSelectedIds] = useState(() => new Set());
46
+ const [search, setSearchState] = useState('');
47
+ const [sort, setSortState] = useState(MEDIA_SORT.Newest);
48
+ const [kindFilter, setKindFilterState] = useState(KIND_FILTER_ALL);
49
+ const [detailAssetId, setDetailAssetId] = useState(null);
50
+ const [page, setPageState] = useState(1);
51
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
52
+ // The box updates on every keystroke; the request waits for a pause. Search
53
+ // is a round trip now, so without this every letter typed is a query — and
54
+ // the results would flicker through the prefixes on the way to the word.
55
+ const debouncedSearch = useDebouncedValue(search, SEARCH_DEBOUNCE_MS);
56
+ const foldersQuery = useQuery({
57
+ queryKey: mediaKeys.folders(workspaceId),
58
+ queryFn: () => httpMediaGateway.listFolders(),
59
+ enabled
60
+ });
61
+ // Everything the server filters, orders and pages by, in one object — used
62
+ // as both the request and the cache key, so the two cannot disagree.
63
+ const listParams = useMemo(() => ({
64
+ search: debouncedSearch.trim(),
65
+ kind: kindFilter,
66
+ sort,
67
+ page,
68
+ pageSize
69
+ }), [debouncedSearch, kindFilter, sort, page, pageSize]);
70
+ const assetsQuery = useQuery({
71
+ queryKey: mediaKeys.assets(workspaceId, currentFolderId, listParams),
72
+ queryFn: () => httpMediaGateway.listAssets({
73
+ folderId: currentFolderId,
74
+ ...listParams
75
+ }),
76
+ // Paging without this blanks the grid between pages, which on a fast
77
+ // connection reads as a flicker and on a slow one as a broken folder.
78
+ placeholderData: keepPreviousData,
79
+ enabled
80
+ });
81
+ const folders = useMemo(() => foldersQuery.data?.folders ?? [], [foldersQuery.data]);
82
+ /** The assets on the open page — already searched, filtered and sorted. */
83
+ const assets = useMemo(() => assetsQuery.data?.items ?? [], [assetsQuery.data]);
84
+ /** How many assets match across the whole folder, not just this page. */
85
+ const total = assetsQuery.data?.total ?? 0;
86
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
87
+ const folderCounts = useMemo(() => foldersQuery.data?.folderCounts ?? new Map(), [foldersQuery.data]);
88
+ /** Refetch folders + the visible assets after any mutation. */
89
+ const invalidate = useCallback(() => {
90
+ queryClient.invalidateQueries({ queryKey: mediaKeys.all(workspaceId) });
91
+ }, [queryClient, workspaceId]);
92
+ /**
93
+ * Surfaces a mutation failure and resyncs from the server. The API's own
94
+ * sentence wins over our generic copy — `apiMessage` reads it off the
95
+ * response body, because `ApiError.message` is only the transport's
96
+ * "Request failed with status code 400".
97
+ */
98
+ const onError = useCallback((error) => {
99
+ toast.error(apiMessage(error) ?? intl.formatMessage(messages.error));
100
+ invalidate();
101
+ }, [intl, invalidate]);
102
+ /** The open folder record, or `null` at the synthetic root. */
103
+ const currentFolder = useMemo(() => folders.find((f) => f.id === currentFolderId) ?? null, [folders, currentFolderId]);
104
+ /** Ancestor chain (root → current), used by the breadcrumbs. */
105
+ const breadcrumbs = useMemo(() => {
106
+ const chain = [];
107
+ let cursor = currentFolder;
108
+ while (cursor) {
109
+ chain.unshift(cursor);
110
+ const parentId = cursor.parentId;
111
+ cursor =
112
+ parentId === ROOT_FOLDER_ID
113
+ ? null
114
+ : (folders.find((f) => f.id === parentId) ?? null);
115
+ }
116
+ return chain;
117
+ }, [currentFolder, folders]);
118
+ /** Folders directly inside the open folder. */
119
+ const childFolders = useMemo(() => folders
120
+ .filter((f) => f.parentId === currentFolderId)
121
+ .sort((a, b) => a.name.localeCompare(b.name)), [folders, currentFolderId]);
122
+ /**
123
+ * The assets to draw. The server has already searched, filtered, sorted and
124
+ * paged them, so this is the page as it came back.
125
+ *
126
+ * It used to be a `filter` + `sort` over one fixed page of 100, which is
127
+ * what made "oldest first" return the newest hundred in ascending order and
128
+ * let a search miss a file that plainly existed. Neither is fixable in the
129
+ * browser: the answer depends on rows the browser was never sent.
130
+ */
131
+ const visibleAssets = assets;
132
+ /** The asset backing the detail drawer, if open. */
133
+ const detailAsset = useMemo(() => assets.find((a) => a.id === detailAssetId) ?? null, [assets, detailAssetId]);
134
+ /** Currently selected assets (within the open folder). */
135
+ const selectedAssets = useMemo(() => assets.filter((a) => selectedIds.has(a.id)), [assets, selectedIds]);
136
+ const clearSelection = useCallback(() => setSelectedIds(new Set()), []);
137
+ /**
138
+ * Moves to another page, and drops the selection with it.
139
+ *
140
+ * A selection that outlived the page it was made on would be invisible and
141
+ * still armed: the bar counts only what is on screen (`selectedAssets` is
142
+ * derived from the loaded page), so ids left behind on page 1 would vanish
143
+ * from the count on page 2 and silently return later — and Delete would
144
+ * take files the user could not see. Every control that changes *which*
145
+ * assets are listed clears it, for the same reason.
146
+ */
147
+ const setPage = useCallback((next) => {
148
+ setPageState(next);
149
+ clearSelection();
150
+ }, [clearSelection]);
151
+ /** Re-pages the listing from the top; a new size makes old page numbers meaningless. */
152
+ const setPageSize = useCallback((next) => {
153
+ setPageSizeState(next);
154
+ setPageState(1);
155
+ clearSelection();
156
+ }, [clearSelection]);
157
+ /**
158
+ * Narrowing the list invalidates the page you were on — page 4 of an
159
+ * unfiltered folder is usually past the end of the filtered one, and the
160
+ * user would land on an empty grid having just typed a search that matches.
161
+ */
162
+ const setSearch = useCallback((next) => {
163
+ setSearchState(next);
164
+ setPageState(1);
165
+ clearSelection();
166
+ }, [clearSelection]);
167
+ const setKindFilter = useCallback((next) => {
168
+ setKindFilterState(next);
169
+ setPageState(1);
170
+ clearSelection();
171
+ }, [clearSelection]);
172
+ /** Re-orders the whole result set, so page 2 is a different two dozen assets. */
173
+ const setSort = useCallback((next) => {
174
+ setSortState(next);
175
+ setPageState(1);
176
+ clearSelection();
177
+ }, [clearSelection]);
178
+ /** Navigate into a folder (or root), back to page 1, dropping the selection. */
179
+ const navigateTo = useCallback((folderId) => {
180
+ setCurrentFolderId(folderId);
181
+ setPageState(1);
182
+ clearSelection();
183
+ }, [clearSelection]);
184
+ /**
185
+ * Pulls the page back into range when the result set shrinks under it.
186
+ *
187
+ * Deleting the last three assets on page 5, or a search narrowing to two
188
+ * pages, otherwise leaves the pager on a page the server answers with an
189
+ * empty `items` — a folder that reads as empty while its own header says it
190
+ * holds ninety files. Guarded on `isFetching` so it settles on the response
191
+ * rather than on the stale `keepPreviousData` total mid-flight.
192
+ */
193
+ useEffect(() => {
194
+ if (assetsQuery.isFetching)
195
+ return;
196
+ if (page > pageCount)
197
+ setPageState(pageCount);
198
+ }, [page, pageCount, assetsQuery.isFetching]);
199
+ const toggleSelect = useCallback((id) => {
200
+ setSelectedIds((prev) => {
201
+ const next = new Set(prev);
202
+ if (next.has(id))
203
+ next.delete(id);
204
+ else
205
+ next.add(id);
206
+ return next;
207
+ });
208
+ }, []);
209
+ const openDetail = useCallback((id) => setDetailAssetId(id), []);
210
+ const closeDetail = useCallback(() => setDetailAssetId(null), []);
211
+ // --- Mutations (post to the API, then invalidate the cache) ---------------
212
+ const createFolderM = useMutation({
213
+ mutationFn: httpMediaGateway.createFolder,
214
+ onSuccess: invalidate,
215
+ onError
216
+ });
217
+ const renameFolderM = useMutation({
218
+ mutationFn: httpMediaGateway.renameFolder,
219
+ onSuccess: invalidate,
220
+ onError
221
+ });
222
+ const deleteFolderM = useMutation({
223
+ mutationFn: httpMediaGateway.deleteFolder,
224
+ onSuccess: invalidate,
225
+ onError
226
+ });
227
+ const renameAssetM = useMutation({
228
+ mutationFn: httpMediaGateway.renameAsset,
229
+ onSuccess: invalidate,
230
+ onError
231
+ });
232
+ const moveAssetsM = useMutation({
233
+ mutationFn: httpMediaGateway.moveAssets,
234
+ onSuccess: invalidate,
235
+ onError
236
+ });
237
+ const duplicateAssetsM = useMutation({
238
+ mutationFn: httpMediaGateway.duplicateAssets,
239
+ onSuccess: invalidate,
240
+ onError
241
+ });
242
+ const deleteAssetsM = useMutation({
243
+ mutationFn: httpMediaGateway.deleteAssets,
244
+ onSuccess: invalidate,
245
+ onError
246
+ });
247
+ const updateAssetM = useMutation({
248
+ mutationFn: httpMediaGateway.updateAsset,
249
+ onSuccess: invalidate,
250
+ onError
251
+ });
252
+ // Uploads don't go through `useMutation`: the banner needs per-file progress
253
+ // and independent per-file failure, which a single mutation can't express.
254
+ const uploads = useUploadQueue(currentFolderId, invalidate);
255
+ const createFolder = useCallback((name, parentId = currentFolderId) => settle(createFolderM.mutateAsync({ name, parentId })), [createFolderM, currentFolderId]);
256
+ const renameFolder = useCallback((id, name) => settle(renameFolderM.mutateAsync({ id, name })), [renameFolderM]);
257
+ const deleteFolder = useCallback((id) => settle(deleteFolderM.mutateAsync(id)), [deleteFolderM]);
258
+ const renameAsset = useCallback((id, name) => settle(renameAssetM.mutateAsync({ id, name })), [renameAssetM]);
259
+ const setAssetAlt = useCallback((id, alt) => settle(updateAssetM.mutateAsync({ id, alt })), [updateAssetM]);
260
+ const moveAssets = useCallback((ids, folderId) => settle(moveAssetsM.mutateAsync({ ids, folderId })), [moveAssetsM]);
261
+ const duplicateAssets = useCallback((ids) => settle(duplicateAssetsM.mutateAsync(ids)), [duplicateAssetsM]);
262
+ const deleteAssets = useCallback((ids) => {
263
+ const done = settle(deleteAssetsM.mutateAsync(ids));
264
+ // Drop them from the selection; the drawer closes on its own once the
265
+ // refetch removes the row (detailAsset derives to null).
266
+ setSelectedIds((prev) => {
267
+ const next = new Set(prev);
268
+ ids.forEach((id) => next.delete(id));
269
+ return next;
270
+ });
271
+ return done;
272
+ }, [deleteAssetsM]);
273
+ const uploadFiles = uploads.enqueue;
274
+ return {
275
+ // data
276
+ folders,
277
+ assets,
278
+ currentFolderId,
279
+ currentFolder,
280
+ breadcrumbs,
281
+ childFolders,
282
+ visibleAssets,
283
+ folderCounts,
284
+ // paging
285
+ total,
286
+ page,
287
+ pageCount,
288
+ pageSize,
289
+ setPage,
290
+ setPageSize,
291
+ // load state
292
+ isLoading: foldersQuery.isPending || assetsQuery.isPending,
293
+ /**
294
+ * True while the grid on screen is not yet the answer to the controls
295
+ * as they now read — the debounce window, then the request.
296
+ *
297
+ * `isFetching` alone is a poor cue for a search box: it only goes true
298
+ * *after* the debounce commits, so for the first 300ms of typing
299
+ * nothing visible happens at all and the old results sit there looking
300
+ * like the new ones. Or-ing in the un-committed keystrokes is what
301
+ * makes the grid say "not this, yet" from the first letter.
302
+ */
303
+ isRefreshing: assetsQuery.isFetching || search.trim() !== debouncedSearch.trim(),
304
+ isError: foldersQuery.isError || assetsQuery.isError,
305
+ reload: invalidate,
306
+ // selection
307
+ selectedIds,
308
+ selectedAssets,
309
+ toggleSelect,
310
+ clearSelection,
311
+ // detail drawer
312
+ detailAsset,
313
+ openDetail,
314
+ closeDetail,
315
+ // browse controls
316
+ search,
317
+ setSearch,
318
+ sort,
319
+ setSort,
320
+ kindFilter,
321
+ setKindFilter,
322
+ // navigation
323
+ navigateTo,
324
+ // mutations
325
+ createFolder,
326
+ renameFolder,
327
+ renameAsset,
328
+ setAssetAlt,
329
+ deleteAssets,
330
+ deleteFolder,
331
+ duplicateAssets,
332
+ moveAssets,
333
+ uploadFiles,
334
+ // upload queue (progress banner)
335
+ uploadItems: uploads.items,
336
+ uploadSummary: uploads.summary,
337
+ retryUpload: uploads.retry,
338
+ cancelUpload: uploads.cancel,
339
+ dismissUploads: uploads.dismissSettled
340
+ };
341
+ }
@@ -0,0 +1,26 @@
1
+ import type { EntryPresave } from '@orthacms/content-admin';
2
+ /** The id the media plugin's presave contribution is registered (and keyed) under. */
3
+ export declare const MEDIA_PRESAVE_ID = "media.entry.presave";
4
+ /**
5
+ * The media plugin's contribution to the entry **save**: files chosen on a media
6
+ * field are staged here and uploaded only when the record is saved or published.
7
+ *
8
+ * Why deferred — an upload is a write. Uploading the moment a file is picked
9
+ * fills the Media Library with assets for a record the user then abandons, and a
10
+ * media field is the one control where "I picked something" and "I saved" used
11
+ * to mean two different write moments. Staging keeps the whole record one commit.
12
+ *
13
+ * A staged file gets a **placeholder uuid**, which is what the form value holds
14
+ * ({@link PendingUpload}) — so client validation, the Changed badge, and the
15
+ * publish gate treat it exactly like an attached asset. `commit` (run by
16
+ * `ContentEntryView` before the write) uploads every staged file the values
17
+ * still reference, `UPLOAD_CONCURRENCY` at a time, and returns the values with
18
+ * the placeholders swapped for real asset ids. One failed file aborts the save
19
+ * with a toast naming it; the files that did upload remember their asset id, so
20
+ * a retry doesn't upload them twice.
21
+ *
22
+ * Mounted once per entry view through `ENTRY_PRESAVE_SLOT`, so the staging (and
23
+ * its object-URL previews) survive the Media tab unmounting on a tab switch.
24
+ */
25
+ export declare function usePendingMediaUploads(): EntryPresave;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/hooks/usePendingMediaUploads/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAmB5D,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AA+DtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,IAAI,YAAY,CAmJrD"}
@@ -0,0 +1,217 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { useQueryClient } from '@tanstack/react-query';
4
+ import { toast } from '@orthacms/design-system';
5
+ import { useCurrentWorkspace } from '@orthacms/workspaces-admin';
6
+ import { mediaValueIds, toMediaValueRef } from '@orthacms/content-domain';
7
+ import { ROOT_FOLDER_ID, UPLOAD_CONCURRENCY } from '../../constants';
8
+ import { httpMediaGateway } from '../../infrastructure/httpMediaGateway';
9
+ import { mediaKeys } from '../../infrastructure/mediaKeys';
10
+ /** Intl descriptors for the save-time upload step, co-located. */
11
+ const messages = defineMessages({
12
+ failed: {
13
+ id: 'media.pending.failed',
14
+ defaultMessage: 'Could not upload {name} — the record was not saved. Try again.'
15
+ }
16
+ });
17
+ /** The id the media plugin's presave contribution is registered (and keyed) under. */
18
+ export const MEDIA_PRESAVE_ID = 'media.entry.presave';
19
+ /** Everything staged that the values actually still reference, in no order. */
20
+ function referenced(values, pending) {
21
+ // Through the kernel: a media value is `{ id, alt?, decorative? }` as well
22
+ // as a bare id since `ORT-83`, and reading only strings meant a staged file
23
+ // attached to a field was no longer *referenced* by it — so the save
24
+ // uploaded nothing and wrote a record pointing at a placeholder.
25
+ const ids = new Set();
26
+ for (const value of Object.values(values))
27
+ for (const id of mediaValueIds(value))
28
+ ids.add(id);
29
+ return [...ids]
30
+ .map((id) => pending.get(id))
31
+ .filter((entry) => !!entry);
32
+ }
33
+ /** Swap every placeholder id in the values for the asset it resolved to. */
34
+ function resolveValues(values, resolved) {
35
+ if (resolved.size === 0)
36
+ return values;
37
+ const out = { ...values };
38
+ /**
39
+ * Swaps one value's placeholder for the real asset id, **keeping whatever
40
+ * else the value carries** — the alt text and the decorative flag an author
41
+ * typed against the staged file belong to the asset it became.
42
+ */
43
+ const swap = (item) => {
44
+ const ref = toMediaValueRef(item);
45
+ if (!ref)
46
+ return { value: item, changed: false };
47
+ const id = resolved.get(ref.id);
48
+ if (!id)
49
+ return { value: item, changed: false };
50
+ return {
51
+ // A bare id in, a bare id out: only widen the shape where the author
52
+ // actually said something, so an untouched attach keeps the wire
53
+ // form it had.
54
+ value: typeof item === 'string' ? id : { ...ref, id },
55
+ changed: true
56
+ };
57
+ };
58
+ for (const [name, value] of Object.entries(values)) {
59
+ if (Array.isArray(value)) {
60
+ let changed = false;
61
+ const next = value.map((item) => {
62
+ const swapped = swap(item);
63
+ if (swapped.changed)
64
+ changed = true;
65
+ return swapped.value;
66
+ });
67
+ if (changed)
68
+ out[name] = next;
69
+ }
70
+ else {
71
+ const swapped = swap(value);
72
+ if (swapped.changed)
73
+ out[name] = swapped.value;
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ /**
79
+ * The media plugin's contribution to the entry **save**: files chosen on a media
80
+ * field are staged here and uploaded only when the record is saved or published.
81
+ *
82
+ * Why deferred — an upload is a write. Uploading the moment a file is picked
83
+ * fills the Media Library with assets for a record the user then abandons, and a
84
+ * media field is the one control where "I picked something" and "I saved" used
85
+ * to mean two different write moments. Staging keeps the whole record one commit.
86
+ *
87
+ * A staged file gets a **placeholder uuid**, which is what the form value holds
88
+ * ({@link PendingUpload}) — so client validation, the Changed badge, and the
89
+ * publish gate treat it exactly like an attached asset. `commit` (run by
90
+ * `ContentEntryView` before the write) uploads every staged file the values
91
+ * still reference, `UPLOAD_CONCURRENCY` at a time, and returns the values with
92
+ * the placeholders swapped for real asset ids. One failed file aborts the save
93
+ * with a toast naming it; the files that did upload remember their asset id, so
94
+ * a retry doesn't upload them twice.
95
+ *
96
+ * Mounted once per entry view through `ENTRY_PRESAVE_SLOT`, so the staging (and
97
+ * its object-URL previews) survive the Media tab unmounting on a tab switch.
98
+ */
99
+ export function usePendingMediaUploads() {
100
+ const intl = useIntl();
101
+ const workspace = useCurrentWorkspace();
102
+ const queryClient = useQueryClient();
103
+ const [pending, setPending] = useState(() => new Map());
104
+ // `commit` runs across awaits and after `settle`; it reads the map through a
105
+ // ref so it never works from a render-stale copy.
106
+ const pendingRef = useRef(pending);
107
+ pendingRef.current = pending;
108
+ const write = useCallback((next) => {
109
+ pendingRef.current = next;
110
+ setPending(next);
111
+ }, []);
112
+ const stage = useCallback((files) => {
113
+ const next = new Map(pendingRef.current);
114
+ const ids = files.map((file) => {
115
+ const id = crypto.randomUUID();
116
+ next.set(id, {
117
+ id,
118
+ file,
119
+ previewUrl: file.type.startsWith('image/')
120
+ ? URL.createObjectURL(file)
121
+ : undefined
122
+ });
123
+ return id;
124
+ });
125
+ write(next);
126
+ return ids;
127
+ }, [write]);
128
+ const drop = useCallback((id) => {
129
+ const entry = pendingRef.current.get(id);
130
+ if (!entry)
131
+ return;
132
+ if (entry.previewUrl)
133
+ URL.revokeObjectURL(entry.previewUrl);
134
+ const next = new Map(pendingRef.current);
135
+ next.delete(id);
136
+ write(next);
137
+ }, [write]);
138
+ const commit = useCallback(async ({ values }) => {
139
+ const queue = referenced(values, pendingRef.current);
140
+ if (queue.length === 0)
141
+ return values;
142
+ const resolved = new Map();
143
+ const uploaded = new Map();
144
+ const failed = [];
145
+ // A small pool of workers over one shared cursor — the same shape as
146
+ // the library's upload queue, minus the progress bookkeeping (the
147
+ // save's busy cover is the status surface here).
148
+ let cursor = 0;
149
+ const worker = async () => {
150
+ for (;;) {
151
+ const entry = queue[cursor++];
152
+ if (!entry)
153
+ return;
154
+ if (entry.uploadedId) {
155
+ resolved.set(entry.id, entry.uploadedId);
156
+ continue;
157
+ }
158
+ try {
159
+ const asset = await httpMediaGateway.uploadFile(ROOT_FOLDER_ID, entry.file);
160
+ resolved.set(entry.id, asset.id);
161
+ uploaded.set(entry.id, asset);
162
+ }
163
+ catch {
164
+ failed.push(entry);
165
+ }
166
+ }
167
+ };
168
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, queue.length) }, worker));
169
+ if (uploaded.size > 0) {
170
+ // The library gained assets even if the save is about to fail.
171
+ queryClient.invalidateQueries({
172
+ queryKey: mediaKeys.all(workspace.id)
173
+ });
174
+ // Remember each asset id against its placeholder, so a retry
175
+ // after a failed write attaches it instead of re-uploading.
176
+ const next = new Map(pendingRef.current);
177
+ for (const [placeholder, asset] of uploaded) {
178
+ const entry = next.get(placeholder);
179
+ if (entry)
180
+ next.set(placeholder, {
181
+ ...entry,
182
+ uploadedId: asset.id
183
+ });
184
+ }
185
+ write(next);
186
+ }
187
+ if (failed.length > 0) {
188
+ for (const entry of failed) {
189
+ toast.error(intl.formatMessage(messages.failed, {
190
+ name: entry.file.name
191
+ }));
192
+ }
193
+ throw new Error('media upload failed');
194
+ }
195
+ return resolveValues(values, resolved);
196
+ }, [intl, queryClient, workspace.id, write]);
197
+ /** The write landed — the saved record carries the ids, so drop the staging. */
198
+ const settle = useCallback(() => {
199
+ for (const entry of pendingRef.current.values()) {
200
+ if (entry.previewUrl)
201
+ URL.revokeObjectURL(entry.previewUrl);
202
+ }
203
+ write(new Map());
204
+ }, [write]);
205
+ // Leaving the editor with files still staged would otherwise leak every
206
+ // preview blob for the tab's life.
207
+ useEffect(() => () => {
208
+ // Read through the ref *inside* the cleanup — the map at mount time
209
+ // is always the empty one.
210
+ for (const entry of pendingRef.current.values()) {
211
+ if (entry.previewUrl)
212
+ URL.revokeObjectURL(entry.previewUrl);
213
+ }
214
+ }, []);
215
+ const handle = { pending, stage, drop };
216
+ return { commit, settle, handle };
217
+ }