@jini-ai/admin 0.4.12 → 0.6.10
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/CHANGELOG.md +70 -2
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +76 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/react/components/MediaRefField.d.ts +1 -1
- package/dist/seo/react/components/MediaRefField.js +19 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
- package/dist/seo/react/pages/SeoPage.d.ts +4 -4
- package/dist/seo/react/pages/SeoPage.js +26 -6
- package/dist/seo/rules.js +39 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +23 -14
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -4,9 +4,6 @@ export declare function ReloadIcon(_required: Record<string, never>, _optional?:
|
|
|
4
4
|
export declare function HiddenEyeIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
5
5
|
export declare function EyeIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
6
6
|
export declare function ExpandIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
7
|
-
export declare function CloseIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
8
|
-
export declare function PreviousIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
9
|
-
export declare function NextIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
10
7
|
export declare function AllIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
11
8
|
export declare function ImageIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
12
9
|
export declare function VideoIcon(_required: Record<string, never>, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
@@ -15,15 +15,6 @@ export function EyeIcon(_required, _optional = {}) {
|
|
|
15
15
|
export function ExpandIcon(_required, _optional = {}) {
|
|
16
16
|
return _jsx("svg", { className: "jini-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { className: "jini-icon-path", d: "M6 2H2v4m10-4h4v4M2 12v4h4m10-4v4h-4" }) });
|
|
17
17
|
}
|
|
18
|
-
export function CloseIcon(_required, _optional = {}) {
|
|
19
|
-
return _jsx("svg", { className: "jini-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { className: "jini-icon-path", d: "m4 4 10 10M14 4 4 14" }) });
|
|
20
|
-
}
|
|
21
|
-
export function PreviousIcon(_required, _optional = {}) {
|
|
22
|
-
return _jsx("svg", { className: "jini-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { className: "jini-icon-path", d: "m11 4-5 5 5 5" }) });
|
|
23
|
-
}
|
|
24
|
-
export function NextIcon(_required, _optional = {}) {
|
|
25
|
-
return _jsx("svg", { className: "jini-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { className: "jini-icon-path", d: "m7 4 5 5-5 5" }) });
|
|
26
|
-
}
|
|
27
18
|
export function AllIcon(_required, _optional = {}) {
|
|
28
19
|
return _jsxs("svg", { className: "jini-icon", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { className: "jini-icon-rect", x: "4", y: "4", width: "6.5", height: "6.5", rx: "1.2" }), _jsx("rect", { className: "jini-icon-rect", x: "13.5", y: "4", width: "6.5", height: "6.5", rx: "1.2" }), _jsx("rect", { className: "jini-icon-rect", x: "4", y: "13.5", width: "6.5", height: "6.5", rx: "1.2" }), _jsx("rect", { className: "jini-icon-rect", x: "13.5", y: "13.5", width: "6.5", height: "6.5", rx: "1.2" })] });
|
|
29
20
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MediaLightboxProps } from '../hooks/MediaLightbox.hooks.js';
|
|
2
2
|
/** One shared dialog for the grid: mounting N real dialogs to show at most one open
|
|
3
|
-
* at a time buys nothing and costs N DOM subtrees.
|
|
3
|
+
* at a time buys nothing and costs N DOM subtrees. Chrome, keyboard and focus come from
|
|
4
|
+
* the shared `Lightbox`; this only supplies the asset and its agent-addressable parts. */
|
|
4
5
|
export declare function MediaLightbox(props: MediaLightboxProps, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
5
6
|
//# sourceMappingURL=MediaLightbox.d.ts.map
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Lightbox } from '@jini-ai/ui/admin-widgets';
|
|
3
3
|
import { useMediaLightbox } from '../hooks/MediaLightbox.hooks.js';
|
|
4
|
-
import { CloseIcon, PreviousIcon, NextIcon } from './MediaIcons.js';
|
|
5
4
|
import { MediaPreview } from './MediaCard.js';
|
|
6
5
|
/** One shared dialog for the grid: mounting N real dialogs to show at most one open
|
|
7
|
-
* at a time buys nothing and costs N DOM subtrees.
|
|
6
|
+
* at a time buys nothing and costs N DOM subtrees. Chrome, keyboard and focus come from
|
|
7
|
+
* the shared `Lightbox`; this only supplies the asset and its agent-addressable parts. */
|
|
8
8
|
export function MediaLightbox(props, _optional = {}) {
|
|
9
9
|
const vm = useMediaLightbox(props);
|
|
10
|
-
return (
|
|
10
|
+
return (_jsx(Lightbox, { ...vm.lightbox, className: "jini-media-lightbox", labels: { close: 'Close', previous: 'Previous', next: 'Next' }, partAttrs: MEDIA_LIGHTBOX_PARTS, children: vm.item && _jsx("div", { className: "jini-media-lightbox-media", children: _jsx(MediaPreview, { item: vm.item, api: props.api }, vm.item.id) }) }));
|
|
11
11
|
}
|
|
12
|
+
const MEDIA_LIGHTBOX_PARTS = {
|
|
13
|
+
dialog: { 'data-jini-part': 'media.lightbox', 'data-jini-size': 'large' },
|
|
14
|
+
close: { 'data-jini-part': 'media.lightbox.close', 'data-agent-element': 'media-lightbox-close' },
|
|
15
|
+
previous: { 'data-jini-part': 'media.lightbox.previous', 'data-agent-element': 'media-lightbox-prev' },
|
|
16
|
+
next: { 'data-jini-part': 'media.lightbox.next', 'data-agent-element': 'media-lightbox-next' },
|
|
17
|
+
};
|
|
12
18
|
//# sourceMappingURL=MediaLightbox.js.map
|
|
@@ -1,19 +1,65 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
1
2
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
3
|
import { createLibraryController } from '../../controllers/library.controller.js';
|
|
3
4
|
import type { MediaAsset } from '../../models.js';
|
|
5
|
+
import type { LibraryState } from '../../controllers/library.controller.js';
|
|
6
|
+
import type { MediaApiPort } from '../../ports.js';
|
|
7
|
+
type LibraryController = ReturnType<typeof createLibraryController>;
|
|
4
8
|
export declare function useLibraryTab({ params, permissions }: TabViewProps, _optional?: Record<string, never>): {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
purgeDialog: {
|
|
10
|
+
filename?: string;
|
|
11
|
+
open: boolean;
|
|
12
|
+
pending: boolean;
|
|
13
|
+
};
|
|
14
|
+
upload({ input, alt }: Parameters<LibraryController["upload"]>[0]): Promise<boolean>;
|
|
15
|
+
cancelPurge: () => void | undefined;
|
|
16
|
+
confirmPurge: () => Promise<void> | undefined;
|
|
17
|
+
t: {
|
|
18
|
+
readonly media: "Media";
|
|
19
|
+
readonly all: "All";
|
|
20
|
+
readonly images: "Images";
|
|
21
|
+
readonly videos: "Videos";
|
|
22
|
+
readonly providers: "External Providers";
|
|
23
|
+
readonly upload: "Upload";
|
|
24
|
+
readonly title: "Title";
|
|
25
|
+
readonly alt: "Alt text";
|
|
26
|
+
readonly save: "Save";
|
|
27
|
+
readonly cancel: "Cancel";
|
|
28
|
+
readonly empty: "No media found";
|
|
29
|
+
readonly loading: "Loading media…";
|
|
30
|
+
readonly choose: "Choose media";
|
|
31
|
+
readonly trash: "Trash";
|
|
32
|
+
readonly purge: "Delete permanently";
|
|
33
|
+
readonly purgeWarning: "This permanently deletes the file. This cannot be undone.";
|
|
34
|
+
readonly edit: "Edit metadata";
|
|
35
|
+
readonly preview: "View larger";
|
|
36
|
+
readonly replace: "Replace file";
|
|
37
|
+
readonly unavailable: "Provider service unavailable";
|
|
38
|
+
readonly untyped: "Some assets have no recorded content type and only appear under All.";
|
|
39
|
+
};
|
|
40
|
+
loading: boolean;
|
|
41
|
+
busy: boolean;
|
|
42
|
+
untyped: boolean;
|
|
10
43
|
statusFilter: string;
|
|
44
|
+
orderBy: import("../../models.js").MediaOrderBy;
|
|
45
|
+
search: string;
|
|
11
46
|
onStatus: ({ value }: {
|
|
12
47
|
value: string;
|
|
13
48
|
}) => void;
|
|
49
|
+
onSearch: ({ value: search }: {
|
|
50
|
+
value: string;
|
|
51
|
+
}) => void;
|
|
52
|
+
onOrder: ({ value }: {
|
|
53
|
+
value: string;
|
|
54
|
+
}) => void;
|
|
55
|
+
emptyLabel: string;
|
|
56
|
+
canUpload: boolean;
|
|
57
|
+
success: string | null;
|
|
58
|
+
gridRef: RefObject<HTMLDivElement | null>;
|
|
59
|
+
onRetry: () => void;
|
|
14
60
|
canPurge: boolean;
|
|
15
61
|
controller: {
|
|
16
|
-
getSnapshot: () => Readonly<
|
|
62
|
+
getSnapshot: () => Readonly<LibraryState>;
|
|
17
63
|
subscribe: ({ listener }: {
|
|
18
64
|
listener: () => void;
|
|
19
65
|
}) => () => void;
|
|
@@ -44,7 +90,7 @@ export declare function useLibraryTab({ params, permissions }: TabViewProps, _op
|
|
|
44
90
|
}, _optional?: Record<string, never>): Promise<boolean>;
|
|
45
91
|
dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
|
|
46
92
|
} | null;
|
|
47
|
-
snapshot:
|
|
93
|
+
snapshot: LibraryState | null;
|
|
48
94
|
actions: {
|
|
49
95
|
onPurge?: () => void | undefined;
|
|
50
96
|
onRestore?: () => void;
|
|
@@ -55,7 +101,7 @@ export declare function useLibraryTab({ params, permissions }: TabViewProps, _op
|
|
|
55
101
|
busy: boolean;
|
|
56
102
|
}[];
|
|
57
103
|
items: readonly MediaAsset[];
|
|
58
|
-
mediaApi:
|
|
104
|
+
mediaApi: MediaApiPort;
|
|
59
105
|
editing: MediaAsset | null;
|
|
60
106
|
closeEdit: () => void;
|
|
61
107
|
saved: ({ message }?: {
|
|
@@ -64,42 +110,6 @@ export declare function useLibraryTab({ params, permissions }: TabViewProps, _op
|
|
|
64
110
|
lightboxIndex: number | null;
|
|
65
111
|
onNavigate: import("react").Dispatch<import("react").SetStateAction<number | null>>;
|
|
66
112
|
closeLightbox: () => void;
|
|
67
|
-
onSearch: ({ value: search }: {
|
|
68
|
-
value: string;
|
|
69
|
-
}) => void;
|
|
70
|
-
orderBy: import("../../models.js").MediaOrderBy;
|
|
71
|
-
onOrder: ({ value }: {
|
|
72
|
-
value: string;
|
|
73
|
-
}) => void;
|
|
74
|
-
search: string;
|
|
75
|
-
loading: boolean;
|
|
76
|
-
busy: boolean;
|
|
77
|
-
untyped: boolean;
|
|
78
|
-
upload({ input, alt }: Parameters<ReturnType<typeof createLibraryController>["upload"]>[0]): Promise<boolean>;
|
|
79
|
-
cancelPurge: () => void | undefined;
|
|
80
|
-
confirmPurge: () => Promise<void> | undefined;
|
|
81
|
-
t: {
|
|
82
|
-
readonly media: "Media";
|
|
83
|
-
readonly all: "All";
|
|
84
|
-
readonly images: "Images";
|
|
85
|
-
readonly videos: "Videos";
|
|
86
|
-
readonly providers: "External Providers";
|
|
87
|
-
readonly upload: "Upload";
|
|
88
|
-
readonly title: "Title";
|
|
89
|
-
readonly alt: "Alt text";
|
|
90
|
-
readonly save: "Save";
|
|
91
|
-
readonly cancel: "Cancel";
|
|
92
|
-
readonly empty: "No media found";
|
|
93
|
-
readonly loading: "Loading media…";
|
|
94
|
-
readonly choose: "Choose media";
|
|
95
|
-
readonly trash: "Trash";
|
|
96
|
-
readonly purge: "Delete permanently";
|
|
97
|
-
readonly purgeWarning: "This permanently deletes the file. This cannot be undone.";
|
|
98
|
-
readonly edit: "Edit metadata";
|
|
99
|
-
readonly preview: "View larger";
|
|
100
|
-
readonly replace: "Replace file";
|
|
101
|
-
readonly unavailable: "Provider service unavailable";
|
|
102
|
-
readonly untyped: "Some assets have no recorded content type and only appear under All.";
|
|
103
|
-
};
|
|
104
113
|
};
|
|
114
|
+
export {};
|
|
105
115
|
//# sourceMappingURL=LibraryTab.hooks.d.ts.map
|
|
@@ -3,13 +3,104 @@ import { MEDIA_QUERY_KEYS, useMediaLibraryQuery } from './MediaQuery.hooks.js';
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import { useController } from '../../../core/react/use-controller.js';
|
|
5
5
|
import { createLibraryController } from '../../controllers/library.controller.js';
|
|
6
|
-
import { useSharedMediaLibrary } from './MediaPage.hooks.js';
|
|
6
|
+
import { settleFromCache, useSharedMediaLibrary } from './MediaPage.hooks.js';
|
|
7
7
|
import { useMediaPorts } from './MediaPorts.hooks.js';
|
|
8
8
|
import { mediaMessagesEn } from '../../messages.en.js';
|
|
9
9
|
import { canRestoreMedia } from '../../rules.js';
|
|
10
|
+
const EMPTY_LABELS = {
|
|
11
|
+
images: 'No images yet. Upload an image to see it here.',
|
|
12
|
+
videos: 'No videos yet. Upload a video to see it here.',
|
|
13
|
+
all: 'No media found',
|
|
14
|
+
};
|
|
15
|
+
function contentFilter({ params }, _optional = {}) {
|
|
16
|
+
return params.filter === 'images' || params.filter === 'videos' ? params.filter : 'all';
|
|
17
|
+
}
|
|
18
|
+
function libraryPermissions({ permissions, api }, _optional = {}) {
|
|
19
|
+
return {
|
|
20
|
+
canUpload: permissions.includes('media.upload'),
|
|
21
|
+
canEdit: permissions.includes('media.update'),
|
|
22
|
+
canTrash: permissions.includes('media.trash'),
|
|
23
|
+
canRestore: permissions.includes('media.restore') && canRestoreMedia({ api }),
|
|
24
|
+
canPurge: permissions.includes('media.delete.force'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Focuses the card with `id`, else whichever card now sits at `index`, else the grid itself. */
|
|
28
|
+
function focusCard({ grid, index }, { id } = {}) {
|
|
29
|
+
const cards = Array.from(grid?.querySelectorAll('[data-jini-part="media.card"]') ?? []);
|
|
30
|
+
const byId = id === undefined ? undefined : cards.find(card => card.dataset.mediaId === id);
|
|
31
|
+
const target = byId ?? cards[Math.min(index, cards.length - 1)] ?? grid;
|
|
32
|
+
target?.focus({ preventScroll: true });
|
|
33
|
+
}
|
|
34
|
+
function queryView({ snapshot }, _optional = {}) {
|
|
35
|
+
return {
|
|
36
|
+
statusFilter: snapshot?.query.status ?? 'all',
|
|
37
|
+
orderBy: snapshot?.query.orderBy ?? 'created',
|
|
38
|
+
search: snapshot?.query.search ?? '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function progressView({ snapshot, filter }, _optional = {}) {
|
|
42
|
+
return {
|
|
43
|
+
loading: !snapshot || (snapshot.items === null && snapshot.loading && !snapshot.error),
|
|
44
|
+
busy: snapshot?.busy ?? false,
|
|
45
|
+
untyped: filter !== 'all' && snapshot?.hasUntyped === true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function purgeDialogView({ snapshot }, _optional = {}) {
|
|
49
|
+
const pending = snapshot?.pendingPurge;
|
|
50
|
+
return { open: !!pending, pending: snapshot?.busy ?? false, ...(pending ? { filename: pending.title } : {}) };
|
|
51
|
+
}
|
|
52
|
+
function queryHandlers({ controller }, _optional = {}) {
|
|
53
|
+
return {
|
|
54
|
+
onStatus: ({ value }) => {
|
|
55
|
+
const { status: _status, ...query } = controller?.getSnapshot().query ?? {};
|
|
56
|
+
void controller?.setQuery({ query: { ...query, ...(value === 'active' || value === 'trashed' ? { status: value } : {}) } });
|
|
57
|
+
},
|
|
58
|
+
onSearch: ({ value: search }) => {
|
|
59
|
+
void controller?.setQuery({ query: { ...controller.getSnapshot().query, search } });
|
|
60
|
+
},
|
|
61
|
+
onOrder: ({ value }) => {
|
|
62
|
+
void controller?.setQuery({ query: { ...controller.getSnapshot().query, orderBy: value === 'alphabetical' ? 'alphabetical' : 'created' } });
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function cardActions({ items, pendingIds, can, controller, announce, onEdit, onPreview }, _optional = {}) {
|
|
67
|
+
return items.map((item, index) => {
|
|
68
|
+
const write = (run, verb) => {
|
|
69
|
+
void run?.then(saved => { if (saved)
|
|
70
|
+
announce({ message: `${verb} ${item.title}`, focus: { id: item.id, index } }); });
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
item,
|
|
74
|
+
busy: pendingIds.includes(item.id),
|
|
75
|
+
...(can.canEdit ? { onEdit: () => onEdit(item) } : {}),
|
|
76
|
+
onPreview: () => onPreview(index),
|
|
77
|
+
...(can.canTrash ? { onTrash: () => write(controller?.trash({ id: item.id }), 'Trashed') } : {}),
|
|
78
|
+
...(can.canRestore ? { onRestore: () => write(controller?.restore({ id: item.id }), 'Restored') } : {}),
|
|
79
|
+
...(can.canPurge ? { onPurge: () => controller?.requestPurge({ item }) } : {}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Once a write has settled and the grid re-rendered, return focus to the affected card. */
|
|
84
|
+
function useFocusAfterWrites({ gridRef, snapshot }, _optional = {}) {
|
|
85
|
+
const [focusAfterAction, setFocusAfterAction] = useState(null);
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
if (!focusAfterAction || snapshot?.busy || snapshot?.loading)
|
|
88
|
+
return;
|
|
89
|
+
focusCard({ grid: gridRef.current, index: focusAfterAction.index }, { id: focusAfterAction.id });
|
|
90
|
+
setFocusAfterAction(null);
|
|
91
|
+
}, [focusAfterAction, snapshot?.busy, snapshot?.loading, snapshot?.items]);
|
|
92
|
+
const [focusAfterPurge, setFocusAfterPurge] = useState(null);
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
if (focusAfterPurge === null || snapshot?.pendingPurge || snapshot?.busy)
|
|
95
|
+
return;
|
|
96
|
+
focusCard({ grid: gridRef.current, index: focusAfterPurge });
|
|
97
|
+
setFocusAfterPurge(null);
|
|
98
|
+
}, [focusAfterPurge, snapshot?.pendingPurge, snapshot?.busy, snapshot?.items]);
|
|
99
|
+
return { setFocusAfterAction, setFocusAfterPurge };
|
|
100
|
+
}
|
|
10
101
|
export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
11
102
|
const { mediaApi, mediaEvents } = useMediaPorts();
|
|
12
|
-
const filter =
|
|
103
|
+
const filter = contentFilter({ params });
|
|
13
104
|
const shared = useSharedMediaLibrary();
|
|
14
105
|
const cached = useMediaLibraryQuery({ api: mediaApi }, { enabled: !shared });
|
|
15
106
|
const invalidate = useInvalidate();
|
|
@@ -25,28 +116,10 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
25
116
|
const { controller, snapshot } = shared ?? local;
|
|
26
117
|
useEffect(() => !shared ? mediaEvents?.subscribe({ onRefresh: () => invalidate({ key: MEDIA_QUERY_KEYS.root }) }) : undefined, [mediaEvents, controller, shared, invalidate]);
|
|
27
118
|
useEffect(() => { if (!shared)
|
|
28
|
-
|
|
119
|
+
settleFromCache({ controller: local.controller, query: cached.query }); }, [shared, local.controller, cached.query.data, cached.query.error]);
|
|
29
120
|
const gridRef = useRef(null);
|
|
30
121
|
const [success, setSuccess] = useState(null);
|
|
31
|
-
const
|
|
32
|
-
useLayoutEffect(() => {
|
|
33
|
-
if (!focusAfterAction || snapshot?.busy || snapshot?.loading)
|
|
34
|
-
return;
|
|
35
|
-
const cards = Array.from(gridRef.current?.querySelectorAll('[data-jini-part="media.card"]') ?? []);
|
|
36
|
-
const target = cards.find(card => card.dataset.mediaId === focusAfterAction.id) ?? cards[Math.min(focusAfterAction.index, cards.length - 1)] ?? gridRef.current;
|
|
37
|
-
target?.focus({ preventScroll: true });
|
|
38
|
-
setFocusAfterAction(null);
|
|
39
|
-
}, [focusAfterAction, snapshot?.busy, snapshot?.loading, snapshot?.items]);
|
|
40
|
-
const [focusAfterPurge, setFocusAfterPurge] = useState(null);
|
|
41
|
-
useLayoutEffect(() => {
|
|
42
|
-
if (focusAfterPurge === null || snapshot?.pendingPurge || snapshot?.busy)
|
|
43
|
-
return;
|
|
44
|
-
const grid = gridRef.current;
|
|
45
|
-
const cards = grid?.querySelectorAll('[data-jini-part="media.card"]');
|
|
46
|
-
const target = cards?.[Math.min(focusAfterPurge, cards.length - 1)] ?? grid;
|
|
47
|
-
target?.focus({ preventScroll: true });
|
|
48
|
-
setFocusAfterPurge(null);
|
|
49
|
-
}, [focusAfterPurge, snapshot?.pendingPurge, snapshot?.busy, snapshot?.items]);
|
|
122
|
+
const { setFocusAfterAction, setFocusAfterPurge } = useFocusAfterWrites({ gridRef, snapshot });
|
|
50
123
|
useEffect(() => {
|
|
51
124
|
// A shared page controller survives tab unmounts. Re-entering after an error must
|
|
52
125
|
// be a recovery opportunity even when its cached filter already matches this tab.
|
|
@@ -58,32 +131,9 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
58
131
|
// buttons move the number without re-deriving "what's next" from an item reference.
|
|
59
132
|
const [lightboxIndex, setLightboxIndex] = useState(null);
|
|
60
133
|
const items = snapshot?.items ?? [], pendingIds = snapshot?.pendingIds ?? [];
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const canRestore = permissions.includes('media.restore') && canRestoreMedia({ api: mediaApi });
|
|
65
|
-
const canPurge = permissions.includes('media.delete.force');
|
|
66
|
-
const actions = items.map((item, index) => ({
|
|
67
|
-
item,
|
|
68
|
-
busy: pendingIds.includes(item.id),
|
|
69
|
-
...(canEdit ? { onEdit: () => setEditing(item) } : {}),
|
|
70
|
-
onPreview: () => setLightboxIndex(index),
|
|
71
|
-
...(canTrash
|
|
72
|
-
? {
|
|
73
|
-
onTrash: () => {
|
|
74
|
-
void controller?.trash({ id: item.id }).then(saved => { if (saved) {
|
|
75
|
-
setSuccess(`Trashed ${item.title}`);
|
|
76
|
-
setFocusAfterAction({ id: item.id, index });
|
|
77
|
-
} });
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
: {}),
|
|
81
|
-
...(canRestore ? { onRestore: () => { void controller?.restore({ id: item.id }).then(saved => { if (saved) {
|
|
82
|
-
setSuccess(`Restored ${item.title}`);
|
|
83
|
-
setFocusAfterAction({ id: item.id, index });
|
|
84
|
-
} }); } } : {}),
|
|
85
|
-
...(canPurge ? { onPurge: () => controller?.requestPurge({ item }) } : {}),
|
|
86
|
-
}));
|
|
134
|
+
const can = libraryPermissions({ permissions, api: mediaApi });
|
|
135
|
+
const announce = ({ message, focus }) => { setSuccess(message); setFocusAfterAction(focus); };
|
|
136
|
+
const actions = cardActions({ items, pendingIds, can, controller, announce, onEdit: setEditing, onPreview: setLightboxIndex });
|
|
87
137
|
function closeEdit() {
|
|
88
138
|
setEditing(null);
|
|
89
139
|
}
|
|
@@ -99,18 +149,12 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
99
149
|
setFocusAfterAction({ id: item.id, index: Math.max(0, items.findIndex(row => row.id === item.id)) });
|
|
100
150
|
}
|
|
101
151
|
return {
|
|
102
|
-
emptyLabel: filter
|
|
103
|
-
|
|
104
|
-
canUpload,
|
|
152
|
+
emptyLabel: EMPTY_LABELS[filter],
|
|
153
|
+
canUpload: can.canUpload,
|
|
105
154
|
success,
|
|
106
155
|
gridRef,
|
|
107
156
|
onRetry: () => { void controller?.load(); },
|
|
108
|
-
|
|
109
|
-
onStatus: ({ value }) => {
|
|
110
|
-
const { status: _status, ...query } = controller?.getSnapshot().query ?? {};
|
|
111
|
-
void controller?.setQuery({ query: { ...query, ...(value === 'active' || value === 'trashed' ? { status: value } : {}) } });
|
|
112
|
-
},
|
|
113
|
-
canPurge,
|
|
157
|
+
canPurge: can.canPurge,
|
|
114
158
|
controller,
|
|
115
159
|
snapshot,
|
|
116
160
|
actions,
|
|
@@ -122,19 +166,12 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
122
166
|
lightboxIndex,
|
|
123
167
|
onNavigate: setLightboxIndex,
|
|
124
168
|
closeLightbox: () => setLightboxIndex(null),
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
onOrder: ({ value }) => {
|
|
130
|
-
void controller?.setQuery({ query: { ...controller.getSnapshot().query, orderBy: value === 'alphabetical' ? 'alphabetical' : 'created' } });
|
|
131
|
-
},
|
|
132
|
-
search: snapshot?.query.search ?? '',
|
|
133
|
-
loading: !snapshot || (snapshot.items === null && snapshot.loading && !snapshot.error),
|
|
134
|
-
busy: snapshot?.busy ?? false,
|
|
135
|
-
untyped: filter !== 'all' && snapshot?.hasUntyped === true,
|
|
169
|
+
...queryHandlers({ controller }),
|
|
170
|
+
...queryView({ snapshot }),
|
|
171
|
+
...progressView({ snapshot, filter }),
|
|
172
|
+
purgeDialog: purgeDialogView({ snapshot }),
|
|
136
173
|
async upload({ input, alt }) {
|
|
137
|
-
if (!canUpload)
|
|
174
|
+
if (!can.canUpload)
|
|
138
175
|
return false;
|
|
139
176
|
const result = await controller?.upload({ input, ...(alt === undefined ? {} : { alt }) }) ?? false;
|
|
140
177
|
if (result)
|
|
@@ -145,7 +182,7 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
145
182
|
confirmPurge: () => {
|
|
146
183
|
const item = controller?.getSnapshot().pendingPurge;
|
|
147
184
|
// The controller refuses to purge without a pending item, so there is nothing to announce.
|
|
148
|
-
if (!canPurge || !controller || !item)
|
|
185
|
+
if (!can.canPurge || !controller || !item)
|
|
149
186
|
return;
|
|
150
187
|
const index = items.findIndex(row => row.id === item.id);
|
|
151
188
|
return controller.confirmPurge({ confirmed: true }).then(saved => {
|
|
@@ -13,14 +13,26 @@ export interface MediaCardProps {
|
|
|
13
13
|
readonly onPurge?: () => void;
|
|
14
14
|
readonly onChoose?: () => void;
|
|
15
15
|
}
|
|
16
|
+
type PreviewStage = 'image' | 'video' | 'unsupported';
|
|
16
17
|
export declare function useMediaCard(props: MediaCardProps, _optional?: Record<string, never>): {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
statusClassName: string;
|
|
19
|
+
statusLabel: "active" | "trashed";
|
|
20
|
+
statusTone: "warning" | "success";
|
|
21
|
+
trashed: boolean;
|
|
22
|
+
menuItems: {
|
|
23
|
+
onPress: () => void;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
attrs?: {
|
|
26
|
+
'data-jini-variant': string;
|
|
27
|
+
};
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}[];
|
|
31
|
+
src: string | undefined;
|
|
32
|
+
stage: PreviewStage;
|
|
33
|
+
onImageError: () => void;
|
|
34
|
+
onVideoError: () => void;
|
|
35
|
+
alt: string;
|
|
24
36
|
handles: {
|
|
25
37
|
expand: string;
|
|
26
38
|
edit: string;
|
|
@@ -43,32 +55,15 @@ export declare function useMediaCard(props: MediaCardProps, _optional?: Record<s
|
|
|
43
55
|
'data-agent-element': string;
|
|
44
56
|
'data-agent-label': string;
|
|
45
57
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
label: string;
|
|
49
|
-
onPress: () => void;
|
|
50
|
-
disabled: boolean;
|
|
51
|
-
attrs?: never;
|
|
52
|
-
} | {
|
|
53
|
-
id: string;
|
|
54
|
-
label: string;
|
|
55
|
-
attrs: {
|
|
56
|
-
'data-jini-variant': string;
|
|
57
|
-
};
|
|
58
|
-
onPress: () => void;
|
|
59
|
-
disabled: boolean;
|
|
60
|
-
})[];
|
|
61
|
-
src: string | undefined;
|
|
62
|
-
stage: "video" | "image" | "unsupported";
|
|
58
|
+
canPreview: boolean;
|
|
59
|
+
imagePreview: boolean;
|
|
63
60
|
image: boolean | "" | undefined;
|
|
64
61
|
video: boolean | "" | undefined;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
statusTone: "warning" | "success";
|
|
71
|
-
trashed: boolean;
|
|
62
|
+
preview: () => void | undefined;
|
|
63
|
+
byteSize: string | null;
|
|
64
|
+
uploadDate: string | null;
|
|
65
|
+
metadataSeparator: string;
|
|
66
|
+
busy: boolean;
|
|
72
67
|
item: MediaAsset;
|
|
73
68
|
handleBase?: string;
|
|
74
69
|
locale?: string;
|
|
@@ -80,4 +75,5 @@ export declare function useMediaCard(props: MediaCardProps, _optional?: Record<s
|
|
|
80
75
|
onPurge?: () => void;
|
|
81
76
|
onChoose?: () => void;
|
|
82
77
|
};
|
|
78
|
+
export {};
|
|
83
79
|
//# sourceMappingURL=MediaCard.hooks.d.ts.map
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { safeMediaUrl, formatByteSize, formatUploadDate, mediaRowHandles } from '../../rules.js';
|
|
3
|
+
function menuItems({ props }, _optional = {}) {
|
|
4
|
+
const disabled = props.busy === true, trashed = props.item.status === 'trashed';
|
|
5
|
+
const entries = [
|
|
6
|
+
{ id: 'edit', label: 'Edit metadata', onPress: props.onEdit, shown: true },
|
|
7
|
+
{ id: 'trash', label: 'Trash', onPress: props.onTrash, shown: !trashed },
|
|
8
|
+
// Restore belongs only in a trashed card's menu; active cards have nothing to restore.
|
|
9
|
+
{ id: 'restore', label: 'Restore', onPress: props.onRestore, shown: trashed },
|
|
10
|
+
{ id: 'purge', label: 'Delete permanently', onPress: props.onPurge, shown: trashed },
|
|
11
|
+
];
|
|
12
|
+
return entries.flatMap(({ id, label, onPress, shown }) => onPress && shown
|
|
13
|
+
? [{ id, label, ...(id === 'purge' ? { attrs: { 'data-jini-variant': 'danger' } } : {}), onPress, disabled }]
|
|
14
|
+
: []);
|
|
15
|
+
}
|
|
16
|
+
function metadata({ item }, { locale = 'en-US' } = {}) {
|
|
17
|
+
const byteSize = item.byteSize === undefined ? null : formatByteSize({ bytes: item.byteSize }, { locale });
|
|
18
|
+
const uploadDate = formatUploadDate({ createdAt: item.createdAt }, { locale });
|
|
19
|
+
return { byteSize, uploadDate, metadataSeparator: byteSize && uploadDate ? ' · ' : '' };
|
|
20
|
+
}
|
|
21
|
+
function previewFlags({ src, stage }, { onPreview } = {}) {
|
|
22
|
+
const previewable = !!onPreview && !!src;
|
|
23
|
+
return {
|
|
24
|
+
canPreview: previewable && stage !== 'unsupported',
|
|
25
|
+
imagePreview: previewable && stage === 'image',
|
|
26
|
+
image: src && stage === 'image',
|
|
27
|
+
video: src && stage === 'video',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function statusView({ item }, _optional = {}) {
|
|
31
|
+
const trashed = item.status === 'trashed';
|
|
32
|
+
return {
|
|
33
|
+
statusClassName: `jini-status jini-status-${item.status}`,
|
|
34
|
+
statusLabel: item.status,
|
|
35
|
+
statusTone: trashed ? 'warning' : 'success',
|
|
36
|
+
trashed,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function agentAttrs({ handleBase, title }, _optional = {}) {
|
|
40
|
+
return {
|
|
41
|
+
handles: { expand: `${handleBase}-expand`, edit: `${handleBase}-edit`, menu: `${handleBase}-menu` },
|
|
42
|
+
previewAttrs: { 'data-jini-part': 'media.preview', 'data-agent-element': `${handleBase}-expand`,
|
|
43
|
+
'data-agent-label': 'Open this asset larger in the lightbox', 'aria-label': `View "${title}" larger` },
|
|
44
|
+
editAttrs: { 'data-jini-part': 'media.edit', 'data-agent-element': `${handleBase}-edit`,
|
|
45
|
+
'data-agent-label': 'Open the edit form for this asset as a modal', 'aria-label': `Edit "${title}"` },
|
|
46
|
+
menuLabel: `Actions for "${title}"`,
|
|
47
|
+
menuAttrs: { 'data-agent-element': `${handleBase}-menu`, 'data-agent-label': `Actions for "${title}"` },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
3
50
|
export function useMediaCard(props, _optional = {}) {
|
|
4
51
|
/** Preview optimistically: image -> video -> placeholder. The list supplies no reliable media
|
|
5
52
|
* kind, and the byte route guarantees GET rather than HEAD, so probing headers would assume an
|
|
@@ -12,41 +59,20 @@ export function useMediaCard(props, _optional = {}) {
|
|
|
12
59
|
// immediately when the versioned URL changes, including in an already-open lightbox.
|
|
13
60
|
const stage = probe.src === src ? probe.stage : 'image';
|
|
14
61
|
const handleBase = props.handleBase ?? mediaRowHandles({ media: [props.item] })[0];
|
|
15
|
-
const byteSize = props.item.byteSize === undefined ? null : formatByteSize({ bytes: props.item.byteSize }, { locale: props.locale ?? 'en-US' });
|
|
16
|
-
const uploadDate = formatUploadDate({ createdAt: props.item.createdAt }, { locale: props.locale ?? 'en-US' });
|
|
17
62
|
return {
|
|
18
63
|
...props,
|
|
19
64
|
busy: props.busy === true,
|
|
20
|
-
|
|
21
|
-
metadataSeparator: byteSize && uploadDate ? ' · ' : '',
|
|
65
|
+
...metadata({ item: props.item }, { locale: props.locale }),
|
|
22
66
|
preview: () => props.onPreview?.(),
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
previewAttrs: { 'data-jini-part': 'media.preview', 'data-agent-element': `${handleBase}-expand`,
|
|
27
|
-
'data-agent-label': 'Open this asset larger in the lightbox', 'aria-label': `View "${props.item.title}" larger` },
|
|
28
|
-
editAttrs: { 'data-jini-part': 'media.edit', 'data-agent-element': `${handleBase}-edit`,
|
|
29
|
-
'data-agent-label': 'Open the edit form for this asset as a modal', 'aria-label': `Edit "${props.item.title}"` },
|
|
30
|
-
menuLabel: `Actions for "${props.item.title}"`,
|
|
31
|
-
menuAttrs: { 'data-agent-element': `${handleBase}-menu`, 'data-agent-label': `Actions for "${props.item.title}"` },
|
|
32
|
-
menuItems: [
|
|
33
|
-
...(props.onEdit ? [{ id: 'edit', label: 'Edit metadata', onPress: props.onEdit, disabled: props.busy === true }] : []),
|
|
34
|
-
...(props.onTrash && props.item.status !== 'trashed' ? [{ id: 'trash', label: 'Trash', onPress: props.onTrash, disabled: props.busy === true }] : []),
|
|
35
|
-
// Restore belongs only in a trashed card's menu; active cards have nothing to restore.
|
|
36
|
-
...(props.onRestore && props.item.status === 'trashed' ? [{ id: 'restore', label: 'Restore', onPress: props.onRestore, disabled: props.busy === true }] : []),
|
|
37
|
-
...(props.onPurge && props.item.status === 'trashed' ? [{ id: 'purge', label: 'Delete permanently', attrs: { 'data-jini-variant': 'danger' }, onPress: props.onPurge, disabled: props.busy === true }] : []),
|
|
38
|
-
],
|
|
67
|
+
...previewFlags({ src, stage }, { onPreview: props.onPreview }),
|
|
68
|
+
...agentAttrs({ handleBase, title: props.item.title }),
|
|
69
|
+
menuItems: menuItems({ props }),
|
|
39
70
|
src,
|
|
40
71
|
stage,
|
|
41
|
-
image: src && stage === 'image',
|
|
42
|
-
video: src && stage === 'video',
|
|
43
72
|
onImageError: () => setProbe({ src, stage: 'video' }),
|
|
44
73
|
onVideoError: () => setProbe({ src, stage: 'unsupported' }),
|
|
45
74
|
alt: props.item.alt || props.item.title || 'Untitled asset',
|
|
46
|
-
|
|
47
|
-
statusLabel: props.item.status,
|
|
48
|
-
statusTone: props.item.status === 'trashed' ? 'warning' : 'success',
|
|
49
|
-
trashed: props.item.status === 'trashed',
|
|
75
|
+
...statusView({ item: props.item }),
|
|
50
76
|
};
|
|
51
77
|
}
|
|
52
78
|
//# sourceMappingURL=MediaCard.hooks.js.map
|
|
@@ -14,8 +14,8 @@ export declare function useMediaGrid(props: MediaGridProps, _optional?: Record<s
|
|
|
14
14
|
locale?: string;
|
|
15
15
|
busy?: boolean;
|
|
16
16
|
onPurge?: () => void;
|
|
17
|
-
onEdit?: () => void;
|
|
18
17
|
onPreview?: () => void;
|
|
18
|
+
onEdit?: () => void;
|
|
19
19
|
onTrash?: () => void;
|
|
20
20
|
onRestore?: () => void;
|
|
21
21
|
onChoose?: () => void;
|