@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
|
@@ -7,6 +7,23 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
|
|
|
7
7
|
passed++;
|
|
8
8
|
else
|
|
9
9
|
failures.push(name); };
|
|
10
|
+
const created = await checkCreateAndRead({ api, slug, check });
|
|
11
|
+
await checkUpdates({ api, created, check });
|
|
12
|
+
if (submission)
|
|
13
|
+
await checkSubmissions({ api, created, submission, check });
|
|
14
|
+
return { passed, failures };
|
|
15
|
+
}
|
|
16
|
+
/** Resolves true when the operation is refused; only that probe's rejection is absorbed. */
|
|
17
|
+
async function rejects({ operation }) {
|
|
18
|
+
try {
|
|
19
|
+
await operation();
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function checkCreateAndRead({ api, slug, check }) {
|
|
10
27
|
const field = { id: 'email', label: 'Email', type: 'email', required: true, className: 'wide', attributes: { 'aria-label': 'Email' } };
|
|
11
28
|
const created = await api.createFormDefinition({ name: 'Contact', slug, fields: [field] }, { notify: { enabled: true, recipients: ['owner@example.com'] } });
|
|
12
29
|
check('create returns requested definition', created.name === 'Contact' && created.slug === slug && created.status === 'active');
|
|
@@ -16,56 +33,35 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
|
|
|
16
33
|
const snapshot = await api.getFormDefinition({ id: created.id });
|
|
17
34
|
Object.assign(snapshot.fields[0], { label: 'Changed only in the read snapshot' });
|
|
18
35
|
check('read snapshots cannot mutate stored fields', (await api.getFormDefinition({ id: created.id })).fields[0]?.label === 'Email');
|
|
19
|
-
|
|
36
|
+
return created;
|
|
37
|
+
}
|
|
38
|
+
async function checkUpdates({ api, created, check }) {
|
|
39
|
+
const { id } = created;
|
|
40
|
+
const updated = await api.updateFormDefinition({ id }, { name: 'Renamed', fields: [...created.fields, { id: 'message', label: 'Message', type: 'textarea', required: false }] });
|
|
20
41
|
check('name patch and added field persist', updated.name === 'Renamed' && updated.fields.length === 2);
|
|
21
42
|
check('untouched notify survives update', updated.notify.enabled && updated.notify.recipients[0] === 'owner@example.com');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
refused = true;
|
|
28
|
-
}
|
|
29
|
-
check('existing fields cannot be removed', refused);
|
|
30
|
-
check('rejected removal leaves fields intact', (await api.getFormDefinition({ id: created.id })).fields.length === 2);
|
|
31
|
-
const disabled = await api.updateFormDefinition({ id: created.id }, { status: 'disabled' });
|
|
32
|
-
check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id: created.id }, { status: 'active' })).status === 'active');
|
|
33
|
-
const html = await api.updateFormDefinition({ id: created.id }, { mode: 'html', html: '<input name="email">' });
|
|
43
|
+
check('existing fields cannot be removed', await rejects({ operation: () => api.updateFormDefinition({ id }, { fields: [] }) }));
|
|
44
|
+
check('rejected removal leaves fields intact', (await api.getFormDefinition({ id })).fields.length === 2);
|
|
45
|
+
const disabled = await api.updateFormDefinition({ id }, { status: 'disabled' });
|
|
46
|
+
check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id }, { status: 'active' })).status === 'active');
|
|
47
|
+
const html = await api.updateFormDefinition({ id }, { mode: 'html', html: '<input name="email">' });
|
|
34
48
|
check('HTML authoring survives update', html.mode === 'html' && html.html === '<input name="email">');
|
|
35
|
-
check('builder switch is explicit', (await api.updateFormDefinition({ id
|
|
36
|
-
check('empty submission page has null cursor', (await api.listFormSubmissions({ formId:
|
|
49
|
+
check('builder switch is explicit', (await api.updateFormDefinition({ id }, { mode: 'builder' })).mode === 'builder');
|
|
50
|
+
check('empty submission page has null cursor', (await api.listFormSubmissions({ formId: id })).nextCursor === null);
|
|
37
51
|
check('definition has no delete operation', !('deleteFormDefinition' in api));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
check('a submission of another form is unavailable', wrongFormRejected);
|
|
50
|
-
let wrongFormDeleteRejected = false;
|
|
51
|
-
try {
|
|
52
|
-
await api.deleteFormSubmission({ formId: created.id, submissionId: submission.submissionId });
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
wrongFormDeleteRejected = true;
|
|
56
|
-
}
|
|
57
|
-
check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && (await api.getFormSubmission(submission)).id === submission.submissionId);
|
|
52
|
+
}
|
|
53
|
+
async function checkSubmissions({ api, created, submission, check }) {
|
|
54
|
+
const crossForm = { formId: created.id, submissionId: submission.submissionId };
|
|
55
|
+
const page = await api.listFormSubmissions({ formId: submission.formId }, { limit: 1 });
|
|
56
|
+
check('submission page is form scoped', page.items.every(item => item.formDefinitionId === submission.formId));
|
|
57
|
+
check('submission detail is scoped', (await api.getFormSubmission(submission)).formDefinitionId === submission.formId);
|
|
58
|
+
check('a submission of another form is unavailable', await rejects({ operation: () => api.getFormSubmission(crossForm) }));
|
|
59
|
+
const wrongFormDeleteRejected = await rejects({ operation: () => api.deleteFormSubmission(crossForm) });
|
|
60
|
+
const submissionRetained = await api.getFormSubmission(submission).then(item => item.id === submission.submissionId, () => false);
|
|
61
|
+
check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && submissionRetained);
|
|
62
|
+
if (submissionRetained)
|
|
58
63
|
await api.deleteFormSubmission(submission);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await api.getFormSubmission(submission);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
missing = true;
|
|
65
|
-
}
|
|
66
|
-
check('trashed submission detail is unavailable', missing);
|
|
67
|
-
check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
|
|
68
|
-
}
|
|
69
|
-
return { passed, failures };
|
|
64
|
+
check('trashed submission detail is unavailable', await rejects({ operation: () => api.getFormSubmission(submission) }));
|
|
65
|
+
check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
|
|
70
66
|
}
|
|
71
67
|
//# sourceMappingURL=forms-api.conformance.js.map
|
|
@@ -23,6 +23,20 @@ function defaultKindOf(kind) {
|
|
|
23
23
|
return "temporal";
|
|
24
24
|
return kind.control === "textarea" ? "textarea" : "text";
|
|
25
25
|
}
|
|
26
|
+
const isNumeric = ({ kind }) => kind.htmlType === "number" || kind.htmlType === "range";
|
|
27
|
+
/** Native input type for a kind's min/max; number and range share numeric bounds. */
|
|
28
|
+
function boundsOf({ kind }) {
|
|
29
|
+
if (!BOUNDS.has(kind.htmlType))
|
|
30
|
+
return null;
|
|
31
|
+
return isNumeric({ kind }) ? "number" : kind.htmlType;
|
|
32
|
+
}
|
|
33
|
+
/** Whether a (non-hidden) kind shows a placeholder. */
|
|
34
|
+
function takesPlaceholder({ kind }) {
|
|
35
|
+
// A single-choice select shows its placeholder as the empty first option.
|
|
36
|
+
if (kind.control === "select")
|
|
37
|
+
return !kind.multiple;
|
|
38
|
+
return kind.textual === true || kind.htmlType === "number";
|
|
39
|
+
}
|
|
26
40
|
/**
|
|
27
41
|
* Settings for one type, from its registry kind. Unknown (legacy or host-added) types get help
|
|
28
42
|
* text only, so the editor never offers a setting the server would refuse.
|
|
@@ -33,15 +47,13 @@ export function fieldSettingsFor({ type }, _optional = {}) {
|
|
|
33
47
|
if (!kind)
|
|
34
48
|
return NO_SETTINGS;
|
|
35
49
|
const hidden = kind.htmlType === "hidden";
|
|
36
|
-
const numeric =
|
|
37
|
-
const bounds = BOUNDS.has(kind.htmlType) ? (numeric ? "number" : kind.htmlType) : null;
|
|
50
|
+
const numeric = isNumeric({ kind });
|
|
38
51
|
return {
|
|
39
|
-
|
|
40
|
-
placeholder: !hidden && ((kind.textual === true && kind.control !== "select") || kind.htmlType === "number" || (kind.control === "select" && !kind.multiple)),
|
|
52
|
+
placeholder: !hidden && takesPlaceholder({ kind }),
|
|
41
53
|
helpText: !hidden,
|
|
42
54
|
length: kind.textual === true,
|
|
43
55
|
pattern: kind.textual === true && !hidden,
|
|
44
|
-
bounds,
|
|
56
|
+
bounds: boundsOf({ kind }),
|
|
45
57
|
step: numeric,
|
|
46
58
|
options: kind.choices === true,
|
|
47
59
|
multiple: kind.multiple === true,
|
|
@@ -96,53 +108,45 @@ const SEED_OPTIONS = [{ value: "Option 1", label: "Option 1" }, { value: "Option
|
|
|
96
108
|
export function retypeField({ field, type }, _optional = {}) {
|
|
97
109
|
const before = fieldSettingsFor({ type: field.type });
|
|
98
110
|
const after = fieldSettingsFor({ type });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (field.maxFileSize !== undefined)
|
|
130
|
-
next.maxFileSize = field.maxFileSize;
|
|
111
|
+
return {
|
|
112
|
+
id: field.id, label: field.label, type, required: after.helpText ? field.required : false,
|
|
113
|
+
...setKeys({ field, keys: retainedKeys({ field, before, after }) }),
|
|
114
|
+
...(after.options ? { options: (field.options?.length ? field.options : SEED_OPTIONS).map((option) => ({ ...option })) } : {}),
|
|
115
|
+
...setKeys({ field, keys: after.upload ? ["accept", "maxFileSize"] : [] }),
|
|
116
|
+
// A default only survives when its shape is unchanged (text to text, one choice to one choice).
|
|
117
|
+
...(keepsDefault({ before, after }) && field.default !== undefined ? { default: field.default } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const when = (applies, keys) => (applies ? keys : []);
|
|
121
|
+
/** The settings before the options that a retype keeps, in the field's key order. */
|
|
122
|
+
function retainedKeys({ field, before, after }) {
|
|
123
|
+
return [
|
|
124
|
+
...when(after.helpText, ["helpText"]), "className", "attributes", ...when(after.placeholder, ["placeholder"]),
|
|
125
|
+
// A null max length (the wire's "cleared") is dropped rather than carried.
|
|
126
|
+
...when(after.length && field.maxLength != null, ["maxLength"]), ...when(after.length, ["minLength"]),
|
|
127
|
+
...when(after.pattern, ["pattern"]),
|
|
128
|
+
...when(after.bounds !== null && after.bounds === before.bounds, ["min", "max"]),
|
|
129
|
+
...when(after.step, ["step"]),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
const keepsDefault = ({ before, after }) => after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds;
|
|
133
|
+
/** The listed keys the field has set, in order; attribute maps and accept lists are copied, not shared. */
|
|
134
|
+
function setKeys({ field, keys }) {
|
|
135
|
+
const kept = {};
|
|
136
|
+
for (const key of keys) {
|
|
137
|
+
const value = field[key];
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
Object.assign(kept, { [key]: Array.isArray(value) ? [...value] : typeof value === "object" && value !== null ? { ...value } : value });
|
|
131
141
|
}
|
|
132
|
-
|
|
133
|
-
if (field.default !== undefined && after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds)
|
|
134
|
-
next.default = field.default;
|
|
135
|
-
return next;
|
|
142
|
+
return kept;
|
|
136
143
|
}
|
|
137
144
|
const text = (value) => (value === undefined || value === null ? "" : String(value));
|
|
138
145
|
/** Seed the draft from a stored field. @complexity O(o) in options. */
|
|
139
146
|
export function settingsFormFromField({ field }, _optional = {}) {
|
|
140
|
-
const defaults = field.default;
|
|
141
147
|
return {
|
|
142
148
|
placeholder: text(field.placeholder), helpText: text(field.helpText),
|
|
143
|
-
|
|
144
|
-
defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
|
|
145
|
-
defaultChoices: Array.isArray(defaults) ? [...defaults] : typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [],
|
|
149
|
+
...defaultDraft({ field }),
|
|
146
150
|
minLength: text(field.minLength), maxLength: text(field.maxLength), pattern: text(field.pattern),
|
|
147
151
|
min: text(field.min), max: text(field.max), step: text(field.step),
|
|
148
152
|
options: (field.options ?? []).map((option, index) => ({ _rowId: index, value: option.value, label: option.label })),
|
|
@@ -150,6 +154,21 @@ export function settingsFormFromField({ field }, _optional = {}) {
|
|
|
150
154
|
maxFileSizeMb: field.maxFileSize === undefined ? "" : String(Math.round((field.maxFileSize / 1024 / 1024) * 100) / 100),
|
|
151
155
|
};
|
|
152
156
|
}
|
|
157
|
+
/** The stored default as the dialog's three default controls. */
|
|
158
|
+
function defaultDraft({ field }) {
|
|
159
|
+
const defaults = field.default;
|
|
160
|
+
return {
|
|
161
|
+
defaultText: typeof defaults === "boolean" || Array.isArray(defaults) ? "" : text(defaults),
|
|
162
|
+
defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
|
|
163
|
+
defaultChoices: defaultChoicesOf({ field }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function defaultChoicesOf({ field }) {
|
|
167
|
+
const defaults = field.default;
|
|
168
|
+
if (Array.isArray(defaults))
|
|
169
|
+
return [...defaults];
|
|
170
|
+
return typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [];
|
|
171
|
+
}
|
|
153
172
|
/**
|
|
154
173
|
* Edit one option row. A value that still mirrors its label follows label edits, so typing labels
|
|
155
174
|
* is enough for most forms; an edited value stays put.
|
|
@@ -199,59 +218,74 @@ function optionalNumber(value, error, { integer = false } = {}) {
|
|
|
199
218
|
return Number.isFinite(number) && (!integer || (Number.isInteger(number) && number >= 0)) ? { ok: true, value: number } : { ok: false, error };
|
|
200
219
|
}
|
|
201
220
|
/** Text length, pattern and placeholder settings. */
|
|
202
|
-
function textPatch(form, settings) {
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
function textPatch({ form, settings }) {
|
|
222
|
+
const length = settings.length ? lengthPatch({ form }) : { ok: true, value: {} };
|
|
223
|
+
if (!length.ok)
|
|
224
|
+
return length;
|
|
225
|
+
const pattern = settings.pattern ? patternOf({ form }) : { ok: true, value: undefined };
|
|
226
|
+
if (!pattern.ok)
|
|
227
|
+
return pattern;
|
|
228
|
+
return { ok: true, value: {
|
|
229
|
+
placeholder: settings.placeholder && !isBlank(form.placeholder) ? form.placeholder : undefined,
|
|
230
|
+
pattern: pattern.value, minLength: undefined, ...length.value,
|
|
231
|
+
} };
|
|
232
|
+
}
|
|
233
|
+
const MIN_ABOVE_MAX = { ok: false, error: "Minimum must not be above maximum." };
|
|
234
|
+
const isAbove = (min, max) => min !== undefined && max !== undefined && min > max;
|
|
235
|
+
function lengthPatch({ form }) {
|
|
236
|
+
const min = optionalNumber(form.minLength, "Length must be a whole number.", { integer: true });
|
|
237
|
+
const max = optionalNumber(form.maxLength, "Length must be a whole number.", { integer: true });
|
|
238
|
+
if (!min.ok)
|
|
239
|
+
return min;
|
|
240
|
+
if (!max.ok)
|
|
241
|
+
return max;
|
|
242
|
+
if (isAbove(min.value, max.value))
|
|
243
|
+
return MIN_ABOVE_MAX;
|
|
244
|
+
// The wire contract clears a text length with null; unsupported types omit the key entirely.
|
|
245
|
+
return { ok: true, value: { minLength: min.value, maxLength: max.value ?? null } };
|
|
246
|
+
}
|
|
247
|
+
function patternOf({ form }) {
|
|
248
|
+
if (isBlank(form.pattern))
|
|
249
|
+
return { ok: true, value: undefined };
|
|
250
|
+
// Browser-safe too: a `v`-flag-invalid pattern is silently ignored by browsers (2026-10-11).
|
|
251
|
+
if (!isBrowserSafeFieldPattern({ pattern: form.pattern }))
|
|
252
|
+
return { ok: false, error: "That pattern isn't supported." };
|
|
253
|
+
return { ok: true, value: form.pattern };
|
|
224
254
|
}
|
|
225
255
|
/** Numeric or date/time bounds, plus a numeric step. Native date/time strings compare as text. */
|
|
226
|
-
function boundsPatch(form, settings) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
256
|
+
function boundsPatch({ form, settings }) {
|
|
257
|
+
if (settings.bounds === "number")
|
|
258
|
+
return numberBoundsPatch({ form });
|
|
259
|
+
if (settings.bounds)
|
|
260
|
+
return textBoundsPatch({ form });
|
|
261
|
+
return { ok: true, value: { min: undefined, max: undefined, step: undefined } };
|
|
262
|
+
}
|
|
263
|
+
function numberBoundsPatch({ form }) {
|
|
264
|
+
const error = "Enter numbers for min, max and step.";
|
|
265
|
+
const min = optionalNumber(form.min, error);
|
|
266
|
+
const max = optionalNumber(form.max, error);
|
|
267
|
+
const step = optionalNumber(form.step, error);
|
|
268
|
+
if (!min.ok)
|
|
269
|
+
return min;
|
|
270
|
+
if (!max.ok)
|
|
271
|
+
return max;
|
|
272
|
+
if (!step.ok)
|
|
273
|
+
return step;
|
|
274
|
+
if (step.value !== undefined && step.value <= 0)
|
|
275
|
+
return { ok: false, error: "Step must be above 0." };
|
|
276
|
+
if (isAbove(min.value, max.value))
|
|
277
|
+
return MIN_ABOVE_MAX;
|
|
278
|
+
return { ok: true, value: { min: min.value, max: max.value, step: step.value } };
|
|
279
|
+
}
|
|
280
|
+
function textBoundsPatch({ form }) {
|
|
281
|
+
const min = isBlank(form.min) ? undefined : form.min;
|
|
282
|
+
const max = isBlank(form.max) ? undefined : form.max;
|
|
283
|
+
if (isAbove(min, max))
|
|
284
|
+
return MIN_ABOVE_MAX;
|
|
285
|
+
return { ok: true, value: { min, max } };
|
|
252
286
|
}
|
|
253
287
|
/** Choice options: blank rows are skipped; the rest need a label and a unique value. */
|
|
254
|
-
function optionsPatch(form, settings) {
|
|
288
|
+
function optionsPatch({ form, settings }) {
|
|
255
289
|
if (!settings.options)
|
|
256
290
|
return { ok: true, value: undefined };
|
|
257
291
|
const options = form.options.filter((row) => !(isBlank(row.value) && isBlank(row.label))).map((row) => ({ value: row.value.trim(), label: row.label.trim() }));
|
|
@@ -262,26 +296,27 @@ function optionsPatch(form, settings) {
|
|
|
262
296
|
}
|
|
263
297
|
return { ok: true, value: options };
|
|
264
298
|
}
|
|
299
|
+
const typedDefault = ({ form }) => ({ ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText });
|
|
300
|
+
const DEFAULT_READERS = {
|
|
301
|
+
// A box with an authored value submits that value, so its "checked" default is that value too.
|
|
302
|
+
boolean: ({ form, checkboxValue }) => ({ ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined }),
|
|
303
|
+
choice: ({ form, offered }) => ({ ok: true, value: form.defaultChoices.find((value) => offered.has(value)) }),
|
|
304
|
+
choices: ({ form, offered }) => {
|
|
305
|
+
const chosen = form.defaultChoices.filter((value) => offered.has(value));
|
|
306
|
+
return { ok: true, value: chosen.length ? chosen : undefined };
|
|
307
|
+
},
|
|
308
|
+
number: ({ form }) => optionalNumber(form.defaultText, "Default must be a number."),
|
|
309
|
+
text: typedDefault, textarea: typedDefault, temporal: typedDefault,
|
|
310
|
+
};
|
|
265
311
|
/** The default in the stored shape for its kind; choices drop values no longer offered. */
|
|
266
|
-
function defaultValue(form, settings, { options, checkboxValue }) {
|
|
312
|
+
function defaultValue({ form, settings }, { options, checkboxValue }) {
|
|
313
|
+
if (settings.defaultKind === null)
|
|
314
|
+
return { ok: true, value: undefined };
|
|
267
315
|
const offered = new Set((options ?? []).map((option) => option.value));
|
|
268
|
-
|
|
269
|
-
// A box with an authored value submits that value, so its "checked" default is that value too.
|
|
270
|
-
case "boolean": return { ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined };
|
|
271
|
-
case "choice": return { ok: true, value: form.defaultChoices.find((value) => offered.has(value)) };
|
|
272
|
-
case "choices": {
|
|
273
|
-
const chosen = form.defaultChoices.filter((value) => offered.has(value));
|
|
274
|
-
return { ok: true, value: chosen.length ? chosen : undefined };
|
|
275
|
-
}
|
|
276
|
-
case "number": return optionalNumber(form.defaultText, "Default must be a number.");
|
|
277
|
-
case "text":
|
|
278
|
-
case "textarea":
|
|
279
|
-
case "temporal": return { ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText };
|
|
280
|
-
default: return { ok: true, value: undefined };
|
|
281
|
-
}
|
|
316
|
+
return DEFAULT_READERS[settings.defaultKind]({ form, offered, checkboxValue });
|
|
282
317
|
}
|
|
283
318
|
/** Upload limits: no boxes ticked means every safe type; size is whole megabytes up to the cap. */
|
|
284
|
-
function uploadPatch(form, settings) {
|
|
319
|
+
function uploadPatch({ form, settings }) {
|
|
285
320
|
if (!settings.upload)
|
|
286
321
|
return { ok: true, value: {} };
|
|
287
322
|
const size = optionalNumber(form.maxFileSizeMb, "Max size must be up to 10 MB.");
|
|
@@ -302,19 +337,19 @@ function uploadPatch(form, settings) {
|
|
|
302
337
|
*/
|
|
303
338
|
export function buildFieldSettingsPatch({ form, field }, _optional = {}) {
|
|
304
339
|
const settings = fieldSettingsFor({ type: field.type });
|
|
305
|
-
const textual = textPatch(form, settings);
|
|
340
|
+
const textual = textPatch({ form, settings });
|
|
306
341
|
if (!textual.ok)
|
|
307
342
|
return textual;
|
|
308
|
-
const bounds = boundsPatch(form, settings);
|
|
343
|
+
const bounds = boundsPatch({ form, settings });
|
|
309
344
|
if (!bounds.ok)
|
|
310
345
|
return bounds;
|
|
311
|
-
const options = optionsPatch(form, settings);
|
|
346
|
+
const options = optionsPatch({ form, settings });
|
|
312
347
|
if (!options.ok)
|
|
313
348
|
return options;
|
|
314
|
-
const fallback = defaultValue(form, settings, { options: options.value, checkboxValue: field.checkboxValue });
|
|
349
|
+
const fallback = defaultValue({ form, settings }, { options: options.value, checkboxValue: field.checkboxValue });
|
|
315
350
|
if (!fallback.ok)
|
|
316
351
|
return fallback;
|
|
317
|
-
const upload = uploadPatch(form, settings);
|
|
352
|
+
const upload = uploadPatch({ form, settings });
|
|
318
353
|
if (!upload.ok)
|
|
319
354
|
return upload;
|
|
320
355
|
return { ok: true, patch: {
|
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
|