@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,422 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { ImagePlus, UploadCloud } from 'lucide-react';
|
|
5
|
+
import { Button, cn, Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, toast } from '@orthacms/design-system';
|
|
6
|
+
import { useHasPermission } from '@orthacms/identity-admin';
|
|
7
|
+
import { toMediaValueRef } from '@orthacms/content-domain';
|
|
8
|
+
import { MEDIA_CREATE, MEDIA_KIND, MEDIA_READ } from '../../constants';
|
|
9
|
+
import { kindFromMime } from '../../utils/kindFromMime';
|
|
10
|
+
import { acceptsFile, describeAccept } from '../../utils/mediaAccept';
|
|
11
|
+
import { MediaPickerDialog } from '../MediaPickerDialog';
|
|
12
|
+
import { UploadDialog } from '../UploadDialog';
|
|
13
|
+
import { MediaFieldItem } from './MediaFieldItem';
|
|
14
|
+
const messages = defineMessages({
|
|
15
|
+
select: { id: 'media.field.select', defaultMessage: 'Select from library' },
|
|
16
|
+
add: { id: 'media.field.add', defaultMessage: 'Add from library' },
|
|
17
|
+
upload: { id: 'media.field.upload', defaultMessage: 'Upload' },
|
|
18
|
+
replace: { id: 'media.field.replace', defaultMessage: 'Replace' },
|
|
19
|
+
attach: { id: 'media.field.attach', defaultMessage: 'Attach files' },
|
|
20
|
+
uploadTo: {
|
|
21
|
+
id: 'media.field.uploadTo',
|
|
22
|
+
defaultMessage: 'Files are attached now and uploaded to {location} when you save the record.'
|
|
23
|
+
},
|
|
24
|
+
stagedCount: {
|
|
25
|
+
id: 'media.field.stagedCount',
|
|
26
|
+
defaultMessage: '{count, plural, one {# file uploads on save} other {# files upload on save}}'
|
|
27
|
+
},
|
|
28
|
+
// No size is quoted: the cap is server config the admin cannot read, and
|
|
29
|
+
// the old "Up to 250 MB each" was five times the shipped 50 MB default.
|
|
30
|
+
uploadHintAccept: {
|
|
31
|
+
id: 'media.field.uploadHintAccept',
|
|
32
|
+
defaultMessage: 'This field accepts {what}.'
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Names the panel so its drop affordance isn't sighted-mouse-only. The
|
|
36
|
+
* Upload button remains the keyboard route; this just says the drop exists.
|
|
37
|
+
*/
|
|
38
|
+
dropZone: {
|
|
39
|
+
id: 'media.field.dropZone',
|
|
40
|
+
defaultMessage: 'Attached assets. Drop files here to attach them, or use the Upload button.'
|
|
41
|
+
},
|
|
42
|
+
dropZoneReadOnly: {
|
|
43
|
+
id: 'media.field.dropZoneReadOnly',
|
|
44
|
+
defaultMessage: 'Attached assets'
|
|
45
|
+
},
|
|
46
|
+
libraryRoot: { id: 'media.field.libraryRoot', defaultMessage: 'All media' },
|
|
47
|
+
emptySingle: {
|
|
48
|
+
id: 'media.field.emptySingle',
|
|
49
|
+
defaultMessage: 'No asset selected'
|
|
50
|
+
},
|
|
51
|
+
emptyMultiple: {
|
|
52
|
+
id: 'media.field.emptyMultiple',
|
|
53
|
+
defaultMessage: 'No assets attached'
|
|
54
|
+
},
|
|
55
|
+
emptyBody: {
|
|
56
|
+
id: 'media.field.emptyBody',
|
|
57
|
+
defaultMessage: 'Drop a file here, or pick one from the Media Library. New files upload when you save.'
|
|
58
|
+
},
|
|
59
|
+
emptyBodyPickOnly: {
|
|
60
|
+
id: 'media.field.emptyBodyPickOnly',
|
|
61
|
+
defaultMessage: 'Pick an asset from the Media Library.'
|
|
62
|
+
},
|
|
63
|
+
dropHere: {
|
|
64
|
+
id: 'media.field.dropHere',
|
|
65
|
+
defaultMessage: 'Drop to attach'
|
|
66
|
+
},
|
|
67
|
+
dropBody: {
|
|
68
|
+
id: 'media.field.dropBody',
|
|
69
|
+
defaultMessage: 'Dropped files open in the upload dialog first, so you can check them.'
|
|
70
|
+
},
|
|
71
|
+
accepts: { id: 'media.field.accepts', defaultMessage: 'Accepts {what}' },
|
|
72
|
+
count: {
|
|
73
|
+
id: 'media.field.count',
|
|
74
|
+
defaultMessage: '{count, plural, one {# asset attached} other {# assets attached}}'
|
|
75
|
+
},
|
|
76
|
+
uploadRejected: {
|
|
77
|
+
id: 'media.field.uploadRejected',
|
|
78
|
+
defaultMessage: '{name} is not an accepted file type for this field.'
|
|
79
|
+
},
|
|
80
|
+
noMediaAccess: {
|
|
81
|
+
id: 'media.field.noMediaAccess',
|
|
82
|
+
defaultMessage: 'You don’t have access to the Media Library, so this field can only be read.'
|
|
83
|
+
},
|
|
84
|
+
emptyReadOnly: {
|
|
85
|
+
id: 'media.field.emptyReadOnly',
|
|
86
|
+
defaultMessage: 'Nothing attached'
|
|
87
|
+
},
|
|
88
|
+
emptyBodyReadOnly: {
|
|
89
|
+
id: 'media.field.emptyBodyReadOnly',
|
|
90
|
+
defaultMessage: 'This field has no assets.'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Normalize the field value to an ordered list of {@link MediaValueRef}s.
|
|
95
|
+
*
|
|
96
|
+
* The value is `{ id, alt?, decorative? }` (or a list of them) since `ORT-83`,
|
|
97
|
+
* and a bare id or id array before that — both still arrive, so both are
|
|
98
|
+
* normalized here and the legacy form stays confined to the kernel's
|
|
99
|
+
* `toMediaValueRef`. A legacy value reads as "no alternative supplied", which is
|
|
100
|
+
* exactly what it is.
|
|
101
|
+
*/
|
|
102
|
+
function toRefs(value) {
|
|
103
|
+
return (Array.isArray(value) ? value : [value])
|
|
104
|
+
.map(toMediaValueRef)
|
|
105
|
+
.filter((ref) => ref !== null && !!ref.id);
|
|
106
|
+
}
|
|
107
|
+
/** The original-bytes route for an id we know nothing else about. */
|
|
108
|
+
function rawUrl(id) {
|
|
109
|
+
return `/api/media/assets/${id}/raw`;
|
|
110
|
+
}
|
|
111
|
+
/** Whether a drag carries files (as opposed to text or an in-page element). */
|
|
112
|
+
function dragHasFiles(transfer) {
|
|
113
|
+
return !!transfer && Array.from(transfer.types).includes('Files');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The editor control for one media field — a single asset or an ordered list.
|
|
117
|
+
* The attached assets render as preview tiles ({@link MediaFieldItem}, resolved
|
|
118
|
+
* from the server-provided refs or a just-picked/uploaded asset) inside a panel
|
|
119
|
+
* that doubles as a **drop zone**. Below it sit **Select from library** (the
|
|
120
|
+
* {@link MediaPickerDialog}) and **Upload**, plus the field's accept hint and, in
|
|
121
|
+
* multiple mode, the attached count.
|
|
122
|
+
*
|
|
123
|
+
* **Choosing a file uploads nothing.** The {@link UploadDialog} — the Media
|
|
124
|
+
* Library's own modal, reused whole — stages and previews the files (a drop on
|
|
125
|
+
* the panel opens it pre-staged), and confirming only *stages* them: they get a
|
|
126
|
+
* placeholder id, render as "Pending upload" tiles, and the bytes move when the
|
|
127
|
+
* record is **saved or published** (`usePendingMediaUploads`, the plugin's
|
|
128
|
+
* presave contribution). So abandoning an edit leaves nothing behind in the
|
|
129
|
+
* library, and a record plus its new assets land as one commit.
|
|
130
|
+
*
|
|
131
|
+
* Fully controlled: the parent (the Media tab, bound to the entry form) owns the
|
|
132
|
+
* value as an asset id or id array.
|
|
133
|
+
*
|
|
134
|
+
* **Read-only** (the reader has no `content:update`/`content:create` on the
|
|
135
|
+
* record) reduces it to the tiles: no drop zone, no Select/Upload row, no
|
|
136
|
+
* remove or reorder on a tile. Note this is a *content* permission, distinct
|
|
137
|
+
* from the `media:*` gates below — a user may hold every media permission there
|
|
138
|
+
* is and still not be allowed to change **this record**, and attaching an asset
|
|
139
|
+
* to a record is a write to the record.
|
|
140
|
+
*/
|
|
141
|
+
export function MediaFieldControl({ id, multiple, accept, value, initialRefs, refsPending, invalid, describedBy, required, uploads, readOnly = false, onChange, onBlur }) {
|
|
142
|
+
const intl = useIntl();
|
|
143
|
+
// Mirror the server's media matrix. Without these the controls still
|
|
144
|
+
// *render*, and the failure lands late and loud: a deferred upload 403s
|
|
145
|
+
// inside the save, which aborts the whole record write — so a missing
|
|
146
|
+
// permission would cost the user their unrelated edits too.
|
|
147
|
+
const canPickMedia = useHasPermission(MEDIA_READ);
|
|
148
|
+
const canUploadMedia = useHasPermission(MEDIA_CREATE);
|
|
149
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
150
|
+
const [uploadOpen, setUploadOpen] = useState(false);
|
|
151
|
+
// Files dropped on the panel, staged into the upload dialog when it opens —
|
|
152
|
+
// a drop gets the same preview-before-you-commit step as the Upload button.
|
|
153
|
+
const [dropped, setDropped] = useState([]);
|
|
154
|
+
const [dragging, setDragging] = useState(false);
|
|
155
|
+
// Nested elements fire dragleave as the pointer crosses them, so the highlight
|
|
156
|
+
// is held by a depth counter rather than the last event to arrive.
|
|
157
|
+
const dragDepth = useRef(0);
|
|
158
|
+
// Locally-known assets (just picked or uploaded), merged over the server refs
|
|
159
|
+
// so a freshly-added asset shows its real name/thumbnail before any refetch.
|
|
160
|
+
const [known, setKnown] = useState(new Map());
|
|
161
|
+
const refs = toRefs(value);
|
|
162
|
+
const ids = refs.map((ref) => ref.id);
|
|
163
|
+
// Staging is only offered when the user may actually upload; the presave
|
|
164
|
+
// step would otherwise fail the save on their behalf. A read-only record is
|
|
165
|
+
// the same argument one level up: there is no save to ride.
|
|
166
|
+
//
|
|
167
|
+
// This also disarms the drop zone, whose enter/drop handlers both bail on
|
|
168
|
+
// `!canStage` — so a read-only field can't be dropped onto.
|
|
169
|
+
const canStage = !!uploads && canUploadMedia && !readOnly;
|
|
170
|
+
const pending = uploads?.pending;
|
|
171
|
+
const displays = useMemo(() => {
|
|
172
|
+
const refs = new Map(initialRefs?.map((ref) => [ref.id, ref]));
|
|
173
|
+
return ids.map((assetId) => {
|
|
174
|
+
// A staged (not yet uploaded) file: everything is known locally, and
|
|
175
|
+
// an image previews straight off its object URL.
|
|
176
|
+
const staged = pending?.get(assetId);
|
|
177
|
+
if (staged)
|
|
178
|
+
return {
|
|
179
|
+
id: assetId,
|
|
180
|
+
name: staged.file.name,
|
|
181
|
+
url: staged.previewUrl ?? '',
|
|
182
|
+
kind: kindFromMime(staged.file.type),
|
|
183
|
+
mimeType: staged.file.type,
|
|
184
|
+
size: staged.file.size,
|
|
185
|
+
pending: true
|
|
186
|
+
};
|
|
187
|
+
const local = known.get(assetId);
|
|
188
|
+
if (local)
|
|
189
|
+
return {
|
|
190
|
+
id: assetId,
|
|
191
|
+
name: local.name,
|
|
192
|
+
// Tiles are small: prefer the derivative the server made.
|
|
193
|
+
url: local.thumbUrl ?? local.previewUrl ?? local.url,
|
|
194
|
+
originalUrl: local.url,
|
|
195
|
+
kind: local.kind,
|
|
196
|
+
mimeType: local.mimeType,
|
|
197
|
+
size: local.size,
|
|
198
|
+
dimensions: local.dimensions
|
|
199
|
+
};
|
|
200
|
+
const ref = refs.get(assetId);
|
|
201
|
+
if (ref)
|
|
202
|
+
return {
|
|
203
|
+
id: assetId,
|
|
204
|
+
name: ref.name,
|
|
205
|
+
url: ref.thumbUrl ?? ref.previewUrl ?? ref.url,
|
|
206
|
+
originalUrl: ref.url,
|
|
207
|
+
kind: ref.kind,
|
|
208
|
+
mimeType: ref.mimeType,
|
|
209
|
+
missing: ref.missing
|
|
210
|
+
};
|
|
211
|
+
// Nothing known about this id. While the entry's media read is in
|
|
212
|
+
// flight, wait: guessing the raw route made every edit-mode open
|
|
213
|
+
// pull **full-size originals** to draw 180px tiles, and printed a
|
|
214
|
+
// uuid where the file name goes. Once the read has settled without
|
|
215
|
+
// a ref (it failed, or no media plugin is bound) waiting would
|
|
216
|
+
// never end — so fall back to the raw route and show *something*.
|
|
217
|
+
// The id names the controls either way, so "Remove …" stays unique.
|
|
218
|
+
if (refsPending)
|
|
219
|
+
return {
|
|
220
|
+
id: assetId,
|
|
221
|
+
name: assetId,
|
|
222
|
+
url: '',
|
|
223
|
+
kind: '',
|
|
224
|
+
mimeType: '',
|
|
225
|
+
resolving: true
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
id: assetId,
|
|
229
|
+
name: assetId,
|
|
230
|
+
url: rawUrl(assetId),
|
|
231
|
+
kind: MEDIA_KIND.Image,
|
|
232
|
+
mimeType: ''
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}, [ids, initialRefs, known, pending, refsPending]);
|
|
236
|
+
const remember = (assets) => {
|
|
237
|
+
setKnown((current) => {
|
|
238
|
+
const next = new Map(current);
|
|
239
|
+
for (const asset of assets)
|
|
240
|
+
next.set(asset.id, asset);
|
|
241
|
+
return next;
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
const setRefs = (nextRefs) => {
|
|
245
|
+
// Anything staged that just left the field is forgotten here — one place,
|
|
246
|
+
// so a remove, a replace, and a re-pick all release the preview blob (and
|
|
247
|
+
// stop the save uploading a file nothing references).
|
|
248
|
+
if (pending) {
|
|
249
|
+
const kept = new Set(nextRefs.map((ref) => ref.id));
|
|
250
|
+
for (const assetId of ids) {
|
|
251
|
+
if (!kept.has(assetId) && pending.has(assetId))
|
|
252
|
+
uploads?.drop(assetId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
onChange(multiple ? nextRefs : (nextRefs[0] ?? null));
|
|
256
|
+
onBlur?.();
|
|
257
|
+
};
|
|
258
|
+
/** Attaching by id, keeping the alt text of anything already attached. */
|
|
259
|
+
const setIds = (nextIds) => {
|
|
260
|
+
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
261
|
+
setRefs(nextIds.map((assetId) => byId.get(assetId) ?? { id: assetId }));
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Patches one usage's text alternative.
|
|
265
|
+
*
|
|
266
|
+
* `alt` and `decorative` are mutually exclusive by the kernel's validation —
|
|
267
|
+
* an image cannot be presentational *and* carry a description — so marking a
|
|
268
|
+
* usage decorative drops whatever was typed rather than leaving a value the
|
|
269
|
+
* publish gate would refuse. Un-marking it leaves the field empty, which is
|
|
270
|
+
* the honest state: the author has not answered yet.
|
|
271
|
+
*/
|
|
272
|
+
const patchRef = (index, patch) => {
|
|
273
|
+
setRefs(refs.map((ref, at) => {
|
|
274
|
+
if (at !== index)
|
|
275
|
+
return ref;
|
|
276
|
+
const next = { ...ref, ...patch };
|
|
277
|
+
if (next.decorative === true)
|
|
278
|
+
delete next.alt;
|
|
279
|
+
if (next.decorative !== true)
|
|
280
|
+
delete next.decorative;
|
|
281
|
+
if (next.alt === '')
|
|
282
|
+
delete next.alt;
|
|
283
|
+
return next;
|
|
284
|
+
}));
|
|
285
|
+
};
|
|
286
|
+
const onPicked = (assets) => {
|
|
287
|
+
if (!assets.length)
|
|
288
|
+
return;
|
|
289
|
+
remember(assets);
|
|
290
|
+
if (multiple) {
|
|
291
|
+
// Append the newly-picked, skipping any already attached.
|
|
292
|
+
const existing = new Set(ids);
|
|
293
|
+
const added = assets
|
|
294
|
+
.map((a) => a.id)
|
|
295
|
+
.filter((assetId) => !existing.has(assetId));
|
|
296
|
+
setIds([...ids, ...added]);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
setIds(assets[0] ? [assets[0].id] : []);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Stage the confirmed files against the field — no request yet. Each gets a
|
|
304
|
+
* placeholder id that rides the form until the save uploads it. The field's
|
|
305
|
+
* own `accept` is enforced here on the file's MIME type (the server checks
|
|
306
|
+
* the real asset again on save), so a rejected file never reaches the value.
|
|
307
|
+
*/
|
|
308
|
+
const onStaged = (files) => {
|
|
309
|
+
if (!uploads || !canStage)
|
|
310
|
+
return;
|
|
311
|
+
const allowed = [];
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
if (acceptsFile(accept, file))
|
|
314
|
+
allowed.push(file);
|
|
315
|
+
else
|
|
316
|
+
toast.error(intl.formatMessage(messages.uploadRejected, {
|
|
317
|
+
name: file.name
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
if (!allowed.length)
|
|
321
|
+
return;
|
|
322
|
+
const staged = uploads.stage(multiple ? allowed : allowed.slice(0, 1));
|
|
323
|
+
setIds(multiple ? [...ids, ...staged] : staged.slice(0, 1));
|
|
324
|
+
};
|
|
325
|
+
/** Open the upload dialog, optionally pre-staged with dropped files. */
|
|
326
|
+
const openUploadWith = (files) => {
|
|
327
|
+
setDropped(multiple ? files : files.slice(0, 1));
|
|
328
|
+
setUploadOpen(true);
|
|
329
|
+
};
|
|
330
|
+
const removeAt = (index) => setIds(ids.filter((_, i) => i !== index));
|
|
331
|
+
const move = (index, delta) => {
|
|
332
|
+
const target = index + delta;
|
|
333
|
+
if (target < 0 || target >= ids.length)
|
|
334
|
+
return;
|
|
335
|
+
const next = [...ids];
|
|
336
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
337
|
+
setIds(next);
|
|
338
|
+
};
|
|
339
|
+
// Best-effort `accept` on the native file input (server still enforces).
|
|
340
|
+
const acceptAttr = useMemo(() => {
|
|
341
|
+
const mimes = accept?.mimeTypes ?? [];
|
|
342
|
+
const kinds = accept?.kinds ?? [];
|
|
343
|
+
const fromKinds = kinds
|
|
344
|
+
.filter((k) => k === MEDIA_KIND.Image ||
|
|
345
|
+
k === MEDIA_KIND.Video ||
|
|
346
|
+
k === MEDIA_KIND.Audio)
|
|
347
|
+
.map((k) => `${k}/*`);
|
|
348
|
+
const all = [...mimes, ...fromKinds];
|
|
349
|
+
return all.length ? all.join(',') : undefined;
|
|
350
|
+
}, [accept]);
|
|
351
|
+
const acceptHint = describeAccept(accept);
|
|
352
|
+
const isEmpty = displays.length === 0;
|
|
353
|
+
const stagedCount = displays.filter((item) => item.pending).length;
|
|
354
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { role: "group", "aria-label": intl.formatMessage(canStage ? messages.dropZone : messages.dropZoneReadOnly), onDragEnter: (event) => {
|
|
355
|
+
if (!dragHasFiles(event.dataTransfer) || !canStage)
|
|
356
|
+
return;
|
|
357
|
+
dragDepth.current += 1;
|
|
358
|
+
setDragging(true);
|
|
359
|
+
}, onDragOver: (event) => {
|
|
360
|
+
// Without this the browser opens the file instead of dropping.
|
|
361
|
+
if (dragHasFiles(event.dataTransfer))
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
}, onDragLeave: () => {
|
|
364
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
365
|
+
if (dragDepth.current === 0)
|
|
366
|
+
setDragging(false);
|
|
367
|
+
}, onDrop: (event) => {
|
|
368
|
+
if (!dragHasFiles(event.dataTransfer) || !canStage)
|
|
369
|
+
return;
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
dragDepth.current = 0;
|
|
372
|
+
setDragging(false);
|
|
373
|
+
// Straight into the same dialog the Upload button opens, so
|
|
374
|
+
// a drop is reviewed before it's staged.
|
|
375
|
+
openUploadWith(Array.from(event.dataTransfer.files));
|
|
376
|
+
}, className: cn('rounded-xl border bg-muted/20 p-3 transition-colors', isEmpty && 'border-dashed', invalid && 'border-destructive/60', dragging && 'border-primary bg-primary/5'), children: isEmpty ? (_jsx(Empty, { className: "gap-4 p-6 md:p-8", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: dragging ? (_jsx(UploadCloud, { "aria-hidden": true })) : (_jsx(ImagePlus, { "aria-hidden": true })) }), _jsx(EmptyTitle, { className: "text-base", children: readOnly
|
|
377
|
+
? intl.formatMessage(messages.emptyReadOnly)
|
|
378
|
+
: dragging
|
|
379
|
+
? intl.formatMessage(messages.dropHere)
|
|
380
|
+
: intl.formatMessage(multiple
|
|
381
|
+
? messages.emptyMultiple
|
|
382
|
+
: messages.emptySingle) }), _jsx(EmptyDescription, { children: intl.formatMessage(readOnly
|
|
383
|
+
? messages.emptyBodyReadOnly
|
|
384
|
+
: dragging
|
|
385
|
+
? messages.dropBody
|
|
386
|
+
: canStage
|
|
387
|
+
? messages.emptyBody
|
|
388
|
+
: messages.emptyBodyPickOnly) })] }) })) : (_jsx("ul", { className: cn('grid gap-3', multiple
|
|
389
|
+
? 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-4'
|
|
390
|
+
: 'grid-cols-1 sm:max-w-[15rem]'), children: displays.map((item, index) => (_jsx(MediaFieldItem, { item: item, index: index, total: displays.length, multiple: multiple, readOnly: readOnly, onRemove: () => removeAt(index), onMove: (delta) => move(index, delta), ...(refs[index]?.alt !== undefined
|
|
391
|
+
? { alt: refs[index].alt }
|
|
392
|
+
: {}), decorative: refs[index]?.decorative === true, onAltChange: readOnly
|
|
393
|
+
? undefined
|
|
394
|
+
: (next) => patchRef(index, { alt: next }), onDecorativeChange: readOnly
|
|
395
|
+
? undefined
|
|
396
|
+
: (next) => patchRef(index, {
|
|
397
|
+
decorative: next
|
|
398
|
+
}) }, `${item.id}-${index}`))) })) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [readOnly ? null : (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shadow-none", id: id, "aria-required": required, "aria-invalid": invalid, "aria-describedby": describedBy, disabled: !canPickMedia, onClick: () => setPickerOpen(true), children: [_jsx(ImagePlus, { className: "size-4", "aria-hidden": true }), intl.formatMessage(!multiple && displays.length > 0
|
|
399
|
+
? messages.replace
|
|
400
|
+
: multiple
|
|
401
|
+
? messages.add
|
|
402
|
+
: messages.select)] }), canStage ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shadow-none", onClick: () => openUploadWith([]), children: [_jsx(UploadCloud, { className: "size-4", "aria-hidden": true }), intl.formatMessage(messages.upload)] })) : null] })), _jsxs("p", { className: cn('ml-auto flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground', !stagedCount &&
|
|
403
|
+
(!acceptHint || readOnly) &&
|
|
404
|
+
(canPickMedia || readOnly) &&
|
|
405
|
+
!(multiple && displays.length > 0) &&
|
|
406
|
+
'hidden'), children: [stagedCount > 0 ? (_jsx("span", { className: "text-foreground", children: intl.formatMessage(messages.stagedCount, {
|
|
407
|
+
count: stagedCount
|
|
408
|
+
}) })) : null, multiple && displays.length > 0 ? (_jsx("span", { children: intl.formatMessage(messages.count, {
|
|
409
|
+
count: displays.length
|
|
410
|
+
}) })) : null, acceptHint && !readOnly ? (_jsx("span", { children: intl.formatMessage(messages.accepts, {
|
|
411
|
+
what: acceptHint
|
|
412
|
+
}) })) : null, canPickMedia || readOnly ? null : (_jsx("span", { children: intl.formatMessage(messages.noMediaAccess) }))] })] }), pickerOpen ? (_jsx(MediaPickerDialog, { open: true, onOpenChange: setPickerOpen, multiple: multiple, accept: accept, attachedIds: ids, onConfirm: onPicked })) : null, readOnly ? null : (_jsx(UploadDialog, { open: uploadOpen, onOpenChange: setUploadOpen, locationLabel: intl.formatMessage(messages.libraryRoot), description: intl.formatMessage(messages.uploadTo, {
|
|
413
|
+
location: intl.formatMessage(messages.libraryRoot)
|
|
414
|
+
}), hint: acceptHint
|
|
415
|
+
? intl.formatMessage(messages.uploadHintAccept, {
|
|
416
|
+
what: acceptHint
|
|
417
|
+
})
|
|
418
|
+
: undefined, accept: acceptAttr, multiple: multiple, confirmLabel: intl.formatMessage(messages.attach), initialFiles: dropped,
|
|
419
|
+
// No `collectAlt`: a field's files upload with the record, and
|
|
420
|
+
// this control has nowhere to carry a description that far.
|
|
421
|
+
onUpload: (uploads) => onStaged(uploads.map((upload) => upload.file)) }))] }));
|
|
422
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MediaFolder } from '../../../types/mediaFolder';
|
|
2
|
+
/**
|
|
3
|
+
* One folder in the sidebar tree. A folder with sub-folders is a
|
|
4
|
+
* design-system `Collapsible` (a chevron toggles its children under a left tree
|
|
5
|
+
* line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
|
|
6
|
+
* matching spacer so names align. The name is a separate button that navigates
|
|
7
|
+
* into the folder — clicking it never toggles the group — and the open folder is
|
|
8
|
+
* highlighted. Navigation dispatches up.
|
|
9
|
+
*/
|
|
10
|
+
export declare function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }: {
|
|
11
|
+
folder: MediaFolder;
|
|
12
|
+
folders: MediaFolder[];
|
|
13
|
+
currentFolderId: string;
|
|
14
|
+
counts: Map<string, number>;
|
|
15
|
+
onNavigate: (folderId: string) => void;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAW9D;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,eAAe,EACf,MAAM,EACN,UAAU,EACb,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,+BAyFA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, cn } from '@orthacms/design-system';
|
|
4
|
+
import { ChevronRight, Folder } from 'lucide-react';
|
|
5
|
+
/** Intl descriptors for {@link MediaFolderTreeItem}, co-located. */
|
|
6
|
+
const messages = defineMessages({
|
|
7
|
+
toggle: { id: 'media.tree.toggle', defaultMessage: 'Toggle {name}' },
|
|
8
|
+
count: {
|
|
9
|
+
id: 'media.tree.count',
|
|
10
|
+
defaultMessage: '{count, plural, one {# item} other {# items}}'
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* One folder in the sidebar tree. A folder with sub-folders is a
|
|
15
|
+
* design-system `Collapsible` (a chevron toggles its children under a left tree
|
|
16
|
+
* line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
|
|
17
|
+
* matching spacer so names align. The name is a separate button that navigates
|
|
18
|
+
* into the folder — clicking it never toggles the group — and the open folder is
|
|
19
|
+
* highlighted. Navigation dispatches up.
|
|
20
|
+
*/
|
|
21
|
+
export function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }) {
|
|
22
|
+
const intl = useIntl();
|
|
23
|
+
const children = folders
|
|
24
|
+
.filter((f) => f.parentId === folder.id)
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
const hasChildren = children.length > 0;
|
|
27
|
+
const active = currentFolderId === folder.id;
|
|
28
|
+
const nameButton = (_jsxs("button", { type: "button", onClick: () => onNavigate(folder.id), "aria-current": active ? 'true' : undefined, className: cn('flex min-w-0 flex-1 items-center gap-2 rounded-md py-1.5 pr-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', active ? 'font-medium text-primary' : 'text-foreground'), children: [_jsx(Folder, { className: cn('size-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: folder.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.count, {
|
|
29
|
+
count: counts.get(folder.id) ?? 0
|
|
30
|
+
}) })] }));
|
|
31
|
+
if (!hasChildren) {
|
|
32
|
+
return (_jsx("li", { children: _jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx("span", { className: "size-6 shrink-0", "aria-hidden": true }), nameButton] }) }));
|
|
33
|
+
}
|
|
34
|
+
return (_jsx("li", { children: _jsxs(Collapsible, { defaultOpen: true, className: "group/folder", children: [_jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": intl.formatMessage(messages.toggle, {
|
|
35
|
+
name: folder.name
|
|
36
|
+
}), className: "flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronRight, { className: "size-3.5 transition-transform duration-200 group-data-[state=open]/folder:rotate-90" }) }) }), nameButton] }), _jsx(CollapsibleContent, { children: _jsx("ul", { className: "ml-[0.85rem] mt-0.5 space-y-0.5 border-l border-border pl-1.5", children: children.map((child) => (_jsx(MediaFolderTreeItem, { folder: child, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, child.id))) }) })] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MediaFolder } from '../../types/mediaFolder';
|
|
2
|
+
/**
|
|
3
|
+
* The Media Library's folder sidebar — a **flat** nav region sitting flush on the
|
|
4
|
+
* page background (mirroring the Content Library sidebar), not a bordered island.
|
|
5
|
+
* The page supplies the divider (`lg:border-r`) via `className`, so the mobile
|
|
6
|
+
* drawer copy renders without one. An "All media" root entry sits above the
|
|
7
|
+
* folder tree, where any folder with sub-folders is collapsible; a New folder
|
|
8
|
+
* action closes the list. Highlights the open folder and dispatches navigation /
|
|
9
|
+
* create up to the page.
|
|
10
|
+
*/
|
|
11
|
+
export declare function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }: {
|
|
12
|
+
folders: MediaFolder[];
|
|
13
|
+
currentFolderId: string;
|
|
14
|
+
counts: Map<string, number>;
|
|
15
|
+
/** Item count shown against the "All media" root row. */
|
|
16
|
+
rootCount: number;
|
|
17
|
+
onNavigate: (folderId: string) => void;
|
|
18
|
+
onNewFolder: () => void;
|
|
19
|
+
canCreate: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAmFA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Button, cn } from '@orthacms/design-system';
|
|
4
|
+
import { FolderPlus, Home, Images } from 'lucide-react';
|
|
5
|
+
import { ROOT_FOLDER_ID } from '../../constants';
|
|
6
|
+
import { MediaFolderTreeItem } from './MediaFolderTreeItem';
|
|
7
|
+
/** Intl descriptors for {@link MediaFoldersNav}, co-located. */
|
|
8
|
+
const messages = defineMessages({
|
|
9
|
+
nav: { id: 'media.nav.nav', defaultMessage: 'Folders' },
|
|
10
|
+
heading: { id: 'media.nav.heading', defaultMessage: 'Library' },
|
|
11
|
+
allMedia: { id: 'media.nav.allMedia', defaultMessage: 'All media' },
|
|
12
|
+
allCount: {
|
|
13
|
+
id: 'media.nav.allCount',
|
|
14
|
+
defaultMessage: '{count, plural, one {# item} other {# items}}'
|
|
15
|
+
},
|
|
16
|
+
newFolder: { id: 'media.nav.newFolder', defaultMessage: 'New folder' }
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* The Media Library's folder sidebar — a **flat** nav region sitting flush on the
|
|
20
|
+
* page background (mirroring the Content Library sidebar), not a bordered island.
|
|
21
|
+
* The page supplies the divider (`lg:border-r`) via `className`, so the mobile
|
|
22
|
+
* drawer copy renders without one. An "All media" root entry sits above the
|
|
23
|
+
* folder tree, where any folder with sub-folders is collapsible; a New folder
|
|
24
|
+
* action closes the list. Highlights the open folder and dispatches navigation /
|
|
25
|
+
* create up to the page.
|
|
26
|
+
*/
|
|
27
|
+
export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }) {
|
|
28
|
+
const intl = useIntl();
|
|
29
|
+
const topLevel = folders
|
|
30
|
+
.filter((f) => f.parentId === ROOT_FOLDER_ID)
|
|
31
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
32
|
+
const rootActive = currentFolderId === ROOT_FOLDER_ID;
|
|
33
|
+
return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Images, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-sm font-semibold tracking-[-0.01em]", children: intl.formatMessage(messages.heading) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
|
|
34
|
+
? 'bg-primary/10 font-medium text-primary'
|
|
35
|
+
: 'text-foreground hover:bg-accent'), children: [_jsx(Home, { className: cn('size-4 shrink-0', rootActive
|
|
36
|
+
? 'text-primary'
|
|
37
|
+
: 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: intl.formatMessage(messages.allMedia) }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.allCount, {
|
|
38
|
+
count: rootCount
|
|
39
|
+
}) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, folder.id)))] }) }), canCreate ? (_jsx("div", { className: "p-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full justify-start shadow-none", onClick: onNewFolder, children: [_jsx(FolderPlus, { "aria-hidden": true }), intl.formatMessage(messages.newFolder)] }) })) : null] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MediaAsset } from '../../../types/mediaAsset';
|
|
2
|
+
import { type AssetActionKind } from '../../AssetActionsMenu';
|
|
3
|
+
/**
|
|
4
|
+
* An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
|
|
5
|
+
* selection checkbox (always visible once selected), over the name and a
|
|
6
|
+
* size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
|
|
7
|
+
* the checkbox toggles selection. A selected tile gets a primary ring.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }: {
|
|
10
|
+
asset: MediaAsset;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
onToggleSelect: (id: string) => void;
|
|
13
|
+
onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
|
|
14
|
+
canCreate: boolean;
|
|
15
|
+
canUpdate: boolean;
|
|
16
|
+
canDelete: boolean;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAsEA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Checkbox, cn } from '@orthacms/design-system';
|
|
4
|
+
import { formatBytes } from '../../../utils/formatBytes';
|
|
5
|
+
import { buildAssetHandlers } from '../../../utils/assetHandlers';
|
|
6
|
+
import { AssetActionsMenu } from '../../AssetActionsMenu';
|
|
7
|
+
import { MediaThumbnail } from '../../MediaThumbnail';
|
|
8
|
+
/** Intl descriptors for {@link MediaAssetCard}, co-located. */
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
open: { id: 'media.assetCard.open', defaultMessage: 'Open {name}' },
|
|
11
|
+
select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' }
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
|
|
15
|
+
* selection checkbox (always visible once selected), over the name and a
|
|
16
|
+
* size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
|
|
17
|
+
* the checkbox toggles selection. A selected tile gets a primary ring.
|
|
18
|
+
*/
|
|
19
|
+
export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }) {
|
|
20
|
+
const intl = useIntl();
|
|
21
|
+
const handlers = buildAssetHandlers(asset, onAction);
|
|
22
|
+
return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
|
|
23
|
+
name: asset.name
|
|
24
|
+
}), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
|
|
25
|
+
name: asset.name
|
|
26
|
+
}) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) })] }), _jsxs("div", { className: "p-3", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsxs("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: [formatBytes(asset.size), asset.dimensions
|
|
27
|
+
? ` · ${asset.dimensions.width}×${asset.dimensions.height}`
|
|
28
|
+
: null] })] })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MediaFolder } from '../../../types/mediaFolder';
|
|
2
|
+
/**
|
|
3
|
+
* A folder tile in the grid — a folder glyph, name, and item count, with a hover
|
|
4
|
+
* ⋯ menu (Open / Rename / Delete). The tile body is a single button that opens
|
|
5
|
+
* the folder; the menu sits outside it so the two controls don't nest.
|
|
6
|
+
*/
|
|
7
|
+
export declare function MediaFolderCard({ folder, assetCount, onOpen, onRename, onDelete, canUpdate, canDelete }: {
|
|
8
|
+
folder: MediaFolder;
|
|
9
|
+
assetCount: number;
|
|
10
|
+
onOpen: (folderId: string) => void;
|
|
11
|
+
onRename: (folder: MediaFolder) => void;
|
|
12
|
+
onDelete: (folder: MediaFolder) => void;
|
|
13
|
+
canUpdate: boolean;
|
|
14
|
+
canDelete: boolean;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaFolderCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAY9D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC5B,MAAM,EACN,UAAU,EACV,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAwCA"}
|