@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
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
|
-
export declare function LibraryTab(props: TabViewProps
|
|
2
|
+
export declare function LibraryTab(props: TabViewProps): import("react").JSX.Element;
|
|
3
3
|
export default LibraryTab;
|
|
4
4
|
//# sourceMappingURL=LibraryTab.d.ts.map
|
|
@@ -6,9 +6,9 @@ import { UploadButton } from '../components/UploadButton.js';
|
|
|
6
6
|
import { EditMediaPanel } from '../components/EditMediaPanel.js';
|
|
7
7
|
import { MediaLightbox } from '../components/MediaLightbox.js';
|
|
8
8
|
import { MediaPurgeDialog } from '../components/MediaPurgeDialog.js';
|
|
9
|
-
export function LibraryTab(props
|
|
9
|
+
export function LibraryTab(props) {
|
|
10
10
|
const vm = useLibraryTab(props);
|
|
11
|
-
return (_jsxs("section", { className: "jini-media-library", "data-jini-part": "media.library", children: [vm.canUpload && _jsx(UploadButton, { upload: vm.upload, disabled: vm.busy }), _jsx("div", { className: "jini-media-order-control", children: _jsx(Select, { label: "Order by", attrs: { 'data-jini-part': 'media.sort', 'data-agent-element': 'media-order-by' }, value: vm.orderBy, onValueChange: vm.onOrder, options: [{ value: 'created', label: 'Created (newest first)' }, { value: 'alphabetical', label: 'Alphabetical (A–Z)' }] }) }), _jsxs("div", { className: "jini-media-library-filters", hidden: true, children: [_jsx(TextField, { attrs: { 'data-jini-part': 'media.search', 'data-agent-element': 'media-search' }, label: "Search media", value: vm.search, onValueChange: vm.onSearch }), _jsx(Select, { label: "Status", value: vm.statusFilter, onValueChange: vm.onStatus, options: [{ value: 'all', label: 'All statuses' }, { value: 'active', label: 'Active' }, { value: 'trashed', label: 'Trash' }] })] }), _jsx("p", { className: "jini-media-success", role: "status", "aria-live": "polite", "aria-atomic": "true", children: vm.success }), vm.untyped && _jsx("p", { className: "jini-notice", children: vm.t.untyped }), vm.snapshot?.error && _jsxs("div", { className: "jini-notice jini-notice-error", children: [_jsx("p", { className: "jini-notice-text", role: "alert", children: vm.snapshot.error }), _jsx(Button, { onPress: vm.onRetry, children: "Retry" })] }), vm.loading ? (_jsx("p", { className: "jini-notice", role: "status", children: vm.t.loading })) : (_jsx(MediaGrid, { gridRef: vm.gridRef, emptyLabel: vm.emptyLabel, actions: vm.actions, api: vm.mediaApi })), _jsx(MediaLightbox, { items: vm.items, api: vm.mediaApi, activeIndex: vm.lightboxIndex, onNavigate: vm.onNavigate, onClose: vm.closeLightbox }), vm.editing && (_jsx(EditMediaPanel, { api: vm.mediaApi, item: vm.editing, onSaved: vm.saved, onClose: vm.closeEdit }, vm.editing.id)), _jsx(MediaPurgeDialog, {
|
|
11
|
+
return (_jsxs("section", { className: "jini-media-library", "data-jini-part": "media.library", children: [vm.canUpload && _jsx(UploadButton, { upload: vm.upload, disabled: vm.busy }), _jsx("div", { className: "jini-media-order-control", children: _jsx(Select, { label: "Order by", attrs: { 'data-jini-part': 'media.sort', 'data-agent-element': 'media-order-by' }, value: vm.orderBy, onValueChange: vm.onOrder, options: [{ value: 'created', label: 'Created (newest first)' }, { value: 'alphabetical', label: 'Alphabetical (A–Z)' }] }) }), _jsxs("div", { className: "jini-media-library-filters", hidden: true, children: [_jsx(TextField, { attrs: { 'data-jini-part': 'media.search', 'data-agent-element': 'media-search' }, label: "Search media", value: vm.search, onValueChange: vm.onSearch }), _jsx(Select, { label: "Status", value: vm.statusFilter, onValueChange: vm.onStatus, options: [{ value: 'all', label: 'All statuses' }, { value: 'active', label: 'Active' }, { value: 'trashed', label: 'Trash' }] })] }), _jsx("p", { className: "jini-media-success", role: "status", "aria-live": "polite", "aria-atomic": "true", children: vm.success }), vm.untyped && _jsx("p", { className: "jini-notice", children: vm.t.untyped }), vm.snapshot?.error && _jsxs("div", { className: "jini-notice jini-notice-error", children: [_jsx("p", { className: "jini-notice-text", role: "alert", children: vm.snapshot.error }), _jsx(Button, { onPress: vm.onRetry, children: "Retry" })] }), vm.loading ? (_jsx("p", { className: "jini-notice", role: "status", children: vm.t.loading })) : (_jsx(MediaGrid, { gridRef: vm.gridRef, emptyLabel: vm.emptyLabel, actions: vm.actions, api: vm.mediaApi })), _jsx(MediaLightbox, { items: vm.items, api: vm.mediaApi, activeIndex: vm.lightboxIndex, onNavigate: vm.onNavigate, onClose: vm.closeLightbox }), vm.editing && (_jsx(EditMediaPanel, { api: vm.mediaApi, item: vm.editing, onSaved: vm.saved, onClose: vm.closeEdit }, vm.editing.id)), _jsx(MediaPurgeDialog, { ...vm.purgeDialog, onCancel: vm.cancelPurge, onConfirm: vm.confirmPurge })] }));
|
|
12
12
|
}
|
|
13
13
|
export default LibraryTab;
|
|
14
14
|
//# sourceMappingURL=LibraryTab.js.map
|
package/dist/media/rules.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare function formatByteSize({ bytes }: {
|
|
|
56
56
|
}, { locale }?: {
|
|
57
57
|
locale?: string;
|
|
58
58
|
}): string;
|
|
59
|
+
/** The file name a media card shows. A media record keeps only its extension-less title, so the
|
|
60
|
+
* extension comes from the type sniffed from its bytes; an unknown type, or a title that already
|
|
61
|
+
* ends with that extension, shows the title as-is. */
|
|
62
|
+
export declare function mediaFileName({ title, contentType }: {
|
|
63
|
+
title: string;
|
|
64
|
+
contentType: string | null;
|
|
65
|
+
}, _optional?: Record<string, never>): string;
|
|
59
66
|
/** Full-library counts, independent of search/order/type selection; Providers has no count. */
|
|
60
67
|
export declare function countMediaTabs({ media }: {
|
|
61
68
|
media: readonly MediaAsset[];
|
package/dist/media/rules.js
CHANGED
|
@@ -75,8 +75,16 @@ export function isRetryableMediaError({ error }, _optional = {}) {
|
|
|
75
75
|
const failure = error;
|
|
76
76
|
if (typeof failure.retryable === 'boolean')
|
|
77
77
|
return failure.retryable;
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
return isRetryableHttpStatus({ status: mediaErrorHttpStatus({ failure }) }) || error instanceof TypeError;
|
|
79
|
+
}
|
|
80
|
+
/** `status`, else `statusCode`, else the `HTTP nnn` an older transport put in its message —
|
|
81
|
+
* `NaN` when none is present. */
|
|
82
|
+
function mediaErrorHttpStatus({ failure }, _optional = {}) {
|
|
83
|
+
return failure.status ?? failure.statusCode ?? Number(failure.message?.match(/HTTP\s+(\d{3})/i)?.[1]);
|
|
84
|
+
}
|
|
85
|
+
/** Request timeout, rate limiting and any 5xx are temporary; `NaN` and every other status are not. */
|
|
86
|
+
function isRetryableHttpStatus({ status }, _optional = {}) {
|
|
87
|
+
return status === 408 || status === 429 || (status >= 500 && status <= 599);
|
|
80
88
|
}
|
|
81
89
|
/** Blank means native size; invalid values reach the server's positive-integer validator. */
|
|
82
90
|
export function parseOptionalPixelSize({ value }, _optional = {}) {
|
|
@@ -94,6 +102,21 @@ export function formatByteSize({ bytes }, { locale = 'en-US' } = {}) {
|
|
|
94
102
|
const mb = kb / 1024;
|
|
95
103
|
return mb < 1024 ? `${decimal.format(mb)} MB` : `${decimal.format(mb / 1024)} GB`;
|
|
96
104
|
}
|
|
105
|
+
/** The extension each sniffed media type is stored as; JPEG shows as the common ".jpg". */
|
|
106
|
+
const MEDIA_EXTENSION_BY_CONTENT_TYPE = {
|
|
107
|
+
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/avif': 'avif',
|
|
108
|
+
'image/svg+xml': 'svg', 'video/mp4': 'mp4', 'video/webm': 'webm', 'audio/wav': 'wav', 'audio/mpeg': 'mp3',
|
|
109
|
+
'audio/flac': 'flac', 'audio/mp4': 'm4a', 'application/pdf': 'pdf',
|
|
110
|
+
};
|
|
111
|
+
/** The file name a media card shows. A media record keeps only its extension-less title, so the
|
|
112
|
+
* extension comes from the type sniffed from its bytes; an unknown type, or a title that already
|
|
113
|
+
* ends with that extension, shows the title as-is. */
|
|
114
|
+
export function mediaFileName({ title, contentType }, _optional = {}) {
|
|
115
|
+
const extension = contentType ? MEDIA_EXTENSION_BY_CONTENT_TYPE[contentType] : undefined;
|
|
116
|
+
if (!extension || title.toLowerCase().endsWith(`.${extension}`))
|
|
117
|
+
return title;
|
|
118
|
+
return `${title}.${extension}`;
|
|
119
|
+
}
|
|
97
120
|
/** Full-library counts, independent of search/order/type selection; Providers has no count. */
|
|
98
121
|
export function countMediaTabs({ media }, _optional = {}) {
|
|
99
122
|
return { all: media.length, images: filterMediaByTab({ media, tab: 'images' }).length,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { agentHandle } from '@jini-ai/agentic';
|
|
3
2
|
import { useConfirmController } from '@jini-ai/ui-kit/react';
|
|
4
3
|
import { resolveTone, toneClassName } from '../../types.js';
|
|
5
4
|
import { useConfirmDialog, useConfirmDialogCancelLabel } from './ConfirmDialog.hooks.js';
|
|
5
|
+
import { confirmDialogAgentAttrs, withoutAccessibleName } from './confirm-dialog-attrs.js';
|
|
6
6
|
/**
|
|
7
7
|
* UI-kit's confirmation planner owns the sub-handle segments appended to the caller's base:
|
|
8
8
|
* literals chosen by the mechanism, never host data. Neither needs sanitizing, unlike
|
|
@@ -39,16 +39,13 @@ export function ConfirmDialog({ useDialog = useConfirmDialog, agentHandle: baseH
|
|
|
39
39
|
...(props.pending === undefined ? {} : { pending: props.pending }),
|
|
40
40
|
onConfirm: () => props.onConfirm(), onCancel: () => props.onCancel(),
|
|
41
41
|
}, {
|
|
42
|
-
// UI-kit names selects separately;
|
|
43
|
-
agent:
|
|
44
|
-
...(optional.role === undefined ? {} : { role: optional.role === 'select' ? 'field' : optional.role }),
|
|
45
|
-
...(optional.label === undefined ? {} : { label: optional.label }),
|
|
46
|
-
})),
|
|
42
|
+
// UI-kit names selects separately; see `confirmDialogAgentAttrs`.
|
|
43
|
+
agent: confirmDialogAgentAttrs,
|
|
47
44
|
});
|
|
48
45
|
const { titleId, dialogRef, cancelRef, handleNativeCancel, handleBackdropClick } = useDialog({ open: props.open, onCancel: () => { controller.requestDismiss({ reason: 'close' }); }, document }, { pending: controller.pending });
|
|
49
46
|
// Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent labels.
|
|
50
|
-
const {
|
|
51
|
-
const {
|
|
47
|
+
const cancelAttrs = withoutAccessibleName({ attrs: controller.cancel.attrs });
|
|
48
|
+
const confirmAttrs = withoutAccessibleName({ attrs: controller.confirm.attrs });
|
|
52
49
|
return (_jsxs("dialog", { ref: dialogRef, className: "confirm-dialog", "aria-labelledby": titleId, onCancel: handleNativeCancel, onClick: handleBackdropClick, children: [_jsx("h2", { id: titleId, children: props.title }), _jsxs("div", { className: "confirm-dialog-body", children: [props.body, controller.error] }), _jsxs("div", { className: "confirm-dialog-actions", children: [_jsx("button", { ref: cancelRef, type: "button", className: "btn-secondary", disabled: controller.pending, onClick: () => controller.cancel.onPress?.({}), ...cancelAttrs, children: cancelLabel }), _jsx("button", { type: "button", className: toneClassName({ tone }), disabled: controller.pending, onClick: () => controller.confirm.onPress?.({}), ...confirmAttrs, children: props.confirmLabel })] })] }));
|
|
53
50
|
}
|
|
54
51
|
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentAttrsPort, KitAttrs } from '@jini-ai/ui-kit';
|
|
2
|
+
/**
|
|
3
|
+
* The agent-attribute port `ConfirmDialog` hands UI-kit's confirm controller. UI-kit names selects
|
|
4
|
+
* separately; agentic publishes them as fields and still owns handle validation. Absent role/label
|
|
5
|
+
* options stay absent rather than being sent as `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @complexity O(1).
|
|
8
|
+
*/
|
|
9
|
+
export declare const confirmDialogAgentAttrs: AgentAttrsPort;
|
|
10
|
+
/**
|
|
11
|
+
* Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent
|
|
12
|
+
* labels. Returns the action's attributes without UI-kit's `aria-label`. UI-kit's `ButtonProps`
|
|
13
|
+
* types `attrs` as optional, so a missing set yields an empty one.
|
|
14
|
+
*
|
|
15
|
+
* @complexity O(k) in the number of attributes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withoutAccessibleName({ attrs }: {
|
|
18
|
+
attrs: KitAttrs | undefined;
|
|
19
|
+
}, _optional?: Record<string, never>): KitAttrs;
|
|
20
|
+
//# sourceMappingURL=confirm-dialog-attrs.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
2
|
+
/**
|
|
3
|
+
* The agent-attribute port `ConfirmDialog` hands UI-kit's confirm controller. UI-kit names selects
|
|
4
|
+
* separately; agentic publishes them as fields and still owns handle validation. Absent role/label
|
|
5
|
+
* options stay absent rather than being sent as `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @complexity O(1).
|
|
8
|
+
*/
|
|
9
|
+
export const confirmDialogAgentAttrs = (required, optional = {}) => agentHandle(required, {
|
|
10
|
+
...(optional.role === undefined ? {} : { role: optional.role === 'select' ? 'field' : optional.role }),
|
|
11
|
+
...(optional.label === undefined ? {} : { label: optional.label }),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent
|
|
15
|
+
* labels. Returns the action's attributes without UI-kit's `aria-label`. UI-kit's `ButtonProps`
|
|
16
|
+
* types `attrs` as optional, so a missing set yields an empty one.
|
|
17
|
+
*
|
|
18
|
+
* @complexity O(k) in the number of attributes.
|
|
19
|
+
*/
|
|
20
|
+
export function withoutAccessibleName({ attrs }, _optional = {}) {
|
|
21
|
+
const { 'aria-label': _accessibleName, ...rest } = attrs ?? {};
|
|
22
|
+
return rest;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=confirm-dialog-attrs.js.map
|
|
@@ -125,7 +125,17 @@ function identityLabel(config) {
|
|
|
125
125
|
* @complexity O(1) — one attribute read, one `JSON.parse` of a small config object, one table lookup.
|
|
126
126
|
*/
|
|
127
127
|
export function describeEmbedPlaceholder({ el }) {
|
|
128
|
-
const
|
|
128
|
+
const marker = parseEmbedConfig(el.getAttribute('data-embed-config'));
|
|
129
|
+
if (!marker)
|
|
130
|
+
return undefined;
|
|
131
|
+
const { config } = marker;
|
|
132
|
+
const type = marker.type.toLowerCase();
|
|
133
|
+
if (type === 'gallery')
|
|
134
|
+
return { kindLabel: EMBED_KIND_LABELS.gallery, identityLabel: galleryIdentityLabel(config) };
|
|
135
|
+
return { kindLabel: EMBED_KIND_LABELS[type] ?? titleCase(type), identityLabel: identityLabel(config) };
|
|
136
|
+
}
|
|
137
|
+
/** A marker's config object and its non-empty string `type`, or `undefined` for anything else. */
|
|
138
|
+
function parseEmbedConfig(raw) {
|
|
129
139
|
if (!raw)
|
|
130
140
|
return undefined;
|
|
131
141
|
let parsed;
|
|
@@ -140,12 +150,11 @@ export function describeEmbedPlaceholder({ el }) {
|
|
|
140
150
|
const config = parsed;
|
|
141
151
|
if (typeof config.type !== 'string' || config.type === '')
|
|
142
152
|
return undefined;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
return { kindLabel: EMBED_KIND_LABELS[type] ?? titleCase(type), identityLabel: identityLabel(config) };
|
|
153
|
+
return { config, type: config.type };
|
|
154
|
+
}
|
|
155
|
+
function galleryIdentityLabel(config) {
|
|
156
|
+
const count = Array.isArray(config.images) ? config.images.length : 0;
|
|
157
|
+
return count === 0 ? 'no images set' : `${count} ${count === 1 ? 'image' : 'images'}`;
|
|
149
158
|
}
|
|
150
159
|
export function InteractiveHtmlEditor(props) {
|
|
151
160
|
return (_jsx(HtmlEditor, { ...props, isProtectedElement: (el) => isProtectedEmbedElement({ el }), describeEmbedPlaceholder: (el) => describeEmbedPlaceholder({ el }) }));
|
|
@@ -17,6 +17,21 @@ import { useRowMenu } from './RowMenu.hooks.js';
|
|
|
17
17
|
function rowMenuAgentProps(handle, options) {
|
|
18
18
|
return handle === undefined ? {} : agentHandle({ handle }, options);
|
|
19
19
|
}
|
|
20
|
+
function rowMenuPopupClassName(position) {
|
|
21
|
+
return `row-menu-popup${position?.placement === 'above' ? ' row-menu-popup-above' : ''}`;
|
|
22
|
+
}
|
|
23
|
+
function rowMenuPopupStyle(position) {
|
|
24
|
+
return {
|
|
25
|
+
position: 'fixed',
|
|
26
|
+
top: position ? position.top : -9999,
|
|
27
|
+
left: position ? position.left : -9999,
|
|
28
|
+
// Laid out but invisible until the first `reposition()` measurement lands (see
|
|
29
|
+
// `RowMenu.hooks.tsx`) — keeps `offsetHeight`/`offsetWidth` measurable without a
|
|
30
|
+
// visible flash at the wrong coordinates.
|
|
31
|
+
visibility: position ? 'visible' : 'hidden',
|
|
32
|
+
transform: position?.placement === 'above' ? 'translateY(-100%)' : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
20
35
|
export function RowMenu({ useRowMenu: useRowMenuState = useRowMenu, agentHandle: baseHandle, ...props }) {
|
|
21
36
|
const { open, position, triggerRef, menuRef, itemRefs, onTriggerClick, onTriggerKeyDown, onMenuKeyDown, selectItem, } = useRowMenuState({ itemCount: props.items.length, window, document });
|
|
22
37
|
// One base handle per item, positionally aligned with `props.items` — undefined (rather than an
|
|
@@ -26,16 +41,7 @@ export function RowMenu({ useRowMenu: useRowMenuState = useRowMenu, agentHandle:
|
|
|
26
41
|
? buildAgentListHandles({ prefix: `${baseHandle}-item`, ids: props.items.map((item) => item.key) })
|
|
27
42
|
: undefined;
|
|
28
43
|
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: triggerRef, type: "button", className: "row-menu-trigger", "aria-label": props.triggerLabel, "aria-haspopup": "menu", "aria-expanded": open, onClick: onTriggerClick, onKeyDown: onTriggerKeyDown, ...rowMenuAgentProps(baseHandle, { role: 'button', label: props.triggerLabel }), children: _jsxs("svg", { viewBox: "0 0 18 18", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "9", cy: "4.5", r: "1.5" }), _jsx("circle", { cx: "9", cy: "9", r: "1.5" }), _jsx("circle", { cx: "9", cy: "13.5", r: "1.5" })] }) }), open
|
|
29
|
-
? createPortal(_jsx("div", { ref: menuRef, role: "menu", "aria-label": props.triggerLabel, className:
|
|
30
|
-
position: 'fixed',
|
|
31
|
-
top: position ? position.top : -9999,
|
|
32
|
-
left: position ? position.left : -9999,
|
|
33
|
-
// Laid out but invisible until the first `reposition()` measurement lands (see
|
|
34
|
-
// `RowMenu.hooks.tsx`) — keeps `offsetHeight`/`offsetWidth` measurable without a
|
|
35
|
-
// visible flash at the wrong coordinates.
|
|
36
|
-
visibility: position ? 'visible' : 'hidden',
|
|
37
|
-
transform: position?.placement === 'above' ? 'translateY(-100%)' : undefined,
|
|
38
|
-
}, onKeyDown: onMenuKeyDown, children: props.items.map((item, index) => {
|
|
44
|
+
? createPortal(_jsx("div", { ref: menuRef, role: "menu", "aria-label": props.triggerLabel, className: rowMenuPopupClassName(position), style: rowMenuPopupStyle(position), onKeyDown: onMenuKeyDown, children: props.items.map((item, index) => {
|
|
39
45
|
const toneClass = toneClassName({ tone: resolveTone({}, item) });
|
|
40
46
|
return (_jsx("button", { ref: (el) => {
|
|
41
47
|
itemRefs.current[index] = el;
|
|
@@ -210,34 +210,40 @@ function SidebarNav(props) {
|
|
|
210
210
|
*/
|
|
211
211
|
const collapsible = Boolean(group.label) && !collapsed && (props.collapsibleGroups ?? []).includes(group.label);
|
|
212
212
|
const open = !collapsible || sections.isOpen({ groupLabel: group.label });
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// activation, and the button role all come free, and `aria-expanded` only carries its
|
|
217
|
-
// meaning on an element that is actually a control.
|
|
218
|
-
_jsxs("button", { type: "button", className: `cms-group cms-group-toggle${open ? '' : ' is-closed'}`, "aria-expanded": open, "aria-controls": panelId, onClick: () => sections.toggle({ groupLabel: group.label }), children: [_jsx("span", { children: group.label }), _jsx("svg", { className: "cms-group-chevron", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M5 7l4 4 4-4", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })) : group.label ? (_jsx("div", { className: "cms-group", children: group.label })) : null, _jsx("div", { className: "cms-section-items", id: panelId, hidden: !open, children: group.items.map((item) => {
|
|
219
|
-
const active = item.id === activeId;
|
|
220
|
-
const href = adminHref({ routePath: item.href }, { base });
|
|
221
|
-
if (props.renderItem) {
|
|
222
|
-
return _jsx(Fragment, { children: props.renderItem(item, { active, href, collapsed }) }, item.id);
|
|
223
|
-
}
|
|
224
|
-
if (item.soon && !item.soonPreviewable) {
|
|
225
|
-
return (_jsxs("div", { className: "cms-item is-soon", "aria-disabled": "true", children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), _jsx("span", { className: "soon", children: props.soonLabel ?? 'Soon' })] }, item.id));
|
|
226
|
-
}
|
|
227
|
-
if (item.soon) {
|
|
228
|
-
// `soonPreviewable`: same real `<a>` as a shipped row (active state, aria-current,
|
|
229
|
-
// rail tooltip, cmd-click/copy-link all work identically) with the `soon` badge kept
|
|
230
|
-
// alongside the label — the announcement stays honest, but the row is no longer inert.
|
|
231
|
-
return (_jsxs("a", { className: `cms-item is-soon${active ? ' active' : ''}`, href: href, "aria-current": active ? 'page' : undefined, ...railTooltipProps(item.label), children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), _jsx("span", { className: "soon", children: props.soonLabel ?? 'Soon' })] }, item.id));
|
|
232
|
-
}
|
|
233
|
-
return (_jsxs("a", { className: `cms-item${active ? ' active' : ''}`,
|
|
234
|
-
// `AdminNavItem.href` is a route path; the base is applied here so the panel
|
|
235
|
-
// manifest never has to know it. A real URL, so cmd-click and "copy link address"
|
|
236
|
-
// behave normally.
|
|
237
|
-
href: href, "aria-current": active ? 'page' : undefined, ...railTooltipProps(item.label), children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label })] }, item.id));
|
|
238
|
-
}) })] }, group.label ?? `top-${gi}`));
|
|
213
|
+
const sectionKey = group.label ?? `top-${gi}`;
|
|
214
|
+
const panelId = `cms-section-${sectionKey.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
|
|
215
|
+
return (_jsxs("div", { className: "cms-section", children: [_jsx(NavGroupHeading, { label: group.label, collapsible: collapsible, open: open, panelId: panelId, onToggle: () => sections.toggle({ groupLabel: group.label }) }), _jsx("div", { className: "cms-section-items", id: panelId, hidden: !open, children: group.items.map((item) => (_jsx(NavItemRow, { item: item, nav: props, active: item.id === activeId, href: adminHref({ routePath: item.href }, { base }), collapsed: collapsed, railTooltipProps: railTooltipProps }, item.id))) })] }, sectionKey));
|
|
239
216
|
}) }));
|
|
240
217
|
}
|
|
218
|
+
function NavGroupHeading({ label, collapsible, open, panelId, onToggle }) {
|
|
219
|
+
if (!label)
|
|
220
|
+
return null;
|
|
221
|
+
if (!collapsible)
|
|
222
|
+
return _jsx("div", { className: "cms-group", children: label });
|
|
223
|
+
return (
|
|
224
|
+
// A real `<button>`, not a click-handled `<div>`: keyboard focus, Enter/Space
|
|
225
|
+
// activation, and the button role all come free, and `aria-expanded` only carries its
|
|
226
|
+
// meaning on an element that is actually a control.
|
|
227
|
+
_jsxs("button", { type: "button", className: `cms-group cms-group-toggle${open ? '' : ' is-closed'}`, "aria-expanded": open, "aria-controls": panelId, onClick: onToggle, children: [_jsx("span", { children: label }), _jsx("svg", { className: "cms-group-chevron", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M5 7l4 4 4-4", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] }));
|
|
228
|
+
}
|
|
229
|
+
function DisabledSoonItem({ item, soonLabel }) {
|
|
230
|
+
return (_jsxs("div", { className: "cms-item is-soon", "aria-disabled": "true", children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), _jsx("span", { className: "soon", children: soonLabel })] }));
|
|
231
|
+
}
|
|
232
|
+
function NavItemRow({ item, nav, active, href, collapsed, railTooltipProps }) {
|
|
233
|
+
if (nav.renderItem)
|
|
234
|
+
return _jsx(_Fragment, { children: nav.renderItem(item, { active, href, collapsed }) });
|
|
235
|
+
const soonLabel = nav.soonLabel ?? 'Soon';
|
|
236
|
+
if (item.soon && !item.soonPreviewable)
|
|
237
|
+
return _jsx(DisabledSoonItem, { item: item, soonLabel: soonLabel });
|
|
238
|
+
// `soonPreviewable`: same real `<a>` as a shipped row (active state, aria-current,
|
|
239
|
+
// rail tooltip, cmd-click/copy-link all work identically) with the `soon` badge kept
|
|
240
|
+
// alongside the label — the announcement stays honest, but the row is no longer inert.
|
|
241
|
+
return (_jsxs("a", { className: `cms-item${item.soon ? ' is-soon' : ''}${active ? ' active' : ''}`,
|
|
242
|
+
// `AdminNavItem.href` is a route path; the base is applied here so the panel
|
|
243
|
+
// manifest never has to know it. A real URL, so cmd-click and "copy link address"
|
|
244
|
+
// behave normally.
|
|
245
|
+
href: href, "aria-current": active ? 'page' : undefined, ...railTooltipProps(item.label), children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), item.soon ? _jsx("span", { className: "soon", children: soonLabel }) : null] }));
|
|
246
|
+
}
|
|
241
247
|
/** The bottom region. Carries no content of its own — the host decides what belongs there. */
|
|
242
248
|
function SidebarFooter(props) {
|
|
243
249
|
return _jsx("div", { className: ['cms-foot', props.className].filter(Boolean).join(' '), children: props.children });
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useId, useMemo } from 'react';
|
|
3
3
|
import { EntityField } from './EntityField.js';
|
|
4
|
+
import { RelationTruncationNotice } from './RelationTruncationNotice.js';
|
|
4
5
|
import { useEntityEdit } from './use-entity-edit.js';
|
|
5
6
|
import { entityText } from './types.js';
|
|
6
|
-
|
|
7
|
+
function EntityFieldRow({ edit, form, field, inputId, relations, translate }) {
|
|
8
|
+
const missing = edit.missing.includes(field.name);
|
|
9
|
+
const invalid = edit.invalid.some((problem) => problem.field === field.name);
|
|
10
|
+
const jsonError = form.jsonErrors[field.name] === true;
|
|
11
|
+
return _jsxs("div", { children: [_jsxs("label", { htmlFor: inputId, children: [translate({ key: field.label ?? field.name }), field.required ? ' *' : '', field.kind === 'datetime' ? ` (${translate({ key: 'UTC' })})` : ''] }), _jsx(EntityField, { inputId: inputId, field: field, value: form.draft[field.name], text: form.jsonText[field.name] ?? '', relations: relations, translate: translate, disabled: edit.pending !== null, invalid: missing || invalid || jsonError, onChange: ({ value }) => edit.setField({ name: field.name, value }), onTextChange: ({ text }) => edit.setJsonText({ name: field.name, text }) }), missing ? _jsx("p", { children: translate({ key: 'Required' }) }) : null, invalid ? _jsx("p", { children: translate({ key: 'Invalid value' }) }) : null, jsonError ? _jsx("p", { role: "alert", children: translate({ key: 'Invalid JSON' }) }) : null] });
|
|
12
|
+
}
|
|
13
|
+
function EditActions({ edit, canRemove, translate }) {
|
|
14
|
+
return _jsxs(_Fragment, { children: [_jsx("button", { type: "submit", disabled: !edit.canSave, children: translate({ key: edit.pending === 'save' ? 'Saving' : 'Save' }) }), canRemove ? _jsx("button", { type: "button", disabled: edit.pending !== null, onClick: () => { void edit.remove(); }, children: translate({ key: edit.pending === 'remove' ? 'Deleting' : 'Delete' }) }) : null] });
|
|
15
|
+
}
|
|
7
16
|
function EntityEditor(props) {
|
|
8
17
|
const { port, translate, routes, entityName, id } = props;
|
|
9
18
|
const formId = useId();
|
|
@@ -12,19 +21,13 @@ function EntityEditor(props) {
|
|
|
12
21
|
return _jsx("p", { role: "alert", children: translate({ key: 'Unable to load row' }) });
|
|
13
22
|
if (edit.read.loading || !edit.read.data)
|
|
14
23
|
return _jsx("p", { role: "status", children: translate({ key: 'Loading' }) });
|
|
15
|
-
|
|
24
|
+
// useEntityEdit builds the form in the same render its row arrives, so once data has loaded the
|
|
25
|
+
// only way to have no form is an edit route whose row does not exist.
|
|
26
|
+
const { form } = edit;
|
|
27
|
+
if (!form)
|
|
16
28
|
return _jsx("p", { role: "status", children: translate({ key: 'Row not found' }) });
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const busy = edit.pending !== null;
|
|
20
|
-
const truncated = Object.entries(edit.read.data.relations).filter(([, target]) => target.truncated).map(([name]) => name);
|
|
21
|
-
return _jsxs("form", { className: "page", onSubmit: (e) => { e.preventDefault(); void edit.save(); }, children: [_jsx("a", { href: routes.href({ entity: entityName }), children: translate({ key: port.descriptor.labelPlural }) }), _jsx("h1", { children: entityText({ translate, key: id === null ? 'New {entity}' : 'Edit {entity}', values: { entity: translate({ key: port.descriptor.labelSingular }) } }) }), edit.failed ? _jsx("p", { role: "alert", children: translate({ key: 'Operation failed' }) }) : null, port.descriptor.fields.map((field, index) => {
|
|
22
|
-
const inputId = `${formId}-${index}`;
|
|
23
|
-
const missing = edit.missing.includes(field.name);
|
|
24
|
-
const invalid = edit.invalid.some((problem) => problem.field === field.name);
|
|
25
|
-
const jsonError = edit.form?.jsonErrors[field.name] === true;
|
|
26
|
-
return _jsxs("div", { children: [_jsxs("label", { htmlFor: inputId, children: [translate({ key: field.label ?? field.name }), field.required ? ' *' : '', field.kind === 'datetime' ? ` (${translate({ key: 'UTC' })})` : ''] }), _jsx(EntityField, { inputId: inputId, field: field, value: edit.form?.draft[field.name], text: edit.form?.jsonText[field.name] ?? '', relations: edit.read.data.relations, translate: translate, disabled: busy, invalid: missing || invalid || jsonError, onChange: ({ value }) => edit.setField({ name: field.name, value }), onTextChange: ({ text }) => edit.setJsonText({ name: field.name, text }) }), missing ? _jsx("p", { children: translate({ key: 'Required' }) }) : null, invalid ? _jsx("p", { children: translate({ key: 'Invalid value' }) }) : null, jsonError ? _jsx("p", { role: "alert", children: translate({ key: 'Invalid JSON' }) }) : null] }, field.name);
|
|
27
|
-
}), truncated.length > 0 ? _jsx("p", { children: entityText({ translate, key: 'Related names limited to {limit} rows: {targets}', values: { limit: RELATION_OPTION_LIMIT, targets: truncated.join(', ') } }) }) : null, _jsx("button", { type: "submit", disabled: !edit.canSave, children: translate({ key: edit.pending === 'save' ? 'Saving' : 'Save' }) }), id !== null && port.remove ? _jsx("button", { type: "button", disabled: busy, onClick: () => { void edit.remove(); }, children: translate({ key: edit.pending === 'remove' ? 'Deleting' : 'Delete' }) }) : null] });
|
|
29
|
+
const { relations } = edit.read.data;
|
|
30
|
+
return _jsxs("form", { className: "page", onSubmit: (e) => { e.preventDefault(); void edit.save(); }, children: [_jsx("a", { href: routes.href({ entity: entityName }), children: translate({ key: port.descriptor.labelPlural }) }), _jsx("h1", { children: entityText({ translate, key: id === null ? 'New {entity}' : 'Edit {entity}', values: { entity: translate({ key: port.descriptor.labelSingular }) } }) }), edit.failed ? _jsx("p", { role: "alert", children: translate({ key: 'Operation failed' }) }) : null, port.descriptor.fields.map((field, index) => _jsx(EntityFieldRow, { edit: edit, form: form, field: field, inputId: `${formId}-${index}`, relations: relations, translate: translate }, field.name)), _jsx(RelationTruncationNotice, { relations: relations, translate: translate }), _jsx(EditActions, { edit: edit, canRemove: id !== null && port.remove !== undefined, translate: translate })] });
|
|
28
31
|
}
|
|
29
32
|
/** Host entity editing uses the existing registry/ports and core draft validation.
|
|
30
33
|
* Required booleans default false, optional booleans remain unset, and invalid JSON blocks save
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import type { AdminEntityField } from '../../core/ports/entities.js';
|
|
2
3
|
import { type EntityTranslate } from './types.js';
|
|
3
4
|
import { type RelationIndex } from './rules.js';
|
|
4
|
-
|
|
5
|
+
interface EntityFieldProps {
|
|
5
6
|
readonly inputId: string;
|
|
6
7
|
readonly field: AdminEntityField;
|
|
7
8
|
readonly value: unknown;
|
|
@@ -16,5 +17,7 @@ export declare function EntityField(props: {
|
|
|
16
17
|
readonly onTextChange: (args: {
|
|
17
18
|
readonly text: string;
|
|
18
19
|
}) => void;
|
|
19
|
-
}
|
|
20
|
+
}
|
|
21
|
+
export declare function EntityField(props: EntityFieldProps): ReactNode;
|
|
22
|
+
export {};
|
|
20
23
|
//# sourceMappingURL=EntityField.d.ts.map
|
|
@@ -2,32 +2,47 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Select } from '@jini-ai/ui/admin-widgets';
|
|
3
3
|
import { selectTranslate } from './types.js';
|
|
4
4
|
import { fromDatetimeLocalValue, toDatetimeLocalValue } from './rules.js';
|
|
5
|
+
const fieldLabel = ({ field, translate }) => translate({ key: field.label ?? field.name });
|
|
6
|
+
const blankToUndefined = (raw) => (raw === '' ? undefined : raw);
|
|
7
|
+
const commonProps = ({ inputId, disabled, invalid }) => ({ id: inputId, disabled, 'aria-invalid': invalid });
|
|
8
|
+
function RelationInput(props) {
|
|
9
|
+
const { inputId, field, value, translate, disabled, onChange } = props;
|
|
10
|
+
const options = props.relations[field.target]?.options ?? [];
|
|
11
|
+
const selectedId = typeof value === 'string' ? value : '';
|
|
12
|
+
const missing = selectedId !== '' && !options.some((option) => option.id === selectedId);
|
|
13
|
+
return _jsx(Select, { id: inputId, disabled: disabled, value: selectedId, translate: selectTranslate({ translate }), "aria-label": fieldLabel(props), options: [
|
|
14
|
+
{ value: '', label: translate({ key: 'No value' }) },
|
|
15
|
+
...(missing ? [{ value: selectedId, label: `${selectedId} (${translate({ key: 'Unresolved' })})` }] : []),
|
|
16
|
+
...options.map((option) => ({ value: option.id, label: option.title })),
|
|
17
|
+
], onChange: (selected) => onChange({ value: blankToUndefined(selected) }) });
|
|
18
|
+
}
|
|
19
|
+
function booleanSelectValue(value) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return '';
|
|
22
|
+
return value === true ? 'true' : 'false';
|
|
23
|
+
}
|
|
24
|
+
function BooleanInput(props) {
|
|
25
|
+
const { inputId, field, value, translate, disabled, onChange } = props;
|
|
26
|
+
if (field.required)
|
|
27
|
+
return _jsx("input", { ...commonProps(props), type: "checkbox", checked: value === true, onChange: (e) => onChange({ value: e.target.checked }) });
|
|
28
|
+
return _jsx(Select, { id: inputId, disabled: disabled, value: booleanSelectValue(value), translate: selectTranslate({ translate }), "aria-label": fieldLabel(props), options: [
|
|
29
|
+
{ value: '', label: translate({ key: 'No value' }) }, { value: 'true', label: translate({ key: 'Yes' }) }, { value: 'false', label: translate({ key: 'No' }) },
|
|
30
|
+
], onChange: (selected) => onChange({ value: selected === '' ? undefined : selected === 'true' }) });
|
|
31
|
+
}
|
|
32
|
+
function NumberInput(props) {
|
|
33
|
+
const { field, value, onChange } = props;
|
|
34
|
+
return _jsx("input", { ...commonProps(props), type: "number", step: field.kind === 'integer' ? '1' : 'any', value: typeof value === 'number' ? value : '', onChange: (e) => onChange({ value: e.target.value === '' ? undefined : Number(e.target.value) }) });
|
|
35
|
+
}
|
|
5
36
|
export function EntityField(props) {
|
|
6
|
-
const {
|
|
7
|
-
const common = { id: inputId, disabled, 'aria-invalid': invalid };
|
|
8
|
-
const selectT = selectTranslate({ translate });
|
|
37
|
+
const { field, value, onChange } = props;
|
|
9
38
|
switch (field.kind) {
|
|
10
|
-
case 'json': return _jsx("textarea", { ...
|
|
11
|
-
case 'relation': {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const missing = selectedId !== '' && !options.some((option) => option.id === selectedId);
|
|
15
|
-
return _jsx(Select, { id: inputId, disabled: disabled, value: selectedId, translate: selectT, "aria-label": translate({ key: field.label ?? field.name }), options: [
|
|
16
|
-
{ value: '', label: translate({ key: 'No value' }) },
|
|
17
|
-
...(missing ? [{ value: selectedId, label: `${selectedId} (${translate({ key: 'Unresolved' })})` }] : []),
|
|
18
|
-
...options.map((option) => ({ value: option.id, label: option.title })),
|
|
19
|
-
], onChange: (selected) => onChange({ value: selected === '' ? undefined : selected }) });
|
|
20
|
-
}
|
|
21
|
-
case 'boolean':
|
|
22
|
-
if (field.required)
|
|
23
|
-
return _jsx("input", { ...common, type: "checkbox", checked: value === true, onChange: (e) => onChange({ value: e.target.checked }) });
|
|
24
|
-
return _jsx(Select, { id: inputId, disabled: disabled, value: value === undefined ? '' : value === true ? 'true' : 'false', translate: selectT, "aria-label": translate({ key: field.label ?? field.name }), options: [
|
|
25
|
-
{ value: '', label: translate({ key: 'No value' }) }, { value: 'true', label: translate({ key: 'Yes' }) }, { value: 'false', label: translate({ key: 'No' }) },
|
|
26
|
-
], onChange: (selected) => onChange({ value: selected === '' ? undefined : selected === 'true' }) });
|
|
27
|
-
case 'datetime': return _jsx("input", { ...common, type: "datetime-local", step: "0.001", value: toDatetimeLocalValue({ value }), onChange: (e) => onChange({ value: fromDatetimeLocalValue({ raw: e.target.value }) }) });
|
|
39
|
+
case 'json': return _jsx("textarea", { ...commonProps(props), value: props.text, onChange: (e) => props.onTextChange({ text: e.target.value }) });
|
|
40
|
+
case 'relation': return _jsx(RelationInput, { ...props, field: field });
|
|
41
|
+
case 'boolean': return _jsx(BooleanInput, { ...props });
|
|
42
|
+
case 'datetime': return _jsx("input", { ...commonProps(props), type: "datetime-local", step: "0.001", value: toDatetimeLocalValue({ value }), onChange: (e) => onChange({ value: fromDatetimeLocalValue({ raw: e.target.value }) }) });
|
|
28
43
|
case 'integer':
|
|
29
|
-
case 'real': return _jsx(
|
|
30
|
-
case 'text': return _jsx("input", { ...
|
|
44
|
+
case 'real': return _jsx(NumberInput, { ...props });
|
|
45
|
+
case 'text': return _jsx("input", { ...commonProps(props), value: typeof value === 'string' ? value : '', onChange: (e) => onChange({ value: blankToUndefined(e.target.value) }) });
|
|
31
46
|
}
|
|
32
47
|
}
|
|
33
48
|
//# sourceMappingURL=EntityField.js.map
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useMemo, useState } from 'react';
|
|
3
3
|
import { DataTable } from '../components/DataTable.js';
|
|
4
4
|
import { EntityCell } from './EntityCell.js';
|
|
5
|
+
import { RelationTruncationNotice } from './RelationTruncationNotice.js';
|
|
5
6
|
import { entityText } from './types.js';
|
|
6
|
-
import { loadRelationIndex, resolveEntityPageSize
|
|
7
|
+
import { loadRelationIndex, resolveEntityPageSize } from './rules.js';
|
|
7
8
|
import { useEntityRead } from './use-entity-read.js';
|
|
8
9
|
/** Cursor history records actual returned page lengths; never sort a server-paginated page locally.
|
|
9
10
|
* Host translation, registry and navigation are injected, and this list uses the existing DataTable.
|
|
@@ -15,9 +16,10 @@ export function EntityList(props) {
|
|
|
15
16
|
const limit = resolveEntityPageSize({ raw: props.limitParam });
|
|
16
17
|
const token = JSON.stringify([entityName, limit]);
|
|
17
18
|
const [walk, setWalk] = useState({ token, port, entries: [{ cursor: null, offset: 0 }] });
|
|
18
|
-
|
|
19
|
+
const walkIsCurrent = walk.token === token && walk.port === port;
|
|
20
|
+
if (!walkIsCurrent)
|
|
19
21
|
setWalk({ token, port, entries: [{ cursor: null, offset: 0 }] });
|
|
20
|
-
const entries =
|
|
22
|
+
const entries = walkIsCurrent ? walk.entries : [{ cursor: null, offset: 0 }];
|
|
21
23
|
const position = entries[entries.length - 1];
|
|
22
24
|
const load = useCallback(async () => {
|
|
23
25
|
if (port === null)
|
|
@@ -37,7 +39,6 @@ export function EntityList(props) {
|
|
|
37
39
|
return _jsx("p", { role: "status", children: translate({ key: 'Loading' }) });
|
|
38
40
|
const { page, relations } = result.data;
|
|
39
41
|
const descriptor = port.descriptor;
|
|
40
|
-
const truncated = Object.entries(relations).filter(([, options]) => options.truncated).map(([name]) => name);
|
|
41
42
|
return _jsxs("div", { className: "page", children: [_jsx("a", { href: routes.href({}), children: translate({ key: 'Data' }) }), _jsx("h1", { children: translate({ key: descriptor.labelPlural }) }), _jsx("a", { href: routes.href({ entity: entityName, view: 'create' }), children: entityText({ translate, key: 'New {entity}', values: { entity: translate({ key: descriptor.labelSingular }) } }) }), _jsx(DataTable, { rows: [...page.items], rowKey: (row) => row.id, empty: _jsx("p", { children: translate({ key: 'No rows' }) }), columns: descriptor.fields.map((field) => ({
|
|
42
43
|
key: field.name, header: translate({ key: field.label ?? field.name }),
|
|
43
44
|
// Sorting a server-cut page would misrepresent collection order.
|
|
@@ -47,6 +48,6 @@ export function EntityList(props) {
|
|
|
47
48
|
})) }), _jsx("p", { children: page.items.length === 0 ? translate({ key: 'No rows' }) : entityText({ translate, key: 'Showing {from}–{to}', values: { from: position.offset + 1, to: position.offset + page.items.length } }) }), _jsx("button", { type: "button", disabled: entries.length === 1, onClick: () => setWalk((current) => ({ ...current, entries: current.entries.slice(0, -1) })), children: translate({ key: 'Previous' }) }), _jsx("button", { type: "button", disabled: page.nextCursor === null, onClick: () => {
|
|
48
49
|
if (page.nextCursor !== null)
|
|
49
50
|
setWalk((current) => ({ ...current, entries: [...current.entries, { cursor: page.nextCursor, offset: position.offset + page.items.length }] }));
|
|
50
|
-
}, children: translate({ key: 'Next' }) }),
|
|
51
|
+
}, children: translate({ key: 'Next' }) }), _jsx(RelationTruncationNotice, { relations: relations, translate: translate })] });
|
|
51
52
|
}
|
|
52
53
|
//# sourceMappingURL=EntityList.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type EntityTranslate } from './types.js';
|
|
3
|
+
import { type RelationIndex } from './rules.js';
|
|
4
|
+
/** Names every relation target whose option set was cut at the limit; renders nothing otherwise. */
|
|
5
|
+
export declare function RelationTruncationNotice({ relations, translate }: {
|
|
6
|
+
readonly relations: RelationIndex;
|
|
7
|
+
readonly translate: EntityTranslate;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
//# sourceMappingURL=RelationTruncationNotice.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { entityText } from './types.js';
|
|
3
|
+
import { RELATION_OPTION_LIMIT } from './rules.js';
|
|
4
|
+
/** Names every relation target whose option set was cut at the limit; renders nothing otherwise. */
|
|
5
|
+
export function RelationTruncationNotice({ relations, translate }) {
|
|
6
|
+
const truncated = Object.entries(relations).filter(([, options]) => options.truncated).map(([name]) => name);
|
|
7
|
+
return truncated.length > 0 ? _jsx("p", { children: entityText({ translate, key: 'Related names limited to {limit} rows: {targets}', values: { limit: RELATION_OPTION_LIMIT, targets: truncated.join(', ') } }) }) : null;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=RelationTruncationNotice.js.map
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import type { AdminErasedEntityPort } from '../../core/ports/entities.js';
|
|
2
2
|
import type { EntityEditProps } from './types.js';
|
|
3
|
+
import { type EntityLoadedRow } from './use-entity-read.js';
|
|
3
4
|
interface FormState {
|
|
4
5
|
readonly draft: Record<string, unknown>;
|
|
5
6
|
readonly jsonText: Readonly<Record<string, string>>;
|
|
6
7
|
readonly jsonErrors: Readonly<Record<string, boolean>>;
|
|
7
8
|
}
|
|
9
|
+
type MutationKind = 'save' | 'remove';
|
|
8
10
|
/** Called by the editor mounted for one entity/id pair. */
|
|
9
11
|
export declare function useEntityEdit({ port, ...props }: EntityEditProps & {
|
|
10
12
|
readonly port: AdminErasedEntityPort;
|
|
11
13
|
}): {
|
|
12
14
|
form: FormState | null;
|
|
13
15
|
read: {
|
|
14
|
-
readonly data:
|
|
16
|
+
readonly data: EntityLoadedRow | undefined;
|
|
15
17
|
readonly error: unknown;
|
|
16
18
|
readonly loading: boolean;
|
|
17
19
|
};
|
|
18
20
|
missing: readonly string[];
|
|
19
21
|
invalid: readonly import("./rules.js").DraftFieldProblem[];
|
|
20
22
|
canSave: boolean;
|
|
21
|
-
pending:
|
|
23
|
+
pending: MutationKind | null;
|
|
22
24
|
failed: boolean;
|
|
23
25
|
setField: ({ name, value }: {
|
|
24
26
|
readonly name: string;
|