@jini-ai/admin 0.4.5 → 0.4.6
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/dist/agent-plugins/package-file-tree.d.ts +3 -1
- package/dist/agent-plugins/package-file-tree.js +7 -7
- package/dist/comments/controllers/comment-queue.controller.d.ts +5 -1
- package/dist/comments/controllers/comment-queue.controller.js +2 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +14 -3
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -0
- package/dist/consent/react/ConsentSettingsPanel.js +24 -0
- package/dist/consent/react/index.d.ts +3 -0
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +19 -0
- package/dist/consent/react/settings-panel.hooks.js +30 -0
- package/dist/core/ports/forms.d.ts +23 -3
- package/dist/core/ports/forms.js +3 -3
- package/dist/core/ports/seo.d.ts +17 -0
- package/dist/forms/adapters/http.d.ts +5 -2
- package/dist/forms/adapters/http.js +7 -2
- package/dist/forms/controllers/field-attributes-dialog.controller.d.ts +59 -1
- package/dist/forms/controllers/field-attributes-dialog.controller.js +27 -2
- package/dist/forms/field-settings.d.ts +142 -0
- package/dist/forms/field-settings.js +327 -0
- package/dist/forms/form-editor-dirty.d.ts +42 -0
- package/dist/forms/form-editor-dirty.js +25 -0
- package/dist/forms/forms.module.d.ts +72 -0
- package/dist/forms/html-rules.d.ts +41 -3
- package/dist/forms/html-rules.js +118 -17
- package/dist/forms/messages.en.d.ts +72 -0
- package/dist/forms/messages.en.js +73 -0
- package/dist/forms/react/components/FieldAttributesDialog.js +20 -19
- package/dist/forms/react/components/FieldSettingsSection.d.ts +19 -0
- package/dist/forms/react/components/FieldSettingsSection.js +21 -0
- package/dist/forms/react/components/FormFieldsEditor.js +7 -8
- package/dist/forms/react/components/FormSubmissionDetail.js +5 -2
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +75 -5
- package/dist/forms/react/hooks/forms-view.hooks.js +46 -2
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.d.ts +10 -0
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +12 -2
- package/dist/forms/react/hooks/use-form-editor.hooks.d.ts +11 -0
- package/dist/forms/react/hooks/use-form-editor.hooks.js +25 -2
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.d.ts +2 -0
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.js +5 -1
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.d.ts +4 -0
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -0
- package/dist/forms/react/index.d.ts +216 -0
- package/dist/forms/react/pages/FormEditor.js +3 -2
- package/dist/forms/rules.d.ts +10 -1
- package/dist/forms/rules.js +8 -1
- package/dist/media/controllers/library.controller.d.ts +4 -0
- package/dist/media/controllers/library.controller.js +13 -0
- package/dist/media/react/components/MediaPicker.js +2 -2
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +4 -2
- package/dist/media/react/hooks/GallerySelection.hooks.d.ts +31 -0
- package/dist/media/react/hooks/GallerySelection.hooks.js +32 -0
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +4 -0
- package/dist/media/react/hooks/LibraryTab.hooks.js +9 -3
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +4 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +10 -13
- package/dist/media/react/hooks/MediaPicker.hooks.js +5 -1
- package/dist/media/react/hooks/MediaQuery.hooks.d.ts +27 -0
- package/dist/media/react/hooks/MediaQuery.hooks.js +41 -0
- package/dist/media/react/hooks/ProvidersTab.hooks.js +21 -2
- package/dist/media/react/index.d.ts +1 -0
- package/dist/media/react/index.js +1 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +5 -0
- package/dist/react/components/Sidebar.d.ts +6 -0
- package/dist/react/components/Sidebar.js +1 -0
- package/dist/react/entities/use-entity-edit.d.ts +1 -1
- package/dist/react/hooks/use-sidebar-rail.d.ts +10 -1
- package/dist/react/hooks/use-sidebar-rail.js +16 -3
- package/dist/seo/adapters/http.d.ts +11 -1
- package/dist/seo/adapters/http.js +11 -1
- package/dist/seo/controllers/entry-picker.controller.d.ts +2 -0
- package/dist/seo/controllers/entry-picker.controller.js +2 -0
- package/dist/seo/models.d.ts +8 -0
- package/dist/seo/react/components/MediaRefField.js +2 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +4 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +8 -3
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +42 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +65 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -9
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +9 -2
- package/dist/seo/react/hooks/use-seo.hooks.js +22 -4
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +19 -0
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +79 -0
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +5 -3
- package/dist/seo/react/pages/SeoPage.js +1 -1
- package/dist/seo/rules.d.ts +21 -2
- package/dist/seo/rules.js +44 -4
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.d.ts +37 -0
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.js +70 -0
- package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +5 -3
- package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +3 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +7 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +7 -3
- package/package.json +20 -14
|
@@ -3,13 +3,44 @@ export interface FormAnswerColumn {
|
|
|
3
3
|
key: string;
|
|
4
4
|
header: string;
|
|
5
5
|
cell: (submission: AdminFormSubmission) => string;
|
|
6
|
+
/** Formats one stored answer for this field; detail rows reuse it so they match the table. */
|
|
7
|
+
format?: (value: unknown) => string;
|
|
6
8
|
}
|
|
7
|
-
/**
|
|
9
|
+
/** Display options for answers: translated boolean labels and the date-formatting locale (default: runtime's). */
|
|
10
|
+
export type FormAnswerDisplay = {
|
|
11
|
+
yes?: string;
|
|
12
|
+
no?: string;
|
|
13
|
+
locale?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Persisted descriptors, including those derived from HTML, supply column headers, choice labels and field types. */
|
|
8
16
|
export declare function formAnswerColumns({ fields }: {
|
|
9
17
|
fields: readonly AdminFormField[];
|
|
10
|
-
},
|
|
18
|
+
}, display?: FormAnswerDisplay): FormAnswerColumn[];
|
|
19
|
+
/** The stored shape of a file-field answer (`FormFileReference` in `@jini-ai/cms/forms`). */
|
|
20
|
+
export interface FormAnswerFile {
|
|
21
|
+
mediaId: string;
|
|
22
|
+
filename: string;
|
|
23
|
+
byteSize?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Narrows a stored answer to an uploaded-file reference.
|
|
26
|
+
* @complexity O(1). */
|
|
27
|
+
export declare function formAnswerFile({ value }: {
|
|
28
|
+
value: unknown;
|
|
29
|
+
}, _optional?: {}): FormAnswerFile | null;
|
|
30
|
+
/** One answer as display text. Multi-choice answers are arrays and file answers are objects;
|
|
31
|
+
* `String()` turned those into `a,b` and `[object Object]` (2026-10-11).
|
|
32
|
+
* Options resolve stored choice values to visitor-facing labels; removed options retain raw values.
|
|
33
|
+
* Booleans read Yes/No and date/time/datetime answers read in the locale's short form instead of
|
|
34
|
+
* `true` and `2026-11-02T10:15` (2026-10-11); stored data is unchanged.
|
|
35
|
+
* @complexity O(n * (o + 1)) time for n answer items and o options; O(n) output space, no I/O. */
|
|
36
|
+
export declare function formatFormAnswer({ value, options, type }: {
|
|
37
|
+
value: unknown;
|
|
38
|
+
options?: AdminFormField["options"];
|
|
39
|
+
type?: string | undefined;
|
|
40
|
+
}, display?: FormAnswerDisplay): string;
|
|
11
41
|
/** Detail rows reuse the table's persisted field labels, including HTML-derived descriptors.
|
|
12
42
|
* Removed fields retain their answers with a plain fallback instead of exposing their IDs.
|
|
43
|
+
* A file answer also carries its reference so the view can link the download.
|
|
13
44
|
* @complexity O(c + a) time and space for c columns and a submitted answers; no I/O.
|
|
14
45
|
*/
|
|
15
46
|
export declare function formSubmissionRows({ data, answerColumns }: {
|
|
@@ -21,8 +52,15 @@ export declare function formSubmissionRows({ data, answerColumns }: {
|
|
|
21
52
|
key: string;
|
|
22
53
|
label: string;
|
|
23
54
|
value: string;
|
|
55
|
+
file: FormAnswerFile | null;
|
|
24
56
|
}>;
|
|
25
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Editable form BODY only: the server supplies the transport and spam-protection wrapper. Each
|
|
59
|
+
* field's markup follows its registry kind (`tel` for phone, a `<select>` with its options, radio
|
|
60
|
+
* and checkbox groups, `role="switch"` for a toggle), so switching to HTML mode keeps every field
|
|
61
|
+
* type; the CMS derives the same types back from this markup on save.
|
|
62
|
+
* @complexity O(f + o) for fields and their options; no I/O.
|
|
63
|
+
*/
|
|
26
64
|
export declare function formHtmlStarter({ fields }: {
|
|
27
65
|
fields: readonly AdminFormField[];
|
|
28
66
|
}, { submitLabel }?: {
|
package/dist/forms/html-rules.js
CHANGED
|
@@ -1,31 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
})
|
|
1
|
+
import { FIELD_TYPE_REGISTRY } from "@jini-ai/cms/forms";
|
|
2
|
+
/** Persisted descriptors, including those derived from HTML, supply column headers, choice labels and field types. */
|
|
3
|
+
export function formAnswerColumns({ fields }, display = {}) {
|
|
4
|
+
return fields.map((field) => {
|
|
5
|
+
const format = (value) => formatFormAnswer({ value, options: field.options, type: field.type }, display);
|
|
6
|
+
return { key: `answer:${field.id}`, header: field.label, cell: (submission) => format(submission.data[field.id]), format };
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** Narrows a stored answer to an uploaded-file reference.
|
|
10
|
+
* @complexity O(1). */
|
|
11
|
+
export function formAnswerFile({ value }, _optional = {}) {
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
13
|
+
return null;
|
|
14
|
+
const { mediaId, filename, byteSize } = value;
|
|
15
|
+
if (typeof mediaId !== "string" || typeof filename !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
return { mediaId, filename, ...(typeof byteSize === "number" ? { byteSize } : {}) };
|
|
18
|
+
}
|
|
19
|
+
/** One answer as display text. Multi-choice answers are arrays and file answers are objects;
|
|
20
|
+
* `String()` turned those into `a,b` and `[object Object]` (2026-10-11).
|
|
21
|
+
* Options resolve stored choice values to visitor-facing labels; removed options retain raw values.
|
|
22
|
+
* Booleans read Yes/No and date/time/datetime answers read in the locale's short form instead of
|
|
23
|
+
* `true` and `2026-11-02T10:15` (2026-10-11); stored data is unchanged.
|
|
24
|
+
* @complexity O(n * (o + 1)) time for n answer items and o options; O(n) output space, no I/O. */
|
|
25
|
+
export function formatFormAnswer({ value, options, type }, display = {}) {
|
|
26
|
+
if (value === undefined || value === null)
|
|
27
|
+
return "";
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map(item => formatFormAnswer({ value: item, options, type }, display)).join(", ");
|
|
30
|
+
if (typeof value === "boolean")
|
|
31
|
+
return value ? display.yes ?? "Yes" : display.no ?? "No";
|
|
32
|
+
if (typeof value === "string" && type !== undefined && type in TEMPORAL_STYLES)
|
|
33
|
+
return formatTemporal({ value, type }, display);
|
|
34
|
+
if (typeof value === "string")
|
|
35
|
+
return options?.find(option => option.value === value)?.label ?? value;
|
|
36
|
+
const file = formAnswerFile({ value });
|
|
37
|
+
if (file)
|
|
38
|
+
return file.byteSize === undefined ? file.filename : `${file.filename} (${formatByteSize({ bytes: file.byteSize })})`;
|
|
39
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
40
|
+
}
|
|
41
|
+
const TEMPORAL_STYLES = {
|
|
42
|
+
date: { dateStyle: "medium" },
|
|
43
|
+
time: { timeStyle: "short" },
|
|
44
|
+
datetime: { dateStyle: "medium", timeStyle: "short" },
|
|
45
|
+
};
|
|
46
|
+
const TEMPORAL_VALUE = /^(?:(\d{4})-(\d{2})-(\d{2}))?T?(?:(\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
47
|
+
/** Stored temporal answers are zone-less wall-clock values, so they format in UTC to show exactly
|
|
48
|
+
* what the visitor entered; unparseable values stay raw. */
|
|
49
|
+
function formatTemporal({ value, type }, { locale }) {
|
|
50
|
+
const match = TEMPORAL_VALUE.exec(value);
|
|
51
|
+
if (!match || (match[1] === undefined && match[4] === undefined))
|
|
52
|
+
return value;
|
|
53
|
+
const [, y = "1970", mo = "01", d = "01", h = "00", mi = "00", sec = "00"] = match;
|
|
54
|
+
const date = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(sec)));
|
|
55
|
+
return new Intl.DateTimeFormat(locale, { ...TEMPORAL_STYLES[type], timeZone: "UTC" }).format(date);
|
|
56
|
+
}
|
|
57
|
+
function formatByteSize({ bytes }) {
|
|
58
|
+
if (bytes < 1024)
|
|
59
|
+
return `${bytes} B`;
|
|
60
|
+
if (bytes < 1024 * 1024)
|
|
61
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
62
|
+
return `${Math.round((bytes / 1024 / 1024) * 10) / 10} MB`;
|
|
8
63
|
}
|
|
9
64
|
/** Detail rows reuse the table's persisted field labels, including HTML-derived descriptors.
|
|
10
65
|
* Removed fields retain their answers with a plain fallback instead of exposing their IDs.
|
|
66
|
+
* A file answer also carries its reference so the view can link the download.
|
|
11
67
|
* @complexity O(c + a) time and space for c columns and a submitted answers; no I/O.
|
|
12
68
|
*/
|
|
13
69
|
export function formSubmissionRows({ data, answerColumns }, { unknownFieldLabel = "Unknown field" } = {}) {
|
|
14
|
-
const
|
|
15
|
-
return Object.entries(data).map(([key, value]) =>
|
|
16
|
-
|
|
17
|
-
|
|
70
|
+
const columns = new Map(answerColumns.map(column => [column.key, column]));
|
|
71
|
+
return Object.entries(data).map(([key, value]) => {
|
|
72
|
+
const column = columns.get(`answer:${key}`);
|
|
73
|
+
const formatted = column?.format ? column.format(value) : formatFormAnswer({ value });
|
|
74
|
+
return { key, label: column?.header.trim() || unknownFieldLabel, value: formatted, file: formAnswerFile({ value }) };
|
|
75
|
+
});
|
|
18
76
|
}
|
|
19
77
|
function escapeHtml(value) {
|
|
20
78
|
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
21
79
|
}
|
|
22
|
-
|
|
80
|
+
const kindOf = (type) => Object.hasOwn(FIELD_TYPE_REGISTRY, type) ? FIELD_TYPE_REGISTRY[type] : { control: "input", htmlType: "text" };
|
|
81
|
+
const isDefault = (field, value) => Array.isArray(field.default) ? field.default.includes(value) : field.default === value;
|
|
82
|
+
/** Name, required and the stored constraints, as authored HTML attributes the CMS reads back. */
|
|
83
|
+
function starterAttributes(field, kind) {
|
|
84
|
+
const constraints = [
|
|
85
|
+
["placeholder", kind.control === "select" ? undefined : field.placeholder], ["minlength", field.minLength], ["maxlength", field.maxLength],
|
|
86
|
+
["pattern", field.pattern], ["min", field.min], ["max", field.max], ["step", field.step], ["accept", field.accept?.join(",")],
|
|
87
|
+
];
|
|
88
|
+
return `name="${escapeHtml(field.id)}"${field.required ? " required" : ""}` +
|
|
89
|
+
constraints.filter(([, value]) => value != null && value !== "").map(([name, value]) => ` ${name}="${escapeHtml(String(value))}"`).join("");
|
|
90
|
+
}
|
|
91
|
+
/** One field's starter markup; every control comes from the registry kind, as the public render does. */
|
|
92
|
+
function starterField(field) {
|
|
93
|
+
const kind = kindOf(field.type);
|
|
94
|
+
const label = escapeHtml(field.label);
|
|
95
|
+
const attrs = starterAttributes(field, kind);
|
|
96
|
+
const options = field.options ?? [];
|
|
97
|
+
if (kind.control === "group") {
|
|
98
|
+
// One box per option under a shared name; the CMS collects them back into this choice field.
|
|
99
|
+
// Native `required` on every checkbox would demand ALL of them, so a checkbox group leaves it
|
|
100
|
+
// off (the server still enforces one-or-more); a radio group's `required` means "pick one".
|
|
101
|
+
const boxAttrs = kind.multiple ? starterAttributes({ ...field, required: false }, kind) : attrs;
|
|
102
|
+
const boxes = options.map((option) => ` <label><input type="${kind.htmlType}" ${boxAttrs} value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " checked" : ""}> ${escapeHtml(option.label)}</label>`);
|
|
103
|
+
return `<fieldset>\n <legend>${label}</legend>\n${boxes.join("\n")}\n</fieldset>`;
|
|
104
|
+
}
|
|
105
|
+
if (kind.control === "select") {
|
|
106
|
+
const empty = kind.multiple ? [] : [` <option value="">${escapeHtml(field.placeholder ?? "Choose an option")}</option>`];
|
|
107
|
+
const choices = options.map((option) => ` <option value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " selected" : ""}>${escapeHtml(option.label)}</option>`);
|
|
108
|
+
return `<label>${label}\n <select ${attrs}${kind.multiple ? " multiple" : ""}>\n${[...empty, ...choices].join("\n")}\n </select>\n</label>`;
|
|
109
|
+
}
|
|
110
|
+
if (kind.control === "textarea")
|
|
111
|
+
return `<label>${label}\n <textarea ${attrs}></textarea>\n</label>`;
|
|
112
|
+
if (kind.htmlType === "hidden")
|
|
113
|
+
return `<input type="hidden" ${attrs} value="${escapeHtml(String(field.default ?? ""))}" aria-label="${label}">`;
|
|
114
|
+
if (kind.htmlType === "checkbox") {
|
|
115
|
+
const value = field.checkboxValue !== undefined ? ` value="${escapeHtml(field.checkboxValue)}"` : "";
|
|
116
|
+
const checked = field.default === true || (field.checkboxValue !== undefined && field.default === field.checkboxValue) ? " checked" : "";
|
|
117
|
+
return `<label><input type="checkbox"${field.type === "toggle" ? ' role="switch"' : ""} ${attrs}${value}${checked}> ${label}</label>`;
|
|
118
|
+
}
|
|
119
|
+
return `<label>${label}\n <input type="${kind.htmlType}" ${attrs}>\n</label>`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Editable form BODY only: the server supplies the transport and spam-protection wrapper. Each
|
|
123
|
+
* field's markup follows its registry kind (`tel` for phone, a `<select>` with its options, radio
|
|
124
|
+
* and checkbox groups, `role="switch"` for a toggle), so switching to HTML mode keeps every field
|
|
125
|
+
* type; the CMS derives the same types back from this markup on save.
|
|
126
|
+
* @complexity O(f + o) for fields and their options; no I/O.
|
|
127
|
+
*/
|
|
23
128
|
export function formHtmlStarter({ fields }, { submitLabel = "Send" } = {}) {
|
|
24
|
-
return fields.map((
|
|
25
|
-
const attrs = `name="${escapeHtml(field.id)}"${field.required ? " required" : ""}${field.maxLength != null ? ` maxlength="${field.maxLength}"` : ""}`;
|
|
26
|
-
const control = field.type === "textarea" ? `<textarea ${attrs}></textarea>` : `<input type="${field.type}" ${attrs}>`;
|
|
27
|
-
return `<label>${escapeHtml(field.label)}\n ${control}\n</label>`;
|
|
28
|
-
}).join("\n\n") + `\n\n<button type="submit">${escapeHtml(submitLabel)}</button>`;
|
|
129
|
+
return fields.map(starterField).join("\n\n") + `\n\n<button type="submit">${escapeHtml(submitLabel)}</button>`;
|
|
29
130
|
}
|
|
30
131
|
export function formHtmlEmbed({ slug }, _optional = {}) {
|
|
31
132
|
const config = JSON.stringify({ type: "form", id: slug, mode: "html" }).replace(/'/g, "'");
|
|
@@ -59,6 +59,8 @@ export declare const formsMessagesEn: {
|
|
|
59
59
|
readonly "Load more": "Load more";
|
|
60
60
|
readonly "Loading\u2026": "Loading…";
|
|
61
61
|
readonly "No submissions yet.": "No submissions yet.";
|
|
62
|
+
readonly Yes: "Yes";
|
|
63
|
+
readonly No: "No";
|
|
62
64
|
readonly "Create form": "Create form";
|
|
63
65
|
readonly Form: "Form";
|
|
64
66
|
readonly "Configure a new form's fields.": "Configure a new form's fields.";
|
|
@@ -95,6 +97,76 @@ export declare const formsMessagesEn: {
|
|
|
95
97
|
readonly "failed to load submission": "failed to load submission";
|
|
96
98
|
readonly "delete failed": "delete failed";
|
|
97
99
|
readonly "Unknown field": "Unknown field";
|
|
100
|
+
readonly "Short text": "Short text";
|
|
101
|
+
readonly Paragraph: "Paragraph";
|
|
102
|
+
readonly Email: "Email";
|
|
103
|
+
readonly Phone: "Phone";
|
|
104
|
+
readonly "Website address": "Website address";
|
|
105
|
+
readonly Number: "Number";
|
|
106
|
+
readonly Slider: "Slider";
|
|
107
|
+
readonly Date: "Date";
|
|
108
|
+
readonly Time: "Time";
|
|
109
|
+
readonly "Date and time": "Date and time";
|
|
110
|
+
readonly Dropdown: "Dropdown";
|
|
111
|
+
readonly "Multi-select list": "Multi-select list";
|
|
112
|
+
readonly "Radio buttons": "Radio buttons";
|
|
113
|
+
readonly Checkbox: "Checkbox";
|
|
114
|
+
readonly Checkboxes: "Checkboxes";
|
|
115
|
+
readonly "Toggle switch": "Toggle switch";
|
|
116
|
+
readonly "File upload": "File upload";
|
|
117
|
+
readonly "Hidden value": "Hidden value";
|
|
118
|
+
readonly Text: "Text";
|
|
119
|
+
readonly Choices: "Choices";
|
|
120
|
+
readonly "Numbers and dates": "Numbers and dates";
|
|
121
|
+
readonly Other: "Other";
|
|
122
|
+
readonly "Field settings \u2014": "Field settings —";
|
|
123
|
+
readonly "Settings for field \"{name}\"": "Settings for field \"{name}\"";
|
|
124
|
+
readonly "CSS classes and HTML attributes": "CSS classes and HTML attributes";
|
|
125
|
+
readonly Required: "Required";
|
|
126
|
+
readonly Settings: "Settings";
|
|
127
|
+
readonly Placeholder: "Placeholder";
|
|
128
|
+
readonly "Help text": "Help text";
|
|
129
|
+
readonly "Shown under the field and read out by screen readers.": "Shown under the field and read out by screen readers.";
|
|
130
|
+
readonly Options: "Options";
|
|
131
|
+
readonly "The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.": "The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.";
|
|
132
|
+
readonly "Option {n} label": "Option {n} label";
|
|
133
|
+
readonly "Option {n} value": "Option {n} value";
|
|
134
|
+
readonly "Option {n} selected by default": "Option {n} selected by default";
|
|
135
|
+
readonly "Move option {n} up": "Move option {n} up";
|
|
136
|
+
readonly "Move option {n} down": "Move option {n} down";
|
|
137
|
+
readonly "Remove option {n}": "Remove option {n}";
|
|
138
|
+
readonly "Add option": "Add option";
|
|
139
|
+
readonly Default: "Default";
|
|
140
|
+
readonly "Checked by default": "Checked by default";
|
|
141
|
+
readonly None: "None";
|
|
142
|
+
readonly Length: "Length";
|
|
143
|
+
readonly "Min length": "Min length";
|
|
144
|
+
readonly Min: "Min";
|
|
145
|
+
readonly Max: "Max";
|
|
146
|
+
readonly Pattern: "Pattern";
|
|
147
|
+
readonly "Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.": "Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.";
|
|
148
|
+
readonly Range: "Range";
|
|
149
|
+
readonly Step: "Step";
|
|
150
|
+
readonly Files: "Files";
|
|
151
|
+
readonly "Leave all unticked to accept every safe type: images, video and PDF.": "Leave all unticked to accept every safe type: images, video and PDF.";
|
|
152
|
+
readonly Images: "Images";
|
|
153
|
+
readonly Videos: "Videos";
|
|
154
|
+
readonly PDF: "PDF";
|
|
155
|
+
readonly "Max size (MB)": "Max size (MB)";
|
|
156
|
+
readonly "Length must be a whole number.": "Length must be a whole number.";
|
|
157
|
+
readonly "Minimum must not be above maximum.": "Minimum must not be above maximum.";
|
|
158
|
+
readonly "That pattern isn't supported.": "That pattern isn't supported.";
|
|
159
|
+
readonly "Enter numbers for min, max and step.": "Enter numbers for min, max and step.";
|
|
160
|
+
readonly "Step must be above 0.": "Step must be above 0.";
|
|
161
|
+
readonly "Add at least one option.": "Add at least one option.";
|
|
162
|
+
readonly "Each option needs a label and a unique value.": "Each option needs a label and a unique value.";
|
|
163
|
+
readonly "Default must be a number.": "Default must be a number.";
|
|
164
|
+
readonly "Max size must be up to 10 MB.": "Max size must be up to 10 MB.";
|
|
165
|
+
readonly "Discard unsaved changes?": "Discard unsaved changes?";
|
|
166
|
+
readonly "Your changes to this form have not been saved.": "Your changes to this form have not been saved.";
|
|
167
|
+
readonly "Discard changes": "Discard changes";
|
|
168
|
+
readonly "Keep editing": "Keep editing";
|
|
169
|
+
readonly "Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.": "Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.";
|
|
98
170
|
};
|
|
99
171
|
/** Host translators may replace any key; the default preserves the original English bytes. */
|
|
100
172
|
export declare const translateForms: FormsTranslator;
|
|
@@ -58,6 +58,8 @@ export const formsMessagesEn = {
|
|
|
58
58
|
"Load more": "Load more",
|
|
59
59
|
"Loading…": "Loading…",
|
|
60
60
|
"No submissions yet.": "No submissions yet.",
|
|
61
|
+
"Yes": "Yes",
|
|
62
|
+
"No": "No",
|
|
61
63
|
"Create form": "Create form",
|
|
62
64
|
"Form": "Form",
|
|
63
65
|
"Configure a new form's fields.": "Configure a new form's fields.",
|
|
@@ -94,6 +96,77 @@ export const formsMessagesEn = {
|
|
|
94
96
|
"failed to load submission": "failed to load submission",
|
|
95
97
|
"delete failed": "delete failed",
|
|
96
98
|
"Unknown field": "Unknown field",
|
|
99
|
+
// Field builder settings for the registry field types (2026-10-11).
|
|
100
|
+
"Short text": "Short text",
|
|
101
|
+
"Paragraph": "Paragraph",
|
|
102
|
+
"Email": "Email",
|
|
103
|
+
"Phone": "Phone",
|
|
104
|
+
"Website address": "Website address",
|
|
105
|
+
"Number": "Number",
|
|
106
|
+
"Slider": "Slider",
|
|
107
|
+
"Date": "Date",
|
|
108
|
+
"Time": "Time",
|
|
109
|
+
"Date and time": "Date and time",
|
|
110
|
+
"Dropdown": "Dropdown",
|
|
111
|
+
"Multi-select list": "Multi-select list",
|
|
112
|
+
"Radio buttons": "Radio buttons",
|
|
113
|
+
"Checkbox": "Checkbox",
|
|
114
|
+
"Checkboxes": "Checkboxes",
|
|
115
|
+
"Toggle switch": "Toggle switch",
|
|
116
|
+
"File upload": "File upload",
|
|
117
|
+
"Hidden value": "Hidden value",
|
|
118
|
+
"Text": "Text",
|
|
119
|
+
"Choices": "Choices",
|
|
120
|
+
"Numbers and dates": "Numbers and dates",
|
|
121
|
+
"Other": "Other",
|
|
122
|
+
"Field settings —": "Field settings —",
|
|
123
|
+
"Settings for field \"{name}\"": "Settings for field \"{name}\"",
|
|
124
|
+
"CSS classes and HTML attributes": "CSS classes and HTML attributes",
|
|
125
|
+
"Required": "Required",
|
|
126
|
+
"Settings": "Settings",
|
|
127
|
+
"Placeholder": "Placeholder",
|
|
128
|
+
"Help text": "Help text",
|
|
129
|
+
"Shown under the field and read out by screen readers.": "Shown under the field and read out by screen readers.",
|
|
130
|
+
"Options": "Options",
|
|
131
|
+
"The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.": "The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.",
|
|
132
|
+
"Option {n} label": "Option {n} label",
|
|
133
|
+
"Option {n} value": "Option {n} value",
|
|
134
|
+
"Option {n} selected by default": "Option {n} selected by default",
|
|
135
|
+
"Move option {n} up": "Move option {n} up",
|
|
136
|
+
"Move option {n} down": "Move option {n} down",
|
|
137
|
+
"Remove option {n}": "Remove option {n}",
|
|
138
|
+
"Add option": "Add option",
|
|
139
|
+
"Default": "Default",
|
|
140
|
+
"Checked by default": "Checked by default",
|
|
141
|
+
"None": "None",
|
|
142
|
+
"Length": "Length",
|
|
143
|
+
"Min length": "Min length",
|
|
144
|
+
"Min": "Min",
|
|
145
|
+
"Max": "Max",
|
|
146
|
+
"Pattern": "Pattern",
|
|
147
|
+
"Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.": "Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.",
|
|
148
|
+
"Range": "Range",
|
|
149
|
+
"Step": "Step",
|
|
150
|
+
"Files": "Files",
|
|
151
|
+
"Leave all unticked to accept every safe type: images, video and PDF.": "Leave all unticked to accept every safe type: images, video and PDF.",
|
|
152
|
+
"Images": "Images",
|
|
153
|
+
"Videos": "Videos",
|
|
154
|
+
"PDF": "PDF",
|
|
155
|
+
"Max size (MB)": "Max size (MB)",
|
|
156
|
+
"Length must be a whole number.": "Length must be a whole number.",
|
|
157
|
+
"Minimum must not be above maximum.": "Minimum must not be above maximum.",
|
|
158
|
+
"That pattern isn't supported.": "That pattern isn't supported.",
|
|
159
|
+
"Enter numbers for min, max and step.": "Enter numbers for min, max and step.",
|
|
160
|
+
"Step must be above 0.": "Step must be above 0.",
|
|
161
|
+
"Add at least one option.": "Add at least one option.",
|
|
162
|
+
"Each option needs a label and a unique value.": "Each option needs a label and a unique value.",
|
|
163
|
+
"Default must be a number.": "Default must be a number.",
|
|
164
|
+
"Max size must be up to 10 MB.": "Max size must be up to 10 MB.",
|
|
165
|
+
"Discard unsaved changes?": "Discard unsaved changes?",
|
|
166
|
+
"Your changes to this form have not been saved.": "Your changes to this form have not been saved.",
|
|
167
|
+
"Discard changes": "Discard changes",
|
|
168
|
+
"Keep editing": "Keep editing",
|
|
169
|
+
"Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.": "Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.",
|
|
97
170
|
};
|
|
98
171
|
/** Host translators may replace any key; the default preserves the original English bytes. */
|
|
99
172
|
export const translateForms = (key, vars = {}) => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFieldAttributesView } from "../hooks/forms-view.hooks.js";
|
|
3
3
|
import { Dialog } from '@jini-ai/ui-kit/react';
|
|
4
|
-
import {
|
|
4
|
+
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
5
5
|
import { agentHandle } from '@jini-ai/agentic';
|
|
6
6
|
import { ATTRIBUTE_NAME_SUGGESTIONS } from '../../rules.js';
|
|
7
7
|
import { useFieldAttributesDialog } from '../hooks/use-field-attributes-dialog.hooks.js';
|
|
8
|
+
import { FieldSettingsSection } from './FieldSettingsSection.js';
|
|
8
9
|
export function FieldAttributesDialog({ field, fieldIndex, onSave, onCancel, useFieldAttributesDialogHook = useFieldAttributesDialog, t, }, _optional = {}) {
|
|
9
10
|
const controller = useFieldAttributesDialogHook({
|
|
10
11
|
field,
|
|
@@ -17,26 +18,26 @@ export function FieldAttributesDialog({ field, fieldIndex, onSave, onCancel, use
|
|
|
17
18
|
// one field's own CSS-class/HTML-attribute rows (a handful at most), cheap enough that memoizing
|
|
18
19
|
// it was not judged worth the added indirection.
|
|
19
20
|
const { className, addRow, error, submit } = controller;
|
|
20
|
-
const { rows, displayName, onClose, onClassName } = useFieldAttributesView({ field, fieldIndex, controller, onCancel });
|
|
21
|
-
return (_jsx(Dialog, { open: true, title: `${t("Field
|
|
21
|
+
const { rows, displayName, onClose, onClassName, hasStyling } = useFieldAttributesView({ field, fieldIndex, controller, onCancel });
|
|
22
|
+
return (_jsx(Dialog, { open: true, title: `${t("Field settings —")} ${displayName}`, onClose: onClose, className: "settings-dialog tovu-domain-dialog field-attrs-dialog", children: _jsxs("form", { onSubmit: submit, ...agentHandle({ handle: "form-field-attrs-dialog" }, {
|
|
22
23
|
role: "region",
|
|
23
|
-
label: "Field
|
|
24
|
-
}), children: [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
label: "Field settings dialog — type-specific settings, CSS classes and HTML attributes for one form field",
|
|
25
|
+
}), children: [_jsx(FieldSettingsSection, { field: field, controller: controller, t: t }), _jsxs("details", { className: "field-settings-advanced", open: hasStyling, children: [_jsx("summary", { children: t("CSS classes and HTML attributes") }), _jsxs("div", { className: "field", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-attrs-classname", children: t("CSS classes") }), _jsx(InfoTip, { label: t("Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.") })] }), _jsx("input", { id: "field-attrs-classname", value: className, placeholder: "e.g. md:col-span-2 w-1/2 focus:ring-2", onChange: onClassName, ...agentHandle({ handle: "form-field-attrs-classname" }, {
|
|
26
|
+
role: "field",
|
|
27
|
+
label: "CSS classes to add to this form field's input",
|
|
28
|
+
}) })] }), _jsxs("div", { className: "field-attrs-rows", children: [_jsx("span", { className: "field-label", children: t("HTML attributes") }), _jsx("datalist", { id: "field-attrs-name-suggestions", children: ATTRIBUTE_NAME_SUGGESTIONS.map((name) => (_jsx("option", { value: name }, name))) }), rows.map((row, index) => (_jsxs("fieldset", { className: "collections-field-row", children: [_jsxs("legend", { children: [t("Attribute"), " ", index + 1] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: `field-attrs-name-${row._rowId}`, children: t("Name") }), _jsx("input", { id: `field-attrs-name-${row._rowId}`, list: "field-attrs-name-suggestions", value: row.name, placeholder: "e.g. aria-label", onChange: row.onName, ...agentHandle({ handle: `${row.handle}-name` }, {
|
|
29
|
+
role: "field",
|
|
30
|
+
label: "This attribute's name — must be on the allowlisted set (aria-*, data-*, and a few others)",
|
|
31
|
+
}) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: `field-attrs-value-${row._rowId}`, children: t("Value") }), _jsx("input", { id: `field-attrs-value-${row._rowId}`, value: row.value, placeholder: t("e.g. Enter your work email"), onChange: row.onValue, ...agentHandle({ handle: `${row.handle}-value` }, {
|
|
32
|
+
role: "field",
|
|
33
|
+
label: "This attribute's value",
|
|
34
|
+
}) })] }), _jsx("button", { type: "button", className: "btn-secondary", onClick: row.onRemove, ...agentHandle({ handle: `${row.handle}-remove` }, {
|
|
35
|
+
role: "button",
|
|
36
|
+
label: "Remove this CSS-class/HTML-attribute row",
|
|
37
|
+
}), children: t("Remove") })] }, row._rowId))), _jsx("button", { type: "button", className: "btn-secondary", onClick: addRow, ...agentHandle({ handle: "form-field-attrs-add-row" }, {
|
|
34
38
|
role: "button",
|
|
35
|
-
label: "
|
|
36
|
-
}), children: t("
|
|
37
|
-
role: "button",
|
|
38
|
-
label: "Add another HTML attribute row to this field",
|
|
39
|
-
}), children: t("Add attribute") })] }), error ? (_jsx("span", { className: "save-error", role: "alert", children: error })) : null, _jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "submit", ...agentHandle({ handle: "form-field-attrs-save" }, {
|
|
39
|
+
label: "Add another HTML attribute row to this field",
|
|
40
|
+
}), children: t("Add attribute") })] })] }), error ? (_jsx("span", { className: "save-error", role: "alert", children: t(error) })) : null, _jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "submit", ...agentHandle({ handle: "form-field-attrs-save" }, {
|
|
40
41
|
role: "button",
|
|
41
42
|
label: "Save this field's CSS classes and HTML attributes",
|
|
42
43
|
}), children: t("Save") }), _jsx("button", { type: "button", className: "btn-secondary", onClick: onCancel, ...agentHandle({ handle: "form-field-attrs-cancel" }, {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AdminFormField } from '../../models.js';
|
|
2
|
+
import type { FieldAttributesDialogController } from '../hooks/use-field-attributes-dialog.hooks.js';
|
|
3
|
+
export interface FieldSettingsSectionProps {
|
|
4
|
+
field: AdminFormField;
|
|
5
|
+
controller: FieldAttributesDialogController;
|
|
6
|
+
t: (key: string) => string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The settings half of the field dialog: one label-beside-control row per setting the field's
|
|
10
|
+
* registry kind supports (`field-settings.ts` decides which). Markup only; every handler comes from
|
|
11
|
+
* `useFieldSettingsView`. Explanations are ⓘ tips so the rows stay one line each.
|
|
12
|
+
*
|
|
13
|
+
* Initial focus: the native dialog focuses the first `[data-jini-autofocus]`, so the first of the
|
|
14
|
+
* placeholder, help-text and default inputs this type renders gets it. Without it, types with no
|
|
15
|
+
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
16
|
+
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
17
|
+
*/
|
|
18
|
+
export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=FieldSettingsSection.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
6
|
+
/**
|
|
7
|
+
* The settings half of the field dialog: one label-beside-control row per setting the field's
|
|
8
|
+
* registry kind supports (`field-settings.ts` decides which). Markup only; every handler comes from
|
|
9
|
+
* `useFieldSettingsView`. Explanations are ⓘ tips so the rows stay one line each.
|
|
10
|
+
*
|
|
11
|
+
* Initial focus: the native dialog focuses the first `[data-jini-autofocus]`, so the first of the
|
|
12
|
+
* placeholder, help-text and default inputs this type renders gets it. Without it, types with no
|
|
13
|
+
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
14
|
+
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
15
|
+
*/
|
|
16
|
+
export function FieldSettingsSection({ field, controller, t }, _optional = {}) {
|
|
17
|
+
const view = useFieldSettingsView({ field, controller, t });
|
|
18
|
+
const { settings, form } = view;
|
|
19
|
+
return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), 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, settings.options ? (_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') })] })] })) : null, 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, 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, settings.upload ? (_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' }) })] })] })) : null] }));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=FieldSettingsSection.js.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useFormFieldsView } from "../hooks/forms-view.hooks.js";
|
|
3
3
|
import { agentHandle } from '@jini-ai/agentic';
|
|
4
|
-
import { FIELD_TYPES } from '../../rules.js';
|
|
5
4
|
import { useFormFieldsEditor } from '../hooks/use-form-fields-editor.hooks.js';
|
|
6
5
|
import { FieldAttributesDialog } from "./FieldAttributesDialog.js";
|
|
7
6
|
export function FormFieldsEditor({ fields, existingFieldIds = [], onChange, useFormFieldsEditorHook = useFormFieldsEditor, t, }, _optional = {}) {
|
|
@@ -16,22 +15,22 @@ export function FormFieldsEditor({ fields, existingFieldIds = [], onChange, useF
|
|
|
16
15
|
return (_jsxs(_Fragment, { children: [_jsxs("table", { className: "list-table form-fields-table", ...agentHandle({ handle: "form-fields-table" }, {
|
|
17
16
|
role: "region",
|
|
18
17
|
label: "This form's field definitions — one row per field, in the order they render on the live form",
|
|
19
|
-
}), children: [_jsxs("colgroup", { children: [_jsx("col", { style: { width: "14%" } }), _jsx("col", { style: { width: "17%" } }), _jsx("col", { style: { width: "
|
|
18
|
+
}), children: [_jsxs("colgroup", { children: [_jsx("col", { style: { width: "14%" } }), _jsx("col", { style: { width: "17%" } }), _jsx("col", { style: { width: "21%" } }), _jsx("col", { style: { width: "8%" } }), _jsx("col", { style: { width: "14%" } }), _jsx("col", { style: { width: "8%" } }), _jsx("col", { style: { width: "18%" } })] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t("ID") }), _jsx("th", { children: t("Label") }), _jsx("th", { children: t("Type") }), _jsx("th", { children: t("Req") }), _jsx("th", { children: t("Max length") }), _jsx("th", { children: t("More") }), _jsx("th", {})] }) }), _jsx("tbody", { children: rows.map(({ field, index, isExisting, base, displayName, typeGroups, hasLength, onId, onLabel, onType, onRequired, onMaxLength, ref, onAttributes, onRemove }) => (_jsxs("tr", { children: [_jsx("td", { "data-label": t("ID"), children: _jsx("input", { "aria-label": `Field ${index + 1} id`, value: field.id, disabled: isExisting, onChange: onId, ...agentHandle({ handle: `${base}-id` }, {
|
|
20
19
|
role: "field",
|
|
21
20
|
label: "This field's unique id — the key its value is submitted under. Locked once saved.",
|
|
22
|
-
}) }) }), _jsx("td", { children: _jsx("input", { "aria-label": `Field ${index + 1} label`, value: field.label, onChange: onLabel, ...agentHandle({ handle: `${base}-label` }, { role: "field", label: "This field's on-page label" }) }) }), _jsx("td", { children: _jsx("select", { "aria-label": `Field ${index + 1} type`, value: field.type, onChange: onType, ...agentHandle({ handle: `${base}-type` }, {
|
|
21
|
+
}) }) }), _jsx("td", { "data-label": t("Label"), children: _jsx("input", { "aria-label": `Field ${index + 1} label`, value: field.label, onChange: onLabel, ...agentHandle({ handle: `${base}-label` }, { role: "field", label: "This field's on-page label" }) }) }), _jsx("td", { "data-label": t("Type"), children: _jsx("select", { "aria-label": `Field ${index + 1} type`, value: field.type, onChange: onType, ...agentHandle({ handle: `${base}-type` }, {
|
|
23
22
|
role: "field",
|
|
24
23
|
label: "This field's input type — set with page.select_option, not click",
|
|
25
|
-
}), children:
|
|
24
|
+
}), children: typeGroups.map(({ group, types }) => (_jsx("optgroup", { label: t(group), children: types.map((type) => (_jsx("option", { value: type.type, children: t(type.label) }, type.type))) }, group))) }) }), _jsx("td", { "data-label": t("Required"), children: _jsx("input", { type: "checkbox", "aria-label": `Field ${index + 1} required`, checked: field.required, onChange: onRequired, ...agentHandle({ handle: `${base}-required` }, {
|
|
26
25
|
role: "checkbox",
|
|
27
26
|
label: "Whether this field must be filled in before the form can be submitted",
|
|
28
|
-
}) }) }), _jsx("td", {
|
|
27
|
+
}) }) }), _jsx("td", { "data-label": t("Max length"), children: !hasLength ? (_jsx("span", { "aria-hidden": "true", children: "\u2014" })) : (_jsx("input", { type: "number", "aria-label": `Field ${index + 1} max length`, value: field.maxLength ?? "", onChange: onMaxLength, ...agentHandle({ handle: `${base}-max-length` }, {
|
|
29
28
|
role: "field",
|
|
30
29
|
label: "The maximum number of characters this field accepts — blank means no limit",
|
|
31
|
-
}) })) }), _jsx("td", { children: _jsx("button", { ref: ref, type: "button", className: "row-menu-trigger", "aria-label":
|
|
30
|
+
}) })) }), _jsx("td", { "data-label": t("Settings"), children: _jsx("button", { ref: ref, type: "button", className: "row-menu-trigger", "aria-label": t('Settings for field "{name}"').replace("{name}", displayName), onClick: onAttributes, ...agentHandle({ handle: `${base}-attributes` }, {
|
|
32
31
|
role: "button",
|
|
33
|
-
label: "Open this field's CSS classes and HTML attributes
|
|
34
|
-
}), children: _jsxs("svg", { viewBox: "0 0 18 18", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "9", cy: "4.5", r: "1.5" }), _jsx("circle", { cx: "9", cy: "9", r: "1.5" }), _jsx("circle", { cx: "9", cy: "13.5", r: "1.5" })] }) }) }), _jsx("td", { children: _jsx("button", { type: "button", disabled: isExisting, title: isExisting ? t("Existing fields cannot be removed once created") : undefined, onClick: onRemove, ...agentHandle({ handle: `${base}-remove` }, {
|
|
32
|
+
label: "Open this field's settings dialog: placeholder, help text, default, options, limits, CSS classes and HTML attributes",
|
|
33
|
+
}), children: _jsxs("svg", { viewBox: "0 0 18 18", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "9", cy: "4.5", r: "1.5" }), _jsx("circle", { cx: "9", cy: "9", r: "1.5" }), _jsx("circle", { cx: "9", cy: "13.5", r: "1.5" })] }) }) }), _jsx("td", { className: "form-field-remove-cell", children: _jsx("button", { type: "button", disabled: isExisting, title: isExisting ? t("Existing fields cannot be removed once created") : undefined, onClick: onRemove, ...agentHandle({ handle: `${base}-remove` }, {
|
|
35
34
|
role: "button",
|
|
36
35
|
label: "Remove this field from the form. Refused once the field has been saved.",
|
|
37
36
|
}), children: t("Remove") }) })] }, index))) }), _jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { colSpan: 7, children: _jsx("button", { type: "button", className: "btn-secondary", onClick: addField, ...agentHandle({ handle: "form-fields-add" }, { role: "button", label: "Add a new field to this form" }), children: t("Add field") }) }) }) })] }), editingAttrsIndex !== null && editingField ? (_jsx(FieldAttributesDialog, { field: editingField, fieldIndex: editingAttrsIndex, onSave: saveAttributes, onCancel: closeAttrsDialog, t: t })) : null] }));
|