@jini-ai/admin 0.4.11 → 0.4.13
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 +9 -0
- 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 +74 -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/ports/seo.d.ts +20 -0
- 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/adapters/http.js +5 -0
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/messages.en.d.ts +15 -10
- package/dist/seo/messages.en.js +15 -10
- package/dist/seo/models.d.ts +11 -0
- package/dist/seo/react/components/MediaRefField.d.ts +17 -5
- package/dist/seo/react/components/MediaRefField.js +31 -4
- package/dist/seo/react/hooks/MediaRefField.hooks.js +12 -2
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoPage.hooks.js +2 -2
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +1 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -2
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +13 -2
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +4 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -33
- package/dist/seo/react/index.d.ts +45 -30
- package/dist/seo/react/pages/SeoPage.d.ts +6 -4
- package/dist/seo/react/pages/SeoPage.js +51 -29
- package/dist/seo/rules.d.ts +91 -0
- package/dist/seo/rules.js +161 -6
- package/dist/seo/seo.module.d.ts +15 -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 +19 -10
- 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
package/dist/forms/html-rules.js
CHANGED
|
@@ -28,11 +28,22 @@ export function formatFormAnswer({ value, options, type }, display = {}) {
|
|
|
28
28
|
if (Array.isArray(value))
|
|
29
29
|
return value.map(item => formatFormAnswer({ value: item, options, type }, display)).join(", ");
|
|
30
30
|
if (typeof value === "boolean")
|
|
31
|
-
return value
|
|
32
|
-
if (typeof value === "string" && type !== undefined && type in TEMPORAL_STYLES)
|
|
33
|
-
return formatTemporal({ value, type }, display);
|
|
31
|
+
return formatBoolean({ value }, display);
|
|
34
32
|
if (typeof value === "string")
|
|
35
|
-
return
|
|
33
|
+
return formatText({ value, options, type }, display);
|
|
34
|
+
return formatOther({ value });
|
|
35
|
+
}
|
|
36
|
+
function formatBoolean({ value }, { yes = "Yes", no = "No" }) {
|
|
37
|
+
return value ? yes : no;
|
|
38
|
+
}
|
|
39
|
+
/** A temporal field's answer as a date; any other string as its choice label, or raw. */
|
|
40
|
+
function formatText({ value, options, type }, display) {
|
|
41
|
+
if (type !== undefined && type in TEMPORAL_STYLES)
|
|
42
|
+
return formatTemporal({ value, type }, display);
|
|
43
|
+
return options?.find(option => option.value === value)?.label ?? value;
|
|
44
|
+
}
|
|
45
|
+
/** A file reference as its name and size; any other value as JSON or its string form. */
|
|
46
|
+
function formatOther({ value }) {
|
|
36
47
|
const file = formAnswerFile({ value });
|
|
37
48
|
if (file)
|
|
38
49
|
return file.byteSize === undefined ? file.filename : `${file.filename} (${formatByteSize({ bytes: file.byteSize })})`;
|
|
@@ -92,32 +103,38 @@ function starterAttributes(field, kind) {
|
|
|
92
103
|
/** One field's starter markup; every control comes from the registry kind, as the public render does. */
|
|
93
104
|
function starterField(field) {
|
|
94
105
|
const kind = kindOf(field.type);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (kind.control === "group")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const boxAttrs = kind.multiple ? starterAttributes({ ...field, required: false }, kind) : attrs;
|
|
103
|
-
const boxes = options.map((option) => ` <label><input type="${kind.htmlType}" ${boxAttrs} value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " checked" : ""}> ${escapeHtml(option.label)}</label>`);
|
|
104
|
-
return `<fieldset>\n <legend>${label}</legend>\n${boxes.join("\n")}\n</fieldset>`;
|
|
105
|
-
}
|
|
106
|
-
if (kind.control === "select") {
|
|
107
|
-
const empty = kind.multiple ? [] : [` <option value="">${escapeHtml(field.placeholder ?? "Choose an option")}</option>`];
|
|
108
|
-
const choices = options.map((option) => ` <option value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " selected" : ""}>${escapeHtml(option.label)}</option>`);
|
|
109
|
-
return `<label>${label}\n <select ${attrs}${kind.multiple ? " multiple" : ""}>\n${[...empty, ...choices].join("\n")}\n </select>\n</label>`;
|
|
110
|
-
}
|
|
106
|
+
return starterRendererFor({ kind })({ field, kind, label: escapeHtml(field.label), attrs: starterAttributes(field, kind) });
|
|
107
|
+
}
|
|
108
|
+
function starterRendererFor({ kind }) {
|
|
109
|
+
if (kind.control === "group")
|
|
110
|
+
return starterGroup;
|
|
111
|
+
if (kind.control === "select")
|
|
112
|
+
return starterSelect;
|
|
111
113
|
if (kind.control === "textarea")
|
|
112
|
-
return `<label>${label}\n <textarea ${attrs}></textarea>\n</label>`;
|
|
114
|
+
return ({ label, attrs }) => `<label>${label}\n <textarea ${attrs}></textarea>\n</label>`;
|
|
113
115
|
if (kind.htmlType === "hidden")
|
|
114
|
-
return `<input type="hidden" ${attrs} value="${escapeHtml(String(field.default ?? ""))}" aria-label="${label}">`;
|
|
115
|
-
if (kind.htmlType === "checkbox")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
return ({ field, label, attrs }) => `<input type="hidden" ${attrs} value="${escapeHtml(String(field.default ?? ""))}" aria-label="${label}">`;
|
|
117
|
+
if (kind.htmlType === "checkbox")
|
|
118
|
+
return starterCheckbox;
|
|
119
|
+
return ({ label, attrs }) => `<label>${label}\n <input type="${kind.htmlType}" ${attrs}>\n</label>`;
|
|
120
|
+
}
|
|
121
|
+
function starterGroup({ field, kind, label, attrs }) {
|
|
122
|
+
// One box per option under a shared name; the CMS collects them back into this choice field.
|
|
123
|
+
// Native `required` on every checkbox would demand ALL of them, so a checkbox group leaves it
|
|
124
|
+
// off (the server still enforces one-or-more); a radio group's `required` means "pick one".
|
|
125
|
+
const boxAttrs = kind.multiple ? starterAttributes({ ...field, required: false }, kind) : attrs;
|
|
126
|
+
const boxes = (field.options ?? []).map((option) => ` <label><input type="${kind.htmlType}" ${boxAttrs} value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " checked" : ""}> ${escapeHtml(option.label)}</label>`);
|
|
127
|
+
return `<fieldset>\n <legend>${label}</legend>\n${boxes.join("\n")}\n</fieldset>`;
|
|
128
|
+
}
|
|
129
|
+
function starterSelect({ field, kind, label, attrs }) {
|
|
130
|
+
const empty = kind.multiple ? [] : [` <option value="">${escapeHtml(field.placeholder ?? "Choose an option")}</option>`];
|
|
131
|
+
const choices = (field.options ?? []).map((option) => ` <option value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " selected" : ""}>${escapeHtml(option.label)}</option>`);
|
|
132
|
+
return `<label>${label}\n <select ${attrs}${kind.multiple ? " multiple" : ""}>\n${[...empty, ...choices].join("\n")}\n </select>\n</label>`;
|
|
133
|
+
}
|
|
134
|
+
function starterCheckbox({ field, label, attrs }) {
|
|
135
|
+
const value = field.checkboxValue !== undefined ? ` value="${escapeHtml(field.checkboxValue)}"` : "";
|
|
136
|
+
const checked = field.default === true || (field.checkboxValue !== undefined && field.default === field.checkboxValue) ? " checked" : "";
|
|
137
|
+
return `<label><input type="checkbox"${field.type === "toggle" ? ' role="switch"' : ""} ${attrs}${value}${checked}> ${label}</label>`;
|
|
121
138
|
}
|
|
122
139
|
/**
|
|
123
140
|
* Editable form BODY only: the server supplies the transport and spam-protection wrapper. Each
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
2
|
+
/**
|
|
3
|
+
* @file The row groups `FieldSettingsSection` stacks, split out so each group's show/hide branches
|
|
4
|
+
* are counted on their own. Markup only: every value and handler comes from `useFieldSettingsView`.
|
|
5
|
+
*/
|
|
6
|
+
type FieldSettingsView = ReturnType<typeof useFieldSettingsView>;
|
|
7
|
+
export interface FieldSettingsRowsProps {
|
|
8
|
+
view: FieldSettingsView;
|
|
9
|
+
t: (key: string) => string;
|
|
10
|
+
}
|
|
11
|
+
/** Placeholder and help-text rows. */
|
|
12
|
+
export declare function FieldSettingsTextRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
|
|
13
|
+
/** The option list editor of a choice field. */
|
|
14
|
+
export declare function FieldSettingsOptionsRow({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
|
|
15
|
+
/** The default-value row: a box, a single-choice select, or a typed input, whichever the type takes. */
|
|
16
|
+
export declare function FieldSettingsDefaultRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
|
|
17
|
+
/** Length, pattern and range rows. */
|
|
18
|
+
export declare function FieldSettingsLimitRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
|
|
19
|
+
/** Accepted file types and max size of an upload field. */
|
|
20
|
+
export declare function FieldSettingsUploadRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=FieldSettingsRows.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
3
|
+
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
4
|
+
import { MAX_UPLOAD_MB } from '../../field-settings.js';
|
|
5
|
+
/** Placeholder and help-text rows. */
|
|
6
|
+
export function FieldSettingsTextRows({ view, t }) {
|
|
7
|
+
const { settings, form } = view;
|
|
8
|
+
return (_jsxs(_Fragment, { children: [settings.placeholder ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-placeholder", children: t('Placeholder') }), _jsx("input", { id: "field-settings-placeholder", "data-jini-autofocus": "", value: form.placeholder, onChange: view.onPlaceholder, ...agentHandle({ handle: 'form-field-settings-placeholder' }, { role: 'field', label: 'Hint text shown inside the empty control' }) })] })) : null, settings.helpText ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-help", children: t('Help text') }), _jsx(InfoTip, { label: t('Shown under the field and read out by screen readers.') })] }), _jsx("input", { id: "field-settings-help", "data-jini-autofocus": "", value: form.helpText, onChange: view.onHelpText, ...agentHandle({ handle: 'form-field-settings-help' }, { role: 'field', label: 'Help text shown under this field' }) })] })) : null] }));
|
|
9
|
+
}
|
|
10
|
+
/** The option list editor of a choice field. */
|
|
11
|
+
export function FieldSettingsOptionsRow({ view, t }) {
|
|
12
|
+
return (_jsxs("div", { className: "field-settings-row field-settings-row--top", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-options-label", children: t('Options') }), _jsx(InfoTip, { label: t('The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.') })] }), _jsxs("div", { className: "field-options", role: "group", "aria-labelledby": "field-settings-options-label", children: [_jsx("ol", { className: "field-options-list", children: view.options.map((row) => (_jsxs("li", { className: "field-option-row", children: [_jsx("input", { className: "field-option-label", "aria-label": row.labelAria, placeholder: t('Label'), value: row.label, onChange: row.onLabel, ...agentHandle({ handle: `${row.handle}-label` }, { role: 'field', label: 'Option text visitors see' }) }), _jsx("input", { className: "field-option-value", "aria-label": row.valueAria, placeholder: t('Value'), value: row.value, onChange: row.onValue, ...agentHandle({ handle: `${row.handle}-value` }, { role: 'field', label: 'Option value stored with each submission' }) }), view.settings.multiple ? (_jsx("label", { className: "field-option-default", children: _jsx("input", { type: "checkbox", checked: row.isDefault, disabled: !row.value, onChange: row.onDefault, "aria-label": row.defaultAria }) })) : null, _jsxs("span", { className: "field-option-actions", children: [_jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isFirst, onClick: row.onUp, "aria-label": row.upAria, children: "\u2191" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isLast, onClick: row.onDown, "aria-label": row.downAria, children: "\u2193" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", onClick: row.onRemove, "aria-label": row.removeAria, ...agentHandle({ handle: `${row.handle}-remove` }, { role: 'button', label: 'Remove this option' }), children: "\u00D7" })] })] }, row._rowId))) }), _jsx("button", { type: "button", className: "btn-secondary field-settings-add", onClick: view.addOption, ...agentHandle({ handle: 'form-field-options-add' }, { role: 'button', label: 'Add another option to this choice field' }), children: t('Add option') })] })] }));
|
|
13
|
+
}
|
|
14
|
+
/** The default-value row: a box, a single-choice select, or a typed input, whichever the type takes. */
|
|
15
|
+
export function FieldSettingsDefaultRows({ view, t }) {
|
|
16
|
+
const { settings, form } = view;
|
|
17
|
+
return (_jsxs(_Fragment, { children: [settings.defaultKind === 'boolean' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", children: t('Default') }), _jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: form.defaultChecked, onChange: view.onDefaultChecked, ...agentHandle({ handle: 'form-field-settings-default-checked' }, { role: 'checkbox', label: 'Whether this box starts checked' }) }), t('Checked by default')] })] })) : null, settings.defaultKind === 'choice' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsxs("select", { id: "field-settings-default", className: "field-settings-compact", value: form.defaultChoices[0] ?? '', onChange: view.onDefaultChoice, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'Which option starts selected — set with page.select_option' }), children: [_jsx("option", { value: "", children: t('None') }), view.defaultChoices.map((choice) => _jsx("option", { value: choice.value, children: choice.label }, choice.key))] })] })) : null, view.defaultInputType ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsx("input", { id: "field-settings-default", "data-jini-autofocus": "", className: view.defaultClass, type: view.defaultInputType, value: form.defaultText, onChange: view.onDefaultText, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'The value this field starts with' }) })] })) : null] }));
|
|
18
|
+
}
|
|
19
|
+
/** Length, pattern and range rows. */
|
|
20
|
+
export function FieldSettingsLimitRows({ view, t }) {
|
|
21
|
+
const { settings, form } = view;
|
|
22
|
+
return (_jsxs(_Fragment, { children: [settings.length ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-length-label", children: t('Length') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-length-label", children: [_jsx("input", { type: "number", min: 0, className: "field-settings-num", "aria-label": t('Min length'), placeholder: t('Min'), value: form.minLength, onChange: view.onMinLength, ...agentHandle({ handle: 'form-field-settings-min-length' }, { role: 'field', label: 'Fewest characters accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: "number", min: 1, className: "field-settings-num", "aria-label": t('Max length'), placeholder: t('Max'), value: form.maxLength, onChange: view.onMaxLength, ...agentHandle({ handle: 'form-field-settings-max-length' }, { role: 'field', label: 'Most characters accepted; blank means the default limit' }) })] })] })) : null, settings.pattern ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-pattern", children: t('Pattern') }), _jsx(InfoTip, { label: t('Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.') })] }), _jsx("input", { id: "field-settings-pattern", className: "field-settings-code", value: form.pattern, onChange: view.onPattern, spellCheck: false, ...agentHandle({ handle: 'form-field-settings-pattern' }, { role: 'field', label: 'Regular expression the whole answer must match' }) })] })) : null, settings.bounds ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-bounds-label", children: t('Range') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-bounds-label", children: [_jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Min'), placeholder: t('Min'), value: form.min, onChange: view.onMin, ...agentHandle({ handle: 'form-field-settings-min' }, { role: 'field', label: 'Lowest value or earliest date/time accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Max'), placeholder: t('Max'), value: form.max, onChange: view.onMax, ...agentHandle({ handle: 'form-field-settings-max' }, { role: 'field', label: 'Highest value or latest date/time accepted' }) }), settings.step ? (_jsx("input", { type: "number", min: 0, step: "any", className: "field-settings-num", "aria-label": t('Step'), placeholder: t('Step'), value: form.step, onChange: view.onStep, ...agentHandle({ handle: 'form-field-settings-step' }, { role: 'field', label: 'Increment between accepted values' }) })) : null] })] })) : null] }));
|
|
23
|
+
}
|
|
24
|
+
/** Accepted file types and max size of an upload field. */
|
|
25
|
+
export function FieldSettingsUploadRows({ view, t }) {
|
|
26
|
+
const { form } = view;
|
|
27
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-files-label", children: t('Files') }), _jsx(InfoTip, { label: t('Leave all unticked to accept every safe type: images, video and PDF.') })] }), _jsx("span", { className: "field-settings-checks", role: "group", "aria-labelledby": "field-settings-files-label", children: view.uploads.map((choice) => (_jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: choice.checked, onChange: choice.onToggle, ...agentHandle({ handle: choice.handle }, { role: 'checkbox', label: `Accept ${choice.label} uploads` }) }), t(choice.label)] }, choice.value))) })] }), _jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-max-size", children: t('Max size (MB)') }), _jsx("input", { id: "field-settings-max-size", type: "number", min: 0, max: MAX_UPLOAD_MB, step: "any", className: "field-settings-num", placeholder: String(MAX_UPLOAD_MB), value: form.maxFileSizeMb, onChange: view.onMaxFileSize, ...agentHandle({ handle: 'form-field-settings-max-size' }, { role: 'field', label: 'Largest upload accepted, in megabytes' }) })] })] }));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=FieldSettingsRows.js.map
|
|
@@ -15,5 +15,5 @@ export interface FieldSettingsSectionProps {
|
|
|
15
15
|
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
16
16
|
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
17
17
|
*/
|
|
18
|
-
export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps
|
|
18
|
+
export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps): import("react").JSX.Element;
|
|
19
19
|
//# sourceMappingURL=FieldSettingsSection.d.ts.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { agentHandle } from '@jini-ai/agentic';
|
|
3
|
-
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
4
|
-
import { MAX_UPLOAD_MB } from '../../field-settings.js';
|
|
5
3
|
import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
4
|
+
import { FieldSettingsDefaultRows, FieldSettingsLimitRows, FieldSettingsOptionsRow, FieldSettingsTextRows, FieldSettingsUploadRows } from './FieldSettingsRows.js';
|
|
6
5
|
/**
|
|
7
6
|
* The settings half of the field dialog: one label-beside-control row per setting the field's
|
|
8
7
|
* registry kind supports (`field-settings.ts` decides which). Markup only; every handler comes from
|
|
@@ -13,9 +12,9 @@ import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
|
13
12
|
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
14
13
|
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
15
14
|
*/
|
|
16
|
-
export function FieldSettingsSection({ field, controller, t }
|
|
15
|
+
export function FieldSettingsSection({ field, controller, t }) {
|
|
17
16
|
const view = useFieldSettingsView({ field, controller, t });
|
|
18
|
-
const { settings
|
|
19
|
-
return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [
|
|
17
|
+
const { settings } = view;
|
|
18
|
+
return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [_jsx(FieldSettingsTextRows, { view: view, t: t }), settings.options ? _jsx(FieldSettingsOptionsRow, { view: view, t: t }) : null, _jsx(FieldSettingsDefaultRows, { view: view, t: t }), _jsx(FieldSettingsLimitRows, { view: view, t: t }), settings.upload ? _jsx(FieldSettingsUploadRows, { view: view, t: t }) : null] }));
|
|
20
19
|
}
|
|
21
20
|
//# sourceMappingURL=FieldSettingsSection.js.map
|
|
@@ -21,7 +21,7 @@ export interface FormEditorFieldsBodyProps {
|
|
|
21
21
|
onCopyEmbed: () => void;
|
|
22
22
|
copyFeedback: string | null;
|
|
23
23
|
}
|
|
24
|
-
export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps
|
|
24
|
+
export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps): import("react").JSX.Element;
|
|
25
25
|
/** The page-header title/description text — split out because `FormEditor`'s isNew-dependent copy
|
|
26
26
|
* (two ternaries, one with a `||` fallback) is otherwise indistinguishable, in the complexity
|
|
27
27
|
* count, from the branches that actually decide what's on screen. */
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
|
|
3
3
|
import { agentHandle } from '@jini-ai/agentic';
|
|
4
|
-
import {
|
|
5
|
-
export function FormEditorFieldsBody(props
|
|
4
|
+
import { FormEditorActions, FormEditorModeSwitch } from "./FormEditorFieldsBodyParts.js";
|
|
5
|
+
export function FormEditorFieldsBody(props) {
|
|
6
6
|
const handlers = useFormFieldsBodyView(props);
|
|
7
|
-
const { name,
|
|
7
|
+
const { name, slug, isNew, t } = props;
|
|
8
8
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "field-group", children: _jsxs("div", { className: "field-row", children: [_jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-name", children: t("Name") }), _jsx("input", { id: "form-name", value: name, onChange: handlers.onName, ...agentHandle({ handle: "form-editor-name" }, { role: "field", label: "This form's display name" }) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-slug", children: t("Slug") }), _jsx("input", { id: "form-slug", value: slug, disabled: !isNew, onChange: handlers.onSlug, ...agentHandle({ handle: "form-editor-slug" }, {
|
|
9
9
|
role: "field",
|
|
10
10
|
label: "This form's URL slug — only editable while creating a new form",
|
|
11
|
-
}) })] })] }) }),
|
|
12
|
-
// Reversible-but-access-affecting (turns off the live site's ability to accept
|
|
13
|
-
// submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
|
|
14
|
-
// deleted, and the same control flips right back to "Enable". Re-enabling is the safe
|
|
15
|
-
// direction, so it stays `.btn-secondary` rather than inheriting the warning look.
|
|
16
|
-
_jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
|
|
17
|
-
role: "button",
|
|
18
|
-
label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
|
|
19
|
-
}), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
|
|
20
|
-
role: "button",
|
|
21
|
-
label: "Save this form's name, slug, fields and notification settings",
|
|
22
|
-
}), children: isNew ? t("Create form") : t("Save") })] })] }));
|
|
11
|
+
}) })] })] }) }), _jsx("div", { className: "field-group", children: _jsx(FormEditorModeSwitch, { props: props, handlers: handlers }) }), _jsx(FormEditorActions, { props: props })] }));
|
|
23
12
|
}
|
|
24
13
|
//# sourceMappingURL=FormEditorFieldsBody.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
|
|
2
|
+
import type { FormEditorFieldsBodyProps } from "./FormEditorFieldsBody.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file The two halves of `FormEditorFieldsBody` that branch on its props — the Builder/HTML switch
|
|
5
|
+
* and the action row — split out so each one's branches are counted on their own. Markup only.
|
|
6
|
+
*/
|
|
7
|
+
type FieldsBodyHandlers = ReturnType<typeof useFormFieldsBodyView>;
|
|
8
|
+
/** Builder/HTML switch plus whichever editor the current mode shows. */
|
|
9
|
+
export declare function FormEditorModeSwitch({ props, handlers }: {
|
|
10
|
+
props: FormEditorFieldsBodyProps;
|
|
11
|
+
handlers: FieldsBodyHandlers;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
|
|
14
|
+
export declare function FormEditorActions({ props }: {
|
|
15
|
+
props: FormEditorFieldsBodyProps;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=FormEditorFieldsBodyParts.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
3
|
+
import { FormFieldsEditor } from "./FormFieldsEditor.js";
|
|
4
|
+
/** Builder/HTML switch plus whichever editor the current mode shows. */
|
|
5
|
+
export function FormEditorModeSwitch({ props, handlers }) {
|
|
6
|
+
const { fields, existingFieldIds, onFieldsChange, t, mode, html } = props;
|
|
7
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "segmented", role: "group", "aria-label": t("Form authoring mode"), children: [_jsx("button", { type: "button", className: mode === "builder" ? "is-active" : "", "aria-pressed": mode === "builder", onClick: handlers.onBuilder, children: t("Builder") }), _jsx("button", { type: "button", className: mode === "html" ? "is-active" : "", "aria-pressed": mode === "html", onClick: handlers.onHtmlMode, children: t("HTML") })] }), mode === "html" ? (_jsx("textarea", { className: "page-html-source", value: html, onChange: handlers.onHtml, spellCheck: false, "aria-label": t("Form HTML") })) : (_jsx("div", { className: "table-scroll", children: _jsx(FormFieldsEditor, { fields: fields, existingFieldIds: existingFieldIds, onChange: onFieldsChange, t: t }) }))] }));
|
|
8
|
+
}
|
|
9
|
+
/** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
|
|
10
|
+
export function FormEditorActions({ props }) {
|
|
11
|
+
const { isNew, form, saving, onStatusToggle, onSave, t, onCopyEmbed, copyFeedback } = props;
|
|
12
|
+
/* `form-actions` is a spacing-only hook layered on top of the shared `.editor-actions`
|
|
13
|
+
flex row. `.editor-actions` sets direction/gap/alignment but no top margin, and this row
|
|
14
|
+
is not a `.field-group`, so the `.field-group + .field-group` rhythm that separates every
|
|
15
|
+
other block on this screen skips it — leaving Disable/Save flush against the Recipients
|
|
16
|
+
input. Fixed here rather than by adding a margin to `.editor-actions` itself, because that
|
|
17
|
+
class is shared with the other editor screens and a global change would shift spacing on
|
|
18
|
+
screens nobody has looked at yet. */
|
|
19
|
+
return (_jsxs("div", { className: "editor-actions form-actions", children: [form ? _jsx("button", { type: "button", className: "btn-secondary", onClick: onCopyEmbed, children: t("Copy HTML embed") }) : null, copyFeedback ? _jsx("span", { role: "status", children: copyFeedback }) : null, !isNew && form ? (
|
|
20
|
+
// Reversible-but-access-affecting (turns off the live site's ability to accept
|
|
21
|
+
// submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
|
|
22
|
+
// deleted, and the same control flips right back to "Enable". Re-enabling is the safe
|
|
23
|
+
// direction, so it stays `.btn-secondary` rather than inheriting the warning look.
|
|
24
|
+
_jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
|
|
25
|
+
role: "button",
|
|
26
|
+
label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
|
|
27
|
+
}), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
|
|
28
|
+
role: "button",
|
|
29
|
+
label: "Save this form's name, slug, fields and notification settings",
|
|
30
|
+
}), children: isNew ? t("Create form") : t("Save") })] }));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=FormEditorFieldsBodyParts.js.map
|
|
@@ -8,5 +8,5 @@ export interface FormSubmissionsProps {
|
|
|
8
8
|
t: (key: string) => string;
|
|
9
9
|
answerColumns?: FormAnswerColumn[];
|
|
10
10
|
}
|
|
11
|
-
export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps
|
|
11
|
+
export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps): import("react").JSX.Element;
|
|
12
12
|
//# sourceMappingURL=FormSubmissions.d.ts.map
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useSubmissionsView } from "../hooks/forms-view.hooks.js";
|
|
3
|
-
import { DataTable } from '../../../react/components/DataTable.js';
|
|
4
|
-
import { agentHandle } from '@jini-ai/agentic';
|
|
5
3
|
import { useWiredFormSubmissions } from '../hooks/use-form-submissions.hooks.js';
|
|
6
4
|
import { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
|
|
7
5
|
import { FormSubmissionDetail } from "./FormSubmissionDetail.js";
|
|
8
|
-
import {
|
|
9
|
-
export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }
|
|
6
|
+
import { FormSubmissionsTable } from "./FormSubmissionsTable.js";
|
|
7
|
+
export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }) {
|
|
10
8
|
const formatDate = useFormSubmissionDate({});
|
|
11
9
|
const controller = useFormSubmissionsHook({ formId });
|
|
12
10
|
const { submissions, nextCursor, error, selectedId, loadingMore } = controller;
|
|
@@ -27,20 +25,6 @@ export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormS
|
|
|
27
25
|
// own post-early-return computation. `submissions` grows only on an explicit "Load more" click
|
|
28
26
|
// (`use-form-submissions.hooks.ts`'s cursor-append), so this O(n) pass tracks real data changes,
|
|
29
27
|
// not incidental re-renders.
|
|
30
|
-
return (
|
|
31
|
-
{ key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
|
|
32
|
-
{ key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
|
|
33
|
-
...answerColumns,
|
|
34
|
-
{
|
|
35
|
-
key: "view",
|
|
36
|
-
cell: (s, index) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
|
|
37
|
-
role: "button",
|
|
38
|
-
label: "Open this submission's full details",
|
|
39
|
-
}), children: t("View") })),
|
|
40
|
-
},
|
|
41
|
-
] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
|
|
42
|
-
role: "button",
|
|
43
|
-
label: "Load the next page of submissions",
|
|
44
|
-
}), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
|
|
28
|
+
return (_jsx(FormSubmissionsTable, { rows: rows, loadMore: loadMore, error: error, nextCursor: nextCursor, loadingMore: loadingMore, formatDate: formatDate, answerColumns: answerColumns, t: t }));
|
|
45
29
|
}
|
|
46
30
|
//# sourceMappingURL=FormSubmissions.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FormAnswerColumn } from '../../html-rules.js';
|
|
2
|
+
import type { useSubmissionsView } from "../hooks/forms-view.hooks.js";
|
|
3
|
+
import type { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
|
|
4
|
+
/**
|
|
5
|
+
* @file The loaded-list half of `FormSubmissions`: an optional error above the table, the table
|
|
6
|
+
* itself, and "Load more" while a next page exists. Markup only.
|
|
7
|
+
*/
|
|
8
|
+
export interface FormSubmissionsTableProps {
|
|
9
|
+
rows: ReturnType<typeof useSubmissionsView>["rows"];
|
|
10
|
+
loadMore: () => void;
|
|
11
|
+
error: string | null;
|
|
12
|
+
nextCursor: string | null;
|
|
13
|
+
loadingMore: boolean;
|
|
14
|
+
formatDate: ReturnType<typeof useFormSubmissionDate>;
|
|
15
|
+
answerColumns: FormAnswerColumn[];
|
|
16
|
+
t: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
export declare function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }: FormSubmissionsTableProps): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=FormSubmissionsTable.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataTable } from '../../../react/components/DataTable.js';
|
|
3
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
4
|
+
import { FormSubmissionDate } from "./FormSubmissionDate.js";
|
|
5
|
+
export function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }) {
|
|
6
|
+
return (_jsxs("div", { children: [error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx(DataTable, { rows: rows, rowKey: (s) => s.id, columns: [
|
|
7
|
+
{ key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
|
|
8
|
+
{ key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
|
|
9
|
+
...answerColumns,
|
|
10
|
+
{
|
|
11
|
+
key: "view",
|
|
12
|
+
cell: (s) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
|
|
13
|
+
role: "button",
|
|
14
|
+
label: "Open this submission's full details",
|
|
15
|
+
}), children: t("View") })),
|
|
16
|
+
},
|
|
17
|
+
] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
|
|
18
|
+
role: "button",
|
|
19
|
+
label: "Load the next page of submissions",
|
|
20
|
+
}), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=FormSubmissionsTable.js.map
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { queryMedia } from '../rules.js';
|
|
2
|
+
const METADATA_KEYS = ['title', 'slug', 'alt', 'caption', 'credit', 'width', 'height', 'cssClass', 'htmlAttributes'];
|
|
3
|
+
/** Only metadata keys with a defined value; null stays, so a patch can clear a nullable field. */
|
|
4
|
+
function pickMetadata({ patch }) {
|
|
5
|
+
return Object.fromEntries(Object.entries(patch).filter(([key, value]) => METADATA_KEYS.includes(key) && value !== undefined));
|
|
6
|
+
}
|
|
7
|
+
/** Decoded length of a base64 string: 3 bytes per 4 chars, minus trailing padding. */
|
|
8
|
+
function base64ByteSize({ dataBase64 }) {
|
|
9
|
+
const padding = dataBase64.match(/=+$/)?.[0].length ?? 0;
|
|
10
|
+
return Math.max(0, Math.floor(dataBase64.length * 3 / 4) - padding);
|
|
11
|
+
}
|
|
2
12
|
export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().toISOString() } = {}) {
|
|
3
13
|
const items = new Map(assets.map((item) => [item.id, Object.freeze({ ...item })]));
|
|
4
14
|
let sequence = 0;
|
|
@@ -20,7 +30,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
20
30
|
// The REAL server ignores the declared string and stores sniffContentType(bytes) instead.
|
|
21
31
|
// This fake has no bytes to sniff — its input is an opaque base64 string a test made up —
|
|
22
32
|
// so it echoes what the caller declared. Tests needing disagreement must seed assets directly.
|
|
23
|
-
return { byteSize:
|
|
33
|
+
return { byteSize: base64ByteSize(input), contentType: input.contentType, sha256: `memory-content-${++sequence}` };
|
|
24
34
|
}
|
|
25
35
|
return {
|
|
26
36
|
replaceSupported: true,
|
|
@@ -38,15 +48,16 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
38
48
|
const timestamp = now();
|
|
39
49
|
return put({
|
|
40
50
|
id,
|
|
41
|
-
title:
|
|
42
|
-
alt:
|
|
43
|
-
caption:
|
|
44
|
-
credit:
|
|
45
|
-
slug:
|
|
46
|
-
width:
|
|
47
|
-
height:
|
|
48
|
-
cssClass:
|
|
49
|
-
htmlAttributes:
|
|
51
|
+
title: input.filename,
|
|
52
|
+
alt: '',
|
|
53
|
+
caption: '',
|
|
54
|
+
credit: '',
|
|
55
|
+
slug: id,
|
|
56
|
+
width: null,
|
|
57
|
+
height: null,
|
|
58
|
+
cssClass: null,
|
|
59
|
+
htmlAttributes: null,
|
|
60
|
+
...pickMetadata({ patch: options }),
|
|
50
61
|
status: 'active',
|
|
51
62
|
publicUrl: null,
|
|
52
63
|
createdAt: timestamp,
|
|
@@ -58,14 +69,13 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
58
69
|
async update({ id, patch }, options = {}) {
|
|
59
70
|
check(options);
|
|
60
71
|
const previous = get(id);
|
|
61
|
-
|
|
62
|
-
return put({ ...previous, ...metadata, version: previous.version + 1, updatedAt: now() });
|
|
72
|
+
return put({ ...previous, ...pickMetadata({ patch }), version: previous.version + 1, updatedAt: now() });
|
|
63
73
|
},
|
|
64
74
|
async replace({ id, upload }, options = {}) {
|
|
65
75
|
check(options);
|
|
66
76
|
const previous = get(id);
|
|
67
77
|
if (previous.status !== 'active')
|
|
68
|
-
throw new Error(
|
|
78
|
+
throw new Error(`Cannot replace trashed media: ${id} (restore it first)`);
|
|
69
79
|
return put({
|
|
70
80
|
...previous,
|
|
71
81
|
...content(upload),
|
|
@@ -92,7 +102,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
92
102
|
async delete({ id }, options = {}) {
|
|
93
103
|
check(options);
|
|
94
104
|
if (get(id).status !== 'trashed')
|
|
95
|
-
throw new Error(
|
|
105
|
+
throw new Error(`Trash media before permanent deletion: ${id}`);
|
|
96
106
|
items.delete(id);
|
|
97
107
|
return { purged: true };
|
|
98
108
|
},
|
|
@@ -102,11 +112,11 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
102
112
|
}
|
|
103
113
|
export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
|
|
104
114
|
const items = new Map(providers.map((item) => [item.id, Object.freeze({ ...item })]));
|
|
105
|
-
function write(id,
|
|
115
|
+
function write(id, patch) {
|
|
106
116
|
const previous = items.get(id);
|
|
107
117
|
if (!previous)
|
|
108
|
-
throw new Error(
|
|
109
|
-
const next = Object.freeze({ ...previous,
|
|
118
|
+
throw new Error(`Unknown media provider: ${id}`);
|
|
119
|
+
const next = Object.freeze({ ...previous, ...patch });
|
|
110
120
|
items.set(id, next);
|
|
111
121
|
return next;
|
|
112
122
|
}
|
|
@@ -117,22 +127,17 @@ export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
|
|
|
117
127
|
},
|
|
118
128
|
async saveSettings({ id, baseUrl, model }, options = {}) {
|
|
119
129
|
options.signal?.throwIfAborted();
|
|
120
|
-
|
|
121
|
-
if (!previous)
|
|
122
|
-
throw new Error('Unknown provider');
|
|
123
|
-
const next = Object.freeze({ ...previous, baseUrl, model });
|
|
124
|
-
items.set(id, next);
|
|
125
|
-
return next;
|
|
130
|
+
return write(id, { baseUrl, model });
|
|
126
131
|
},
|
|
127
132
|
async saveCredential({ id, credential }, options = {}) {
|
|
128
133
|
options.signal?.throwIfAborted();
|
|
129
134
|
if (!credential.trim())
|
|
130
|
-
throw new Error(
|
|
131
|
-
return write(id, true);
|
|
135
|
+
throw new Error(`Credential is required for media provider: ${id}`);
|
|
136
|
+
return write(id, { configured: true });
|
|
132
137
|
},
|
|
133
138
|
async removeCredential({ id }, options = {}) {
|
|
134
139
|
options.signal?.throwIfAborted();
|
|
135
|
-
return write(id, false);
|
|
140
|
+
return write(id, { configured: false });
|
|
136
141
|
},
|
|
137
142
|
};
|
|
138
143
|
}
|
|
@@ -31,11 +31,12 @@ export function createEditMediaController({ api, item }, _optional = {}) {
|
|
|
31
31
|
store.set({
|
|
32
32
|
patch: { error: error instanceof Error ? error.message : 'Failed to save media metadata' },
|
|
33
33
|
});
|
|
34
|
-
return false;
|
|
35
34
|
}
|
|
36
35
|
finally {
|
|
37
36
|
store.set({ patch: { saving: false } });
|
|
38
37
|
}
|
|
38
|
+
// Reached only through the catch above: the failure is already recorded in `error`.
|
|
39
|
+
return false;
|
|
39
40
|
}
|
|
40
41
|
return {
|
|
41
42
|
getSnapshot: store.getSnapshot,
|