@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/components/AssetActionsMenu/index.d.ts +37 -0
- package/dist/lib/components/AssetActionsMenu/index.d.ts.map +1 -0
- package/dist/lib/components/AssetActionsMenu/index.js +57 -0
- package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts +11 -0
- package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailDrawer/MetaRow/index.js +9 -0
- package/dist/lib/components/AssetDetailDrawer/index.d.ts +28 -0
- package/dist/lib/components/AssetDetailDrawer/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailDrawer/index.js +93 -0
- package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts +28 -0
- package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts.map +1 -0
- package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.js +52 -0
- package/dist/lib/components/EntryMediaTab/index.d.ts +11 -0
- package/dist/lib/components/EntryMediaTab/index.d.ts.map +1 -0
- package/dist/lib/components/EntryMediaTab/index.js +31 -0
- package/dist/lib/components/FolderActionsMenu/index.d.ts +21 -0
- package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -0
- package/dist/lib/components/FolderActionsMenu/index.js +25 -0
- package/dist/lib/components/MediaAltTextWidget/index.d.ts +9 -0
- package/dist/lib/components/MediaAltTextWidget/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAltTextWidget/index.js +66 -0
- package/dist/lib/components/MediaBreadcrumbs/index.d.ts +11 -0
- package/dist/lib/components/MediaBreadcrumbs/index.d.ts.map +1 -0
- package/dist/lib/components/MediaEmptyState/index.d.ts +13 -0
- package/dist/lib/components/MediaEmptyState/index.d.ts.map +1 -0
- package/dist/lib/components/MediaEmptyState/index.js +38 -0
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts +34 -0
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +117 -0
- package/dist/lib/components/MediaFieldControl/index.d.ts +61 -0
- package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFieldControl/index.js +422 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +37 -0
- package/dist/lib/components/MediaFoldersNav/index.d.ts +22 -0
- package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFoldersNav/index.js +40 -0
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +18 -0
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -0
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +29 -0
- package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts +16 -0
- package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts.map +1 -0
- package/dist/lib/components/MediaGrid/MediaFolderCard/index.js +25 -0
- package/dist/lib/components/MediaGrid/index.d.ts +24 -0
- package/dist/lib/components/MediaGrid/index.d.ts.map +1 -0
- package/dist/lib/components/MediaGrid/index.js +20 -0
- package/dist/lib/components/MediaKindBadge/index.d.ts +10 -0
- package/dist/lib/components/MediaKindBadge/index.d.ts.map +1 -0
- package/dist/lib/components/MediaKindBadge/index.js +30 -0
- package/dist/lib/components/MediaKindIcon/index.d.ts +11 -0
- package/dist/lib/components/MediaKindIcon/index.d.ts.map +1 -0
- package/dist/lib/components/MediaKindIcon/index.js +20 -0
- package/dist/lib/components/MediaLibrarySkeleton/index.d.ts +18 -0
- package/dist/lib/components/MediaLibrarySkeleton/index.d.ts.map +1 -0
- package/dist/lib/components/MediaLibrarySkeleton/index.js +43 -0
- package/dist/lib/components/MediaPagination/index.d.ts +26 -0
- package/dist/lib/components/MediaPagination/index.d.ts.map +1 -0
- package/dist/lib/components/MediaPagination/index.js +52 -0
- package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts +17 -0
- package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts.map +1 -0
- package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.js +43 -0
- package/dist/lib/components/MediaPickerDialog/index.d.ts +21 -0
- package/dist/lib/components/MediaPickerDialog/index.d.ts.map +1 -0
- package/dist/lib/components/MediaPickerDialog/index.js +208 -0
- package/dist/lib/components/MediaSelectionBar/index.d.ts +18 -0
- package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -0
- package/dist/lib/components/MediaSelectionBar/index.js +26 -0
- package/dist/lib/components/MediaStorageStat/index.d.ts +9 -0
- package/dist/lib/components/MediaStorageStat/index.d.ts.map +1 -0
- package/dist/lib/components/MediaStorageStat/index.js +31 -0
- package/dist/lib/components/MediaStorageWidget/index.d.ts +10 -0
- package/dist/lib/components/MediaStorageWidget/index.d.ts.map +1 -0
- package/dist/lib/components/MediaStorageWidget/index.js +93 -0
- package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts +17 -0
- package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts.map +1 -0
- package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts +16 -0
- package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts.map +1 -0
- package/dist/lib/components/MediaTable/index.d.ts +26 -0
- package/dist/lib/components/MediaTable/index.d.ts.map +1 -0
- package/dist/lib/components/MediaThumbnail/index.d.ts +19 -0
- package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -0
- package/dist/lib/components/MediaThumbnail/index.js +26 -0
- package/dist/lib/components/MediaToolbar/index.d.ts +19 -0
- package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -0
- package/dist/lib/components/MediaToolbar/index.js +62 -0
- package/dist/lib/components/MediaTopBar/index.d.ts +20 -0
- package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -0
- package/dist/lib/components/MediaTopBar/index.js +35 -0
- package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts +14 -0
- package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts.map +1 -0
- package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.js +51 -0
- package/dist/lib/components/MediaUploadBanner/index.d.ts +24 -0
- package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -0
- package/dist/lib/components/MediaUploadBanner/index.js +99 -0
- package/dist/lib/components/MediaUploadsWidget/index.d.ts +7 -0
- package/dist/lib/components/MediaUploadsWidget/index.d.ts.map +1 -0
- package/dist/lib/components/MediaUploadsWidget/index.js +70 -0
- package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts +19 -0
- package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts.map +1 -0
- package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.js +10 -0
- package/dist/lib/components/MoveAssetsDialog/index.d.ts +18 -0
- package/dist/lib/components/MoveAssetsDialog/index.d.ts.map +1 -0
- package/dist/lib/components/MoveAssetsDialog/index.js +41 -0
- package/dist/lib/components/NewFolderDialog/index.d.ts +13 -0
- package/dist/lib/components/NewFolderDialog/index.d.ts.map +1 -0
- package/dist/lib/components/NewFolderDialog/index.js +45 -0
- package/dist/lib/components/RenameDialog/index.d.ts +13 -0
- package/dist/lib/components/RenameDialog/index.d.ts.map +1 -0
- package/dist/lib/components/RenameDialog/index.js +37 -0
- package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts +25 -0
- package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -0
- package/dist/lib/components/UploadDialog/StagedFileRow/index.js +82 -0
- package/dist/lib/components/UploadDialog/index.d.ts +50 -0
- package/dist/lib/components/UploadDialog/index.d.ts.map +1 -0
- package/dist/lib/components/UploadDialog/index.js +120 -0
- package/dist/lib/components/WysiwygLibrarySource/index.d.ts +14 -0
- package/dist/lib/components/WysiwygLibrarySource/index.d.ts.map +1 -0
- package/dist/lib/components/WysiwygLibrarySource/index.js +28 -0
- package/dist/lib/components/WysiwygUploadSource/index.d.ts +25 -0
- package/dist/lib/components/WysiwygUploadSource/index.d.ts.map +1 -0
- package/dist/lib/components/WysiwygUploadSource/index.js +102 -0
- package/dist/lib/constants/index.d.ts +82 -0
- package/dist/lib/constants/index.d.ts.map +1 -0
- package/dist/lib/constants/index.js +75 -0
- package/dist/lib/hooks/useMediaInsights/index.d.ts +32 -0
- package/dist/lib/hooks/useMediaInsights/index.d.ts.map +1 -0
- package/dist/lib/hooks/useMediaInsights/index.js +85 -0
- package/dist/lib/hooks/useMediaLibrary/index.d.ts +83 -0
- package/dist/lib/hooks/useMediaLibrary/index.d.ts.map +1 -0
- package/dist/lib/hooks/useMediaLibrary/index.js +341 -0
- package/dist/lib/hooks/usePendingMediaUploads/index.d.ts +26 -0
- package/dist/lib/hooks/usePendingMediaUploads/index.d.ts.map +1 -0
- package/dist/lib/hooks/usePendingMediaUploads/index.js +217 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts +33 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -0
- package/dist/lib/hooks/useUploadQueue/index.js +218 -0
- package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts +23 -0
- package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/apiMessage/index.d.ts +17 -0
- package/dist/lib/infrastructure/apiMessage/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/apiMessage/index.js +30 -0
- package/dist/lib/infrastructure/httpMediaGateway/index.d.ts +9 -0
- package/dist/lib/infrastructure/httpMediaGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/httpMediaGateway/index.js +157 -0
- package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts +8 -0
- package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/httpMediaInsightsGateway/index.js +35 -0
- package/dist/lib/infrastructure/mediaGateway/index.d.ts +120 -0
- package/dist/lib/infrastructure/mediaGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/mediaGateway/index.js +1 -0
- package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts +46 -0
- package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/mediaInsightsGateway/index.js +6 -0
- package/dist/lib/infrastructure/mediaKeys/index.d.ts +23 -0
- package/dist/lib/infrastructure/mediaKeys/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/mediaKeys/index.js +11 -0
- package/dist/lib/infrastructure/mediaMapper/index.d.ts +50 -0
- package/dist/lib/infrastructure/mediaMapper/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/mediaMapper/index.js +39 -0
- package/dist/lib/pages/MediaLibraryPage/index.d.ts +23 -0
- package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -0
- package/dist/lib/pages/MediaLibraryPage/index.js +425 -0
- package/dist/lib/types/mediaAsset/index.d.ts +44 -0
- package/dist/lib/types/mediaAsset/index.d.ts.map +1 -0
- package/dist/lib/types/mediaAsset/index.js +1 -0
- package/dist/lib/types/mediaFieldDisplay/index.d.ts +48 -0
- package/dist/lib/types/mediaFieldDisplay/index.d.ts.map +1 -0
- package/dist/lib/types/mediaFieldDisplay/index.js +1 -0
- package/dist/lib/types/mediaFolder/index.d.ts +16 -0
- package/dist/lib/types/mediaFolder/index.d.ts.map +1 -0
- package/dist/lib/types/mediaFolder/index.js +1 -0
- package/dist/lib/types/pendingUpload/index.d.ts +38 -0
- package/dist/lib/types/pendingUpload/index.d.ts.map +1 -0
- package/dist/lib/types/pendingUpload/index.js +1 -0
- package/dist/lib/types/upload/index.d.ts +37 -0
- package/dist/lib/types/upload/index.d.ts.map +1 -0
- package/dist/lib/types/upload/index.js +1 -0
- package/dist/lib/utils/assetGradient/index.d.ts +8 -0
- package/dist/lib/utils/assetGradient/index.d.ts.map +1 -0
- package/dist/lib/utils/assetGradient/index.js +16 -0
- package/dist/lib/utils/assetHandlers/index.d.ts +11 -0
- package/dist/lib/utils/assetHandlers/index.d.ts.map +1 -0
- package/dist/lib/utils/assetHandlers/index.js +17 -0
- package/dist/lib/utils/folderContents/index.d.ts +17 -0
- package/dist/lib/utils/folderContents/index.d.ts.map +1 -0
- package/dist/lib/utils/folderContents/index.js +35 -0
- package/dist/lib/utils/folderTree/index.d.ts +13 -0
- package/dist/lib/utils/folderTree/index.d.ts.map +1 -0
- package/dist/lib/utils/folderTree/index.js +15 -0
- package/dist/lib/utils/formatBytes/index.d.ts +6 -0
- package/dist/lib/utils/formatBytes/index.d.ts.map +1 -0
- package/dist/lib/utils/formatBytes/index.js +17 -0
- package/dist/lib/utils/formatDuration/index.d.ts +6 -0
- package/dist/lib/utils/formatDuration/index.d.ts.map +1 -0
- package/dist/lib/utils/formatDuration/index.js +13 -0
- package/dist/lib/utils/kindFromMime/index.d.ts +10 -0
- package/dist/lib/utils/kindFromMime/index.d.ts.map +1 -0
- package/dist/lib/utils/kindFromMime/index.js +27 -0
- package/dist/lib/utils/mediaAccept/index.d.ts +30 -0
- package/dist/lib/utils/mediaAccept/index.d.ts.map +1 -0
- package/dist/lib/utils/mediaAccept/index.js +63 -0
- package/dist/lib/utils/mediaPlugin/index.d.ts +15 -0
- package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -0
- package/dist/lib/utils/mediaPlugin/index.js +177 -0
- package/dist/lib/utils/toWysiwygEmbed/index.d.ts +23 -0
- package/dist/lib/utils/toWysiwygEmbed/index.d.ts.map +1 -0
- package/dist/lib/utils/toWysiwygEmbed/index.js +44 -0
- 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
|
+
}
|