@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,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Button, Progress } from '@orthacms/design-system';
|
|
5
|
+
import { UploadCloud, X } from 'lucide-react';
|
|
6
|
+
import { UPLOAD_STATUS } from '../../constants';
|
|
7
|
+
import { MediaUploadRow } from './MediaUploadRow';
|
|
8
|
+
/** Intl descriptors for {@link MediaUploadBanner}, co-located. */
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
region: { id: 'media.uploadBanner.region', defaultMessage: 'Uploads' },
|
|
11
|
+
uploading: {
|
|
12
|
+
id: 'media.uploadBanner.uploading',
|
|
13
|
+
defaultMessage: 'Uploading {done} of {total} · {percent}%'
|
|
14
|
+
},
|
|
15
|
+
complete: {
|
|
16
|
+
id: 'media.uploadBanner.complete',
|
|
17
|
+
defaultMessage: '{count, plural, one {# file uploaded} other {# files uploaded}}'
|
|
18
|
+
},
|
|
19
|
+
someFailed: {
|
|
20
|
+
id: 'media.uploadBanner.someFailed',
|
|
21
|
+
defaultMessage: '{done, plural, one {# file uploaded} other {# files uploaded}}, {failed} failed'
|
|
22
|
+
},
|
|
23
|
+
allFailed: {
|
|
24
|
+
id: 'media.uploadBanner.allFailed',
|
|
25
|
+
defaultMessage: '{failed, plural, one {# upload failed} other {# uploads failed}}'
|
|
26
|
+
},
|
|
27
|
+
overall: {
|
|
28
|
+
id: 'media.uploadBanner.overall',
|
|
29
|
+
defaultMessage: 'Overall upload progress'
|
|
30
|
+
},
|
|
31
|
+
dismiss: { id: 'media.uploadBanner.dismiss', defaultMessage: 'Dismiss' },
|
|
32
|
+
/** Announced when one file in a batch fails — the row itself is silent. */
|
|
33
|
+
failureAnnouncement: {
|
|
34
|
+
id: 'media.uploadBanner.failureAnnouncement',
|
|
35
|
+
defaultMessage: '{name} failed to upload. {reason}'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The upload progress banner — the batch headline ("Uploading 3 of 5 · 47%"), an
|
|
40
|
+
* overall size-weighted bar while anything is running, and one
|
|
41
|
+
* {@link MediaUploadRow} per file. It sits above the browser rather than inside
|
|
42
|
+
* the upload dialog, so the dialog can close on submit and the user keeps
|
|
43
|
+
* browsing (and can queue more) while bytes move.
|
|
44
|
+
*
|
|
45
|
+
* `role="status"` sits on the **headline alone**, not the whole banner: a polite
|
|
46
|
+
* live region re-announces its entire contents on every change, so wrapping the
|
|
47
|
+
* file list would read all N rows aloud on each progress tick. Scoped to the
|
|
48
|
+
* headline, a screen-reader user hears "Uploading 3 of 5 · 47%" and then the
|
|
49
|
+
* completion, while per-file detail stays available to read on demand.
|
|
50
|
+
*
|
|
51
|
+
* Renders nothing when the queue is empty.
|
|
52
|
+
*/
|
|
53
|
+
export function MediaUploadBanner({ items, summary, onRetry, onCancel, onDismiss }) {
|
|
54
|
+
const intl = useIntl();
|
|
55
|
+
// Failures are announced from their own region. The headline's counts move
|
|
56
|
+
// when a file fails, but "3 of 5" never says *which* file or why — and the
|
|
57
|
+
// rows carrying that sentence sit outside the live region on purpose (a
|
|
58
|
+
// polite region over the list would re-read all N rows on every progress
|
|
59
|
+
// tick). One line per newly-failed file is the middle ground.
|
|
60
|
+
const announcedRef = useRef(new Set());
|
|
61
|
+
const [failureNotice, setFailureNotice] = useState('');
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const announced = announcedRef.current;
|
|
64
|
+
const fresh = items.filter((item) => item.status === UPLOAD_STATUS.Failed && !announced.has(item.id));
|
|
65
|
+
// A retry can fail again, so a row leaving `Failed` becomes announceable
|
|
66
|
+
// once more.
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (item.status !== UPLOAD_STATUS.Failed)
|
|
69
|
+
announced.delete(item.id);
|
|
70
|
+
}
|
|
71
|
+
if (fresh.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
for (const item of fresh)
|
|
74
|
+
announced.add(item.id);
|
|
75
|
+
setFailureNotice(fresh
|
|
76
|
+
.map((item) => intl.formatMessage(messages.failureAnnouncement, {
|
|
77
|
+
name: item.name,
|
|
78
|
+
reason: item.error ?? ''
|
|
79
|
+
}))
|
|
80
|
+
.join(' '));
|
|
81
|
+
}, [items, intl]);
|
|
82
|
+
if (items.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const headline = summary.active
|
|
85
|
+
? intl.formatMessage(messages.uploading, {
|
|
86
|
+
done: summary.done,
|
|
87
|
+
total: summary.total,
|
|
88
|
+
percent: summary.percent
|
|
89
|
+
})
|
|
90
|
+
: summary.failed > 0 && summary.done > 0
|
|
91
|
+
? intl.formatMessage(messages.someFailed, {
|
|
92
|
+
done: summary.done,
|
|
93
|
+
failed: summary.failed
|
|
94
|
+
})
|
|
95
|
+
: summary.failed > 0
|
|
96
|
+
? intl.formatMessage(messages.allFailed, { failed: summary.failed })
|
|
97
|
+
: intl.formatMessage(messages.complete, { count: summary.done });
|
|
98
|
+
return (_jsxs("section", { "aria-label": intl.formatMessage(messages.region), className: "mb-4 rounded-xl border bg-muted/40 px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(UploadCloud, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "min-w-0 flex-1 text-sm font-medium", role: "status", children: headline }), summary.active ? null : (_jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.dismiss), onClick: onDismiss, children: _jsx(X, { "aria-hidden": true }) }))] }), summary.active ? (_jsx(Progress, { value: summary.percent, className: "mt-2 h-1.5", "aria-label": intl.formatMessage(messages.overall) })) : null, _jsx("p", { className: "sr-only", role: "status", children: failureNotice }), _jsx("ul", { className: "mt-1 max-h-48 divide-y overflow-auto", children: items.map((item) => (_jsx(MediaUploadRow, { item: item, onRetry: onRetry, onCancel: onCancel }, item.id))) })] }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assets added per time bucket. Columns rather than a line: uploads are discrete
|
|
3
|
+
* tallies, and a line between two weekly counts would imply values that were
|
|
4
|
+
* never measured.
|
|
5
|
+
*/
|
|
6
|
+
export declare function MediaUploadsWidget(): import("react").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaUploadsWidget/index.tsx"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,wBAAgB,kBAAkB,gCAqCjC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { ColumnTrend, WidgetCard } from '@orthacms/insights-admin';
|
|
4
|
+
import { useMediaUploads } from '../../hooks/useMediaInsights';
|
|
5
|
+
/** Intl descriptors for the uploads widget, co-located here. */
|
|
6
|
+
const messages = defineMessages({
|
|
7
|
+
title: {
|
|
8
|
+
id: 'media.insights.uploads.title',
|
|
9
|
+
defaultMessage: 'Uploads'
|
|
10
|
+
},
|
|
11
|
+
descriptionDay: {
|
|
12
|
+
id: 'media.insights.uploads.descriptionDay',
|
|
13
|
+
defaultMessage: 'Assets added per day'
|
|
14
|
+
},
|
|
15
|
+
descriptionWeek: {
|
|
16
|
+
id: 'media.insights.uploads.descriptionWeek',
|
|
17
|
+
defaultMessage: 'Assets added per week'
|
|
18
|
+
},
|
|
19
|
+
descriptionMonth: {
|
|
20
|
+
id: 'media.insights.uploads.descriptionMonth',
|
|
21
|
+
defaultMessage: 'Assets added per month'
|
|
22
|
+
},
|
|
23
|
+
tip: {
|
|
24
|
+
id: 'media.insights.uploads.tip',
|
|
25
|
+
defaultMessage: '{period} — {count, number} uploads'
|
|
26
|
+
},
|
|
27
|
+
ariaLabel: {
|
|
28
|
+
id: 'media.insights.uploads.ariaLabel',
|
|
29
|
+
defaultMessage: 'Assets uploaded over time, {total, number} across {points, number} periods.'
|
|
30
|
+
},
|
|
31
|
+
footer: {
|
|
32
|
+
id: 'media.insights.uploads.footer',
|
|
33
|
+
defaultMessage: '{total, number} uploaded this period.'
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
/** Description per bucket width, so the subtitle matches what is plotted. */
|
|
37
|
+
const DESCRIPTION = {
|
|
38
|
+
day: messages.descriptionDay,
|
|
39
|
+
week: messages.descriptionWeek,
|
|
40
|
+
month: messages.descriptionMonth
|
|
41
|
+
};
|
|
42
|
+
/** Date format per bucket width — a month bucket labelled `12 Mar` reads wrong. */
|
|
43
|
+
const LABEL_FORMAT = {
|
|
44
|
+
day: { day: 'numeric', month: 'short' },
|
|
45
|
+
week: { day: 'numeric', month: 'short' },
|
|
46
|
+
month: { month: 'short', year: '2-digit' }
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Assets added per time bucket. Columns rather than a line: uploads are discrete
|
|
50
|
+
* tallies, and a line between two weekly counts would imply values that were
|
|
51
|
+
* never measured.
|
|
52
|
+
*/
|
|
53
|
+
export function MediaUploadsWidget() {
|
|
54
|
+
const intl = useIntl();
|
|
55
|
+
const { data, isPending, isError } = useMediaUploads();
|
|
56
|
+
const granularity = data?.granularity ?? 'week';
|
|
57
|
+
const points = (data?.points ?? []).map((point) => ({
|
|
58
|
+
label: intl.formatDate(point.bucket, LABEL_FORMAT[granularity]),
|
|
59
|
+
value: point.value
|
|
60
|
+
}));
|
|
61
|
+
return (_jsx(WidgetCard, { title: intl.formatMessage(messages.title), description: intl.formatMessage(DESCRIPTION[granularity]), footer: intl.formatMessage(messages.footer, {
|
|
62
|
+
total: data?.total ?? 0
|
|
63
|
+
}), isPending: isPending, isError: isError, isEmpty: points.length === 0, skeletonRows: 5, children: _jsx(ColumnTrend, { points: points, ariaLabel: intl.formatMessage(messages.ariaLabel, {
|
|
64
|
+
total: data?.total ?? 0,
|
|
65
|
+
points: points.length
|
|
66
|
+
}), describe: (point) => intl.formatMessage(messages.tip, {
|
|
67
|
+
period: point.label,
|
|
68
|
+
count: point.value
|
|
69
|
+
}) }) }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One selectable destination row inside {@link MoveAssetsDialog}'s radio list —
|
|
4
|
+
* an indented label wrapping a radio, a folder icon, and the name. The assets'
|
|
5
|
+
* current folder renders dimmed to signal it's a no-op target.
|
|
6
|
+
*/
|
|
7
|
+
export declare function DestinationOption({ value, depth, icon, label, current }: {
|
|
8
|
+
/** Folder id (or root id) this option selects. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Indent level below the root. */
|
|
11
|
+
depth: number;
|
|
12
|
+
/** Leading glyph (home for root, folder otherwise). */
|
|
13
|
+
icon: ReactNode;
|
|
14
|
+
/** Folder name. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Whether this is the assets' current folder (dimmed, no-op). */
|
|
17
|
+
current: boolean;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MoveAssetsDialog/DestinationOption/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,IAAI,EACJ,KAAK,EACL,OAAO,EACV,EAAE;IACC,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,IAAI,EAAE,SAAS,CAAC;IAChB,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;CACpB,+BAeA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Label, RadioGroupItem, cn } from '@orthacms/design-system';
|
|
3
|
+
/**
|
|
4
|
+
* One selectable destination row inside {@link MoveAssetsDialog}'s radio list —
|
|
5
|
+
* an indented label wrapping a radio, a folder icon, and the name. The assets'
|
|
6
|
+
* current folder renders dimmed to signal it's a no-op target.
|
|
7
|
+
*/
|
|
8
|
+
export function DestinationOption({ value, depth, icon, label, current }) {
|
|
9
|
+
return (_jsxs(Label, { htmlFor: `media-move-${value}`, className: cn('flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm font-normal hover:bg-accent', current && 'opacity-60'), style: { paddingLeft: `${depth * 16 + 8}px` }, children: [_jsx(RadioGroupItem, { id: `media-move-${value}`, value: value }), icon, _jsx("span", { className: "truncate", children: label })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MediaFolder } from '../../types/mediaFolder';
|
|
2
|
+
/**
|
|
3
|
+
* Modal that picks a destination folder for the given assets. Offers the root
|
|
4
|
+
* ("All media") plus every folder as an indented radio list; the confirm is
|
|
5
|
+
* disabled until a destination other than the assets' current folder is chosen.
|
|
6
|
+
* The parent owns `open` and performs the move in `onMove`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function MoveAssetsDialog({ open, onOpenChange, folders, count, currentFolderId, onMove }: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
folders: MediaFolder[];
|
|
12
|
+
/** Number of assets being moved (for the description). */
|
|
13
|
+
count: number;
|
|
14
|
+
/** Folder the assets currently live in — preselected, and a no-op target. */
|
|
15
|
+
currentFolderId: string;
|
|
16
|
+
onMove: (folderId: string) => void;
|
|
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/MoveAssetsDialog/index.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAiB3D;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,eAAe,EACf,MAAM,EACT,EAAE;IACC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,+BA6EA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, RadioGroup } from '@orthacms/design-system';
|
|
5
|
+
import { Folder, Home } from 'lucide-react';
|
|
6
|
+
import { ROOT_FOLDER_ID } from '../../constants';
|
|
7
|
+
import { flattenFolderTree } from '../../utils/folderTree';
|
|
8
|
+
import { DestinationOption } from './DestinationOption';
|
|
9
|
+
/** Intl descriptors for {@link MoveAssetsDialog}, co-located. */
|
|
10
|
+
const messages = defineMessages({
|
|
11
|
+
title: { id: 'media.move.title', defaultMessage: 'Move items' },
|
|
12
|
+
description: {
|
|
13
|
+
id: 'media.move.description',
|
|
14
|
+
defaultMessage: 'Choose a destination for {count, plural, one {# item} other {# items}}.'
|
|
15
|
+
},
|
|
16
|
+
root: { id: 'media.move.root', defaultMessage: 'All media' },
|
|
17
|
+
cancel: { id: 'media.move.cancel', defaultMessage: 'Cancel' },
|
|
18
|
+
move: { id: 'media.move.confirm', defaultMessage: 'Move here' }
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Modal that picks a destination folder for the given assets. Offers the root
|
|
22
|
+
* ("All media") plus every folder as an indented radio list; the confirm is
|
|
23
|
+
* disabled until a destination other than the assets' current folder is chosen.
|
|
24
|
+
* The parent owns `open` and performs the move in `onMove`.
|
|
25
|
+
*/
|
|
26
|
+
export function MoveAssetsDialog({ open, onOpenChange, folders, count, currentFolderId, onMove }) {
|
|
27
|
+
const intl = useIntl();
|
|
28
|
+
const [target, setTarget] = useState(currentFolderId);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (open)
|
|
31
|
+
setTarget(currentFolderId);
|
|
32
|
+
}, [open, currentFolderId]);
|
|
33
|
+
const tree = flattenFolderTree(folders);
|
|
34
|
+
const submit = () => {
|
|
35
|
+
if (target === currentFolderId)
|
|
36
|
+
return;
|
|
37
|
+
onMove(target);
|
|
38
|
+
onOpenChange(false);
|
|
39
|
+
};
|
|
40
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: intl.formatMessage(messages.title) }), _jsx(DialogDescription, { children: intl.formatMessage(messages.description, { count }) })] }), _jsxs(RadioGroup, { value: target, onValueChange: setTarget, className: "max-h-72 gap-0.5 overflow-auto rounded-lg border p-1", children: [_jsx(DestinationOption, { value: ROOT_FOLDER_ID, depth: 0, icon: _jsx(Home, { className: "size-4 text-muted-foreground", "aria-hidden": true }), label: intl.formatMessage(messages.root), current: currentFolderId === ROOT_FOLDER_ID }), tree.map(({ folder, depth }) => (_jsx(DestinationOption, { value: folder.id, depth: depth + 1, icon: _jsx(Folder, { className: "size-4 text-muted-foreground", "aria-hidden": true }), label: folder.name, current: currentFolderId === folder.id }, folder.id)))] }), _jsxs(DialogFooter, { className: "mt-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: intl.formatMessage(messages.cancel) }), _jsx(Button, { onClick: submit, disabled: target === currentFolderId, children: intl.formatMessage(messages.move) })] })] }) }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal that collects a name and creates a folder in the current location. The
|
|
3
|
+
* parent owns `open` and performs the create in `onCreate`; the field seeds
|
|
4
|
+
* empty on each open and the confirm is disabled until it is non-blank.
|
|
5
|
+
*/
|
|
6
|
+
export declare function NewFolderDialog({ open, onOpenChange, locationLabel, onCreate }: {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
/** Human name of the folder the new one lands in (e.g. "All media"). */
|
|
10
|
+
locationLabel: string;
|
|
11
|
+
onCreate: (name: string) => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/NewFolderDialog/index.tsx"],"names":[],"mappings":"AA8BA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,+BA2DA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, InputField } from '@orthacms/design-system';
|
|
5
|
+
/** Intl descriptors for {@link NewFolderDialog}, co-located. */
|
|
6
|
+
const messages = defineMessages({
|
|
7
|
+
title: { id: 'media.newFolder.title', defaultMessage: 'New folder' },
|
|
8
|
+
description: {
|
|
9
|
+
id: 'media.newFolder.description',
|
|
10
|
+
defaultMessage: 'Folders help you organise assets. It will be created in {location}.'
|
|
11
|
+
},
|
|
12
|
+
label: { id: 'media.newFolder.label', defaultMessage: 'Folder name' },
|
|
13
|
+
placeholder: {
|
|
14
|
+
id: 'media.newFolder.placeholder',
|
|
15
|
+
defaultMessage: 'e.g. Campaign assets'
|
|
16
|
+
},
|
|
17
|
+
cancel: { id: 'media.newFolder.cancel', defaultMessage: 'Cancel' },
|
|
18
|
+
create: { id: 'media.newFolder.create', defaultMessage: 'Create folder' }
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Modal that collects a name and creates a folder in the current location. The
|
|
22
|
+
* parent owns `open` and performs the create in `onCreate`; the field seeds
|
|
23
|
+
* empty on each open and the confirm is disabled until it is non-blank.
|
|
24
|
+
*/
|
|
25
|
+
export function NewFolderDialog({ open, onOpenChange, locationLabel, onCreate }) {
|
|
26
|
+
const intl = useIntl();
|
|
27
|
+
const [name, setName] = useState('');
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (open)
|
|
30
|
+
setName('');
|
|
31
|
+
}, [open]);
|
|
32
|
+
const trimmed = name.trim();
|
|
33
|
+
const submit = () => {
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return;
|
|
36
|
+
onCreate(trimmed);
|
|
37
|
+
onOpenChange(false);
|
|
38
|
+
};
|
|
39
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: intl.formatMessage(messages.title) }), _jsx(DialogDescription, { children: intl.formatMessage(messages.description, {
|
|
40
|
+
location: locationLabel
|
|
41
|
+
}) })] }), _jsxs("form", { onSubmit: (event) => {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
submit();
|
|
44
|
+
}, children: [_jsx(InputField, { id: "media-new-folder-name", label: intl.formatMessage(messages.label), placeholder: intl.formatMessage(messages.placeholder), value: name, autoFocus: true, onChange: (event) => setName(event.target.value) }), _jsxs(DialogFooter, { className: "mt-6", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: intl.formatMessage(messages.cancel) }), _jsx(Button, { type: "submit", disabled: !trimmed, children: intl.formatMessage(messages.create) })] })] })] }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single-field rename modal reused for assets and folders. The parent supplies
|
|
3
|
+
* the current name and performs the rename in `onRename`; the field seeds from
|
|
4
|
+
* `currentName` each time it opens and the confirm is disabled while blank or
|
|
5
|
+
* unchanged. Selecting the text on open makes an in-place edit quick.
|
|
6
|
+
*/
|
|
7
|
+
export declare function RenameDialog({ open, onOpenChange, currentName, onRename }: {
|
|
8
|
+
open: boolean;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
currentName: string;
|
|
11
|
+
onRename: (name: string) => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/RenameDialog/index.tsx"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,+BAuDA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, InputField } from '@orthacms/design-system';
|
|
5
|
+
/** Intl descriptors for {@link RenameDialog}, co-located. */
|
|
6
|
+
const messages = defineMessages({
|
|
7
|
+
title: { id: 'media.rename.title', defaultMessage: 'Rename' },
|
|
8
|
+
label: { id: 'media.rename.label', defaultMessage: 'Name' },
|
|
9
|
+
cancel: { id: 'media.rename.cancel', defaultMessage: 'Cancel' },
|
|
10
|
+
save: { id: 'media.rename.save', defaultMessage: 'Save' }
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* A single-field rename modal reused for assets and folders. The parent supplies
|
|
14
|
+
* the current name and performs the rename in `onRename`; the field seeds from
|
|
15
|
+
* `currentName` each time it opens and the confirm is disabled while blank or
|
|
16
|
+
* unchanged. Selecting the text on open makes an in-place edit quick.
|
|
17
|
+
*/
|
|
18
|
+
export function RenameDialog({ open, onOpenChange, currentName, onRename }) {
|
|
19
|
+
const intl = useIntl();
|
|
20
|
+
const [name, setName] = useState(currentName);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (open)
|
|
23
|
+
setName(currentName);
|
|
24
|
+
}, [open, currentName]);
|
|
25
|
+
const trimmed = name.trim();
|
|
26
|
+
const changed = trimmed.length > 0 && trimmed !== currentName;
|
|
27
|
+
const submit = () => {
|
|
28
|
+
if (!changed)
|
|
29
|
+
return;
|
|
30
|
+
onRename(trimmed);
|
|
31
|
+
onOpenChange(false);
|
|
32
|
+
};
|
|
33
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: intl.formatMessage(messages.title) }) }), _jsxs("form", { onSubmit: (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
submit();
|
|
36
|
+
}, children: [_jsx(InputField, { id: "media-rename-name", label: intl.formatMessage(messages.label), value: name, autoFocus: true, onFocus: (event) => event.target.select(), onChange: (event) => setName(event.target.value) }), _jsxs(DialogFooter, { className: "mt-6", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: intl.formatMessage(messages.cancel) }), _jsx(Button, { type: "submit", disabled: !changed, children: intl.formatMessage(messages.save) })] })] })] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One staged (not yet uploaded) file in the upload dialog: a real thumbnail for
|
|
3
|
+
* an image, a kind glyph for anything else, the name + size, a remove button,
|
|
4
|
+
* and — when the caller collects it — an **Alt text** input for an image.
|
|
5
|
+
*
|
|
6
|
+
* Alt is asked for *here* rather than left to a later edit because this is the
|
|
7
|
+
* one moment the author is looking at the picture. The field is optional and
|
|
8
|
+
* never blocks the upload: an author who skips it lands where they landed
|
|
9
|
+
* before, and can still describe the asset from its detail drawer.
|
|
10
|
+
*
|
|
11
|
+
* The thumbnail is an **object URL** over the local `File` — no upload, no
|
|
12
|
+
* network. It is revoked when the row unmounts or the file changes; skipping
|
|
13
|
+
* that leaks the whole blob for the tab's lifetime, which matters when someone
|
|
14
|
+
* stages a few hundred megabytes of video and then removes it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function StagedFileRow({ file, alt, collectAlt, onAltChange, onRemove }: {
|
|
17
|
+
file: File;
|
|
18
|
+
/** Current alt text for this staged file. */
|
|
19
|
+
alt?: string;
|
|
20
|
+
/** Renders the alt input (images only); off for callers that drop it. */
|
|
21
|
+
collectAlt?: boolean;
|
|
22
|
+
onAltChange?: (alt: string) => void;
|
|
23
|
+
onRemove: () => void;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/UploadDialog/StagedFileRow/index.tsx"],"names":[],"mappings":"AAiDA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,EAC1B,IAAI,EACJ,GAAQ,EACR,UAAkB,EAClB,WAAW,EACX,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,IAAI,CAAC;IACX,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB,+BAyEA"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Button, InputField } from '@orthacms/design-system';
|
|
5
|
+
import { FileText, Film, Music, Package, X } from 'lucide-react';
|
|
6
|
+
import { formatBytes } from '../../../utils/formatBytes';
|
|
7
|
+
/** Intl descriptors for {@link StagedFileRow}, co-located. */
|
|
8
|
+
const messages = defineMessages({
|
|
9
|
+
remove: { id: 'media.upload.removeFile', defaultMessage: 'Remove {name}' },
|
|
10
|
+
altLabel: { id: 'media.upload.altLabel', defaultMessage: 'Alt text' },
|
|
11
|
+
/**
|
|
12
|
+
* The accessible name, which repeats the file so a run of staged rows isn't
|
|
13
|
+
* five identically-named inputs. It **contains** the visible label, which is
|
|
14
|
+
* what WCAG 2.5.3 (Label in Name) asks for.
|
|
15
|
+
*/
|
|
16
|
+
altLabelFor: {
|
|
17
|
+
id: 'media.upload.altLabelFor',
|
|
18
|
+
defaultMessage: 'Alt text for {name}'
|
|
19
|
+
},
|
|
20
|
+
altPlaceholder: {
|
|
21
|
+
id: 'media.upload.altPlaceholder',
|
|
22
|
+
defaultMessage: 'Describe this image'
|
|
23
|
+
},
|
|
24
|
+
altHint: {
|
|
25
|
+
id: 'media.upload.altHint',
|
|
26
|
+
defaultMessage: 'Optional. Leave blank only if the image is decorative — you can add it later from the asset’s details.'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* The glyph shown for a non-image file, chosen from its MIME type. Deliberately
|
|
31
|
+
* a **local** coarse mapping and not a shared `kindFromMime`: the server owns
|
|
32
|
+
* the real classification (it's what ends up on the row), and this only has to
|
|
33
|
+
* pick an icon for a file that hasn't been uploaded yet.
|
|
34
|
+
*/
|
|
35
|
+
function glyphFor(type) {
|
|
36
|
+
if (type.startsWith('video/'))
|
|
37
|
+
return Film;
|
|
38
|
+
if (type.startsWith('audio/'))
|
|
39
|
+
return Music;
|
|
40
|
+
if (type === 'application/zip' ||
|
|
41
|
+
type === 'application/x-tar' ||
|
|
42
|
+
type === 'application/gzip') {
|
|
43
|
+
return Package;
|
|
44
|
+
}
|
|
45
|
+
return FileText;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One staged (not yet uploaded) file in the upload dialog: a real thumbnail for
|
|
49
|
+
* an image, a kind glyph for anything else, the name + size, a remove button,
|
|
50
|
+
* and — when the caller collects it — an **Alt text** input for an image.
|
|
51
|
+
*
|
|
52
|
+
* Alt is asked for *here* rather than left to a later edit because this is the
|
|
53
|
+
* one moment the author is looking at the picture. The field is optional and
|
|
54
|
+
* never blocks the upload: an author who skips it lands where they landed
|
|
55
|
+
* before, and can still describe the asset from its detail drawer.
|
|
56
|
+
*
|
|
57
|
+
* The thumbnail is an **object URL** over the local `File` — no upload, no
|
|
58
|
+
* network. It is revoked when the row unmounts or the file changes; skipping
|
|
59
|
+
* that leaks the whole blob for the tab's lifetime, which matters when someone
|
|
60
|
+
* stages a few hundred megabytes of video and then removes it.
|
|
61
|
+
*/
|
|
62
|
+
export function StagedFileRow({ file, alt = '', collectAlt = false, onAltChange, onRemove }) {
|
|
63
|
+
const intl = useIntl();
|
|
64
|
+
const altId = useId();
|
|
65
|
+
const [previewUrl, setPreviewUrl] = useState(null);
|
|
66
|
+
const isImage = file.type.startsWith('image/');
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isImage) {
|
|
69
|
+
setPreviewUrl(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const url = URL.createObjectURL(file);
|
|
73
|
+
setPreviewUrl(url);
|
|
74
|
+
return () => URL.revokeObjectURL(url);
|
|
75
|
+
}, [file, isImage]);
|
|
76
|
+
const Glyph = glyphFor(file.type);
|
|
77
|
+
return (_jsxs("li", { className: "flex flex-col gap-2 rounded-lg border bg-background px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md bg-muted", children: previewUrl ? (_jsx("img", { src: previewUrl, alt: "", className: "size-full object-cover" })) : (_jsx(Glyph, { className: "size-4 text-muted-foreground", "aria-hidden": true })) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: file.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: formatBytes(file.size) }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.remove, {
|
|
78
|
+
name: file.name
|
|
79
|
+
}), onClick: onRemove, children: _jsx(X, { "aria-hidden": true }) })] }), collectAlt && isImage ? (_jsx(InputField, { id: `staged-alt-${altId}`, label: intl.formatMessage(messages.altLabel), "aria-label": intl.formatMessage(messages.altLabelFor, {
|
|
80
|
+
name: file.name
|
|
81
|
+
}), description: intl.formatMessage(messages.altHint), placeholder: intl.formatMessage(messages.altPlaceholder), value: alt, maxLength: 1000, onChange: (event) => onAltChange?.(event.target.value), fieldClassName: "gap-1", className: "h-8 text-sm" })) : null] }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StagedUpload } from '../../infrastructure/mediaGateway';
|
|
2
|
+
/**
|
|
3
|
+
* The upload modal — a drag-and-drop zone plus a multi-file picker that stages
|
|
4
|
+
* the chosen files into a removable, **previewed** list before committing:
|
|
5
|
+
* images render a real thumbnail off a local object URL (see
|
|
6
|
+
* {@link StagedFileRow}), other kinds a glyph, so a wrong file is caught before
|
|
7
|
+
* a byte moves. Files accumulate across several drops/picks.
|
|
8
|
+
*
|
|
9
|
+
* On confirm the parent's `onUpload` receives the real `File` objects and the
|
|
10
|
+
* dialog closes immediately — progress then lives in the caller's upload banner,
|
|
11
|
+
* not here, so the user can keep browsing (and queue more) while bytes move.
|
|
12
|
+
* Staged files reset whenever the dialog reopens, seeded from `initialFiles`.
|
|
13
|
+
*
|
|
14
|
+
* **Shared by both upload entry points** — the Media Library's toolbar and a
|
|
15
|
+
* content record's media field — so staging, previewing, and the copy are
|
|
16
|
+
* identical in the two places. The field narrows it with `multiple` / `accept` /
|
|
17
|
+
* its own `description` + `hint`; everything else is the same dialog.
|
|
18
|
+
*/
|
|
19
|
+
export declare function UploadDialog({ open, onOpenChange, locationLabel, description, hint, confirmLabel, accept, multiple, initialFiles, collectAlt, onUpload }: {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
/** Human name of the destination folder (e.g. "All media"). */
|
|
23
|
+
locationLabel: string;
|
|
24
|
+
/** Overrides the default "Files upload to {location}." sentence. */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Overrides the default size/type hint under the browse button. */
|
|
27
|
+
hint?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Overrides the confirm button's "Upload {n} files". A media field stages the
|
|
30
|
+
* files (they upload with the record), so there the button attaches.
|
|
31
|
+
*/
|
|
32
|
+
confirmLabel?: string;
|
|
33
|
+
/** `accept` attribute for the file input (best-effort; the server enforces). */
|
|
34
|
+
accept?: string;
|
|
35
|
+
/** False stages exactly one file — a later pick replaces it. */
|
|
36
|
+
multiple?: boolean;
|
|
37
|
+
/** Files to stage as the dialog opens (e.g. dropped onto a media field). */
|
|
38
|
+
initialFiles?: File[];
|
|
39
|
+
/**
|
|
40
|
+
* Offers an **Alt text** input on each staged image, sent with the upload.
|
|
41
|
+
*
|
|
42
|
+
* Off by default, and deliberately: only the library's queue forwards the
|
|
43
|
+
* description to the API. A caller that would drop it (a media field, whose
|
|
44
|
+
* files upload later with the record) must not ask for it — prompting for a
|
|
45
|
+
* description and then discarding it is worse than never asking.
|
|
46
|
+
*/
|
|
47
|
+
collectAlt?: boolean;
|
|
48
|
+
onUpload: (uploads: StagedUpload[]) => void;
|
|
49
|
+
}): import("react").JSX.Element;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/UploadDialog/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAmDtE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,WAAW,EACX,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,QAAe,EACf,YAAuB,EACvB,UAAkB,EAClB,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC;IACtB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;CAC/C,+BAmJA"}
|