@form-engine-ts/react 2.0.0 → 2.1.1

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/index.js CHANGED
@@ -1,31 +1,36 @@
1
1
  // src/builder.tsx
2
2
  import {
3
- populateSchemaTranslations,
4
- sanitizeSchema
3
+ populateSchemaTranslations
5
4
  } from "@form-engine-ts/core";
6
5
  import { useState } from "react";
7
6
 
8
7
  // src/hooks/useFormBuilder.ts
9
8
  import {
9
+ transformFieldType,
10
10
  validateFormSchema
11
11
  } from "@form-engine-ts/core";
12
12
  import { useCallback, useMemo } from "react";
13
13
  var DEFAULT_PREFIXES = { field: "q", option: "opt", page: "page" };
14
+ var CHOICE_TYPES = ["select", "radio", "multi-select"];
14
15
  function defaultIdFactory(kind, existingIds) {
15
16
  const prefix = DEFAULT_PREFIXES[kind];
16
17
  let id;
17
- do {
18
+ do
18
19
  id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
19
- } while (existingIds.has(id));
20
+ while (existingIds.has(id));
20
21
  return id;
21
22
  }
22
- function newField(type, id, optionId) {
23
+ function defaultCreateField(type, id) {
23
24
  const base = { id, title: "New question", required: false };
24
- if (type === "text" || type === "textarea") return { ...base, type };
25
- if (type === "number") return { ...base, type };
25
+ if (type === "text" || type === "textarea" || type === "number" || type === "checkbox") return { ...base, type };
26
26
  if (type === "rating") return { ...base, type, min: 1, max: 5 };
27
- if (type === "checkbox") return { ...base, type };
28
- return optionId === void 0 ? void 0 : { ...base, type, options: [{ id: optionId, label: "Option 1" }] };
27
+ return { ...base, type, options: [] };
28
+ }
29
+ function defaultCreateOption(field, id) {
30
+ return { id, label: `Option ${"options" in field ? field.options.length + 1 : 1}` };
31
+ }
32
+ function defaultCreatePage(id, questionIds) {
33
+ return { id, title: "New page", questionIds };
29
34
  }
30
35
  function move(items, sourceIndex, targetIndex) {
31
36
  if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
@@ -36,334 +41,553 @@ function move(items, sourceIndex, targetIndex) {
36
41
  result.splice(targetIndex, 0, item);
37
42
  return result;
38
43
  }
39
- function addPolicyIssues(schema, policy, issues) {
40
- if (policy?.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
41
- issues.push({
42
- path: "fields",
43
- code: "max_fields_exceeded",
44
- message: `At most ${policy.maxFields} fields are allowed.`
45
- });
46
- }
47
- schema.fields.forEach((field, index) => {
48
- if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
49
- issues.push({
50
- path: `fields[${index}].type`,
51
- code: "disallowed_field_type",
52
- message: `Field type ${field.type} is not allowed.`
53
- });
54
- }
55
- if (policy?.maxTextLength !== void 0) {
56
- for (const [property, text] of [
57
- ["title", field.title],
58
- ["description", field.description]
59
- ]) {
60
- if (text !== void 0 && text.length > policy.maxTextLength) {
61
- issues.push({
62
- path: `fields[${index}].${property}`,
63
- code: "max_text_length_exceeded",
64
- message: `Text must be at most ${policy.maxTextLength} characters.`
65
- });
66
- }
67
- }
68
- }
69
- if (policy?.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
70
- issues.push({
71
- path: `fields[${index}].options`,
72
- code: "max_options_exceeded",
73
- message: `At most ${policy.maxOptionsPerField} options are allowed.`
74
- });
75
- }
76
- });
77
- for (const locale of policy?.requiredLocales ?? []) {
78
- if (!(schema.supportedLocales ?? []).includes(locale)) {
79
- issues.push({
80
- path: "supportedLocales",
81
- code: "required_locale_missing",
82
- message: `Required locale ${locale} is missing.`
83
- });
84
- }
85
- if (locale === schema.defaultLocale) continue;
86
- const requiredTranslations = [
87
- { path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title },
88
- ...schema.fields.map((field, index) => ({
89
- path: `fields[${index}].translations.${locale}.title`,
90
- value: field.translations?.[locale]?.title
91
- })),
92
- ...schema.fields.flatMap(
93
- (field, fieldIndex) => "options" in field ? field.options.map((option, optionIndex) => ({
94
- path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
95
- value: option.translations?.[locale]
96
- })) : []
97
- ),
98
- ...schema.pages?.flatMap(
99
- (page, pageIndex) => page.title === void 0 ? [] : [{ path: `pages[${pageIndex}].translations.${locale}.title`, value: page.translations?.[locale]?.title }]
100
- ) ?? []
101
- ];
102
- for (const translation of requiredTranslations) {
103
- if (translation.value === void 0 || translation.value.trim().length === 0) {
104
- issues.push({
105
- path: translation.path,
106
- code: "required_translation_missing",
107
- message: `A translation for required locale ${locale} is missing.`
108
- });
109
- }
110
- }
111
- }
44
+ function withoutDisplayCondition(field) {
45
+ const { displayCondition: _displayCondition, ...rest } = field;
46
+ return rest;
47
+ }
48
+ function removeLocalizedProperty(translations, locale, property) {
49
+ const current = translations?.[locale];
50
+ if (current === void 0) return translations;
51
+ const { [property]: _removed, ...remaining } = current;
52
+ return { ...translations, [locale]: remaining };
53
+ }
54
+ function setTranslationMetadata(node, locale, property, metadata, remove) {
55
+ const localeMetadata = node.translationMetadata?.[locale];
56
+ if (metadata === void 0 && !remove) return node;
57
+ const nextLocaleMetadata = remove ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== property)) : { ...localeMetadata, [property]: metadata ?? {} };
58
+ return { ...node, translationMetadata: { ...node.translationMetadata, [locale]: nextLocaleMetadata } };
59
+ }
60
+ function failedId(kind, result) {
61
+ return { success: false, error: result.error ?? { type: "invalid_id", kind, id: "" } };
112
62
  }
113
63
  function useFormBuilder({
114
64
  schema,
115
65
  onChange,
116
66
  policy,
117
- idFactory = defaultIdFactory
67
+ idFactory = defaultIdFactory,
68
+ factories = {}
118
69
  }) {
119
70
  const createId = useCallback(
120
71
  (kind, existingIds) => {
121
- const id = idFactory(kind, existingIds).trim();
122
- return id.length > 0 && !existingIds.has(id) ? id : void 0;
72
+ const rawId = idFactory(kind, existingIds);
73
+ const id = rawId.trim();
74
+ return id.length > 0 && !existingIds.has(id) ? { id } : { error: { type: "invalid_id", kind, id: rawId } };
123
75
  },
124
76
  [idFactory]
125
77
  );
78
+ const textPolicyError = useCallback(
79
+ (text) => policy?.maxTextLength !== void 0 && text.length > policy.maxTextLength ? { success: false, error: { type: "max_text_length_exceeded", max: policy.maxTextLength } } : void 0,
80
+ [policy?.maxTextLength]
81
+ );
82
+ const updateField = useCallback(
83
+ (fieldId, updater) => {
84
+ const current = schema.fields.find((field) => field.id === fieldId);
85
+ if (current === void 0)
86
+ return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
87
+ const updated = updater(current);
88
+ if (updated.id !== fieldId)
89
+ return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
90
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
91
+ return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
92
+ for (const text of [updated.title, updated.description]) {
93
+ if (text !== void 0) {
94
+ const error = textPolicyError(text);
95
+ if (error !== void 0) return error;
96
+ }
97
+ }
98
+ onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
99
+ return { success: true };
100
+ },
101
+ [onChange, policy?.allowedFieldTypes, schema, textPolicyError]
102
+ );
103
+ const updateOption = useCallback(
104
+ (fieldId, optionId, updater) => {
105
+ const field = schema.fields.find((candidate) => candidate.id === fieldId);
106
+ if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
107
+ if (!("options" in field))
108
+ return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
109
+ const current = field.options.find((option) => option.id === optionId);
110
+ if (current === void 0)
111
+ return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
112
+ const updated = updater(current);
113
+ if (updated.id !== optionId)
114
+ return { success: false, error: { type: "invalid_id", kind: "option", id: updated.id } };
115
+ const error = textPolicyError(updated.label);
116
+ if (error !== void 0) return error;
117
+ onChange({
118
+ ...schema,
119
+ fields: schema.fields.map(
120
+ (candidate) => candidate.id === fieldId && "options" in candidate ? {
121
+ ...candidate,
122
+ options: candidate.options.map((option) => option.id === optionId ? updated : option)
123
+ } : candidate
124
+ )
125
+ });
126
+ return { success: true };
127
+ },
128
+ [onChange, schema, textPolicyError]
129
+ );
130
+ const updatePage = useCallback(
131
+ (pageId, updater) => {
132
+ const page = schema.pages?.find((candidate) => candidate.id === pageId);
133
+ if (page === void 0) return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
134
+ const pages = schema.pages;
135
+ if (pages === void 0) return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
136
+ const updated = updater(page);
137
+ if (updated.id !== pageId) return { success: false, error: { type: "invalid_id", kind: "page", id: updated.id } };
138
+ onChange({ ...schema, pages: pages.map((candidate) => candidate.id === pageId ? updated : candidate) });
139
+ return { success: true };
140
+ },
141
+ [onChange, schema]
142
+ );
126
143
  const addField = useCallback(
127
144
  (type, pageId) => {
128
- if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type)) {
145
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
129
146
  return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
130
- }
131
- if (policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields) {
147
+ if (policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields)
132
148
  return { success: false, error: { type: "max_fields_exceeded", max: policy.maxFields } };
133
- }
134
- const fieldId = createId("field", new Set(schema.fields.map((field2) => field2.id)));
135
- const needsOption = ["select", "radio", "multi-select"].includes(type);
136
- const optionId = needsOption ? createId(
137
- "option",
138
- new Set(
139
- schema.fields.flatMap((field2) => "options" in field2 ? field2.options.map((option) => option.id) : [])
140
- )
141
- ) : void 0;
142
- if (fieldId === void 0 || needsOption && optionId === void 0) {
143
- return {
144
- success: false,
145
- error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
146
- };
147
- }
148
- const field = newField(type, fieldId, optionId);
149
- if (field === void 0) {
150
- return { success: false, error: { type: "invalid_operation", message: "Could not create the field." } };
149
+ if (pageId !== void 0 && !schema.pages?.some((page) => page.id === pageId))
150
+ return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
151
+ const fieldIds = new Set(schema.fields.map((field2) => field2.id));
152
+ const fieldId = createId("field", fieldIds);
153
+ if (fieldId.id === void 0) return failedId("field", fieldId);
154
+ let field = (factories.createField ?? defaultCreateField)(type, fieldId.id);
155
+ if (field.id !== fieldId.id || field.type !== type || fieldIds.has(field.id))
156
+ return { success: false, error: { type: "invalid_id", kind: "field", id: field.id } };
157
+ if (CHOICE_TYPES.includes(type) && "options" in field && field.options.length === 0) {
158
+ const optionIds = new Set(
159
+ schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
160
+ );
161
+ const optionId = createId("option", optionIds);
162
+ if (optionId.id === void 0) return failedId("option", optionId);
163
+ const option = (factories.createOption ?? defaultCreateOption)(field, optionId.id);
164
+ if (option.id !== optionId.id || optionIds.has(option.id))
165
+ return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
166
+ field = { ...field, options: [option] };
151
167
  }
152
168
  const pages = schema.pages?.map((page, index) => ({
153
169
  ...page,
154
- questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, fieldId] : page.questionIds
170
+ questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
155
171
  }));
156
- if (schema.pages !== void 0 && !schema.pages.some((page) => page.id === pageId) && pageId !== void 0) {
157
- return { success: false, error: { type: "invalid_operation", message: `Unknown page: ${pageId}` } };
158
- }
159
172
  onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
160
173
  return { success: true };
161
174
  },
162
- [createId, onChange, policy, schema]
175
+ [createId, factories, onChange, policy, schema]
163
176
  );
164
177
  const removeField = useCallback(
165
178
  (fieldId) => {
166
- if (schema.fields.length <= 1 || !schema.fields.some((field) => field.id === fieldId)) return;
167
- const fields = schema.fields.filter((field) => field.id !== fieldId).map(
168
- (field) => field.displayCondition?.questionId === fieldId ? (({ displayCondition: _condition, ...candidate }) => candidate)(field) : field
169
- );
170
- const remainingPages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
171
- if (schema.pages !== void 0 && remainingPages?.length === 0) {
172
- const { pages: _pages, ...singlePageSchema } = schema;
173
- onChange({ ...singlePageSchema, fields });
174
- } else {
175
- onChange({ ...schema, fields, ...remainingPages === void 0 ? {} : { pages: remainingPages } });
176
- }
179
+ if (!schema.fields.some((field) => field.id === fieldId))
180
+ return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
181
+ if (schema.fields.length <= 1)
182
+ return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
183
+ const fields = schema.fields.filter((field) => field.id !== fieldId).map((field) => field.displayCondition?.questionId === fieldId ? withoutDisplayCondition(field) : field);
184
+ const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
185
+ if (schema.pages !== void 0 && pages?.length === 0) {
186
+ const { pages: _pages, ...single } = schema;
187
+ onChange({ ...single, fields });
188
+ } else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
189
+ return { success: true };
177
190
  },
178
191
  [onChange, schema]
179
192
  );
180
193
  const moveField = useCallback(
181
194
  (fieldId, targetIndex) => {
182
- const fields = move(
183
- schema.fields,
184
- schema.fields.findIndex((field) => field.id === fieldId),
185
- targetIndex
186
- );
187
- if (fields !== void 0) {
188
- const indexById = new Map(fields.map((field, index) => [field.id, index]));
189
- const safeFields = fields.map((field, index) => {
190
- const sourceIndex = field.displayCondition === void 0 ? void 0 : indexById.get(field.displayCondition.questionId);
191
- if (field.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < index) return field;
192
- const { displayCondition: _condition, ...withoutCondition } = field;
193
- return withoutCondition;
194
- });
195
- onChange({ ...schema, fields: safeFields });
196
- }
195
+ const sourceIndex = schema.fields.findIndex((field) => field.id === fieldId);
196
+ if (sourceIndex < 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
197
+ const fields = move(schema.fields, sourceIndex, targetIndex);
198
+ if (fields === void 0)
199
+ return { success: false, error: { type: "invalid_operation", message: "Invalid field position." } };
200
+ const indexById = new Map(fields.map((field, index) => [field.id, index]));
201
+ onChange({
202
+ ...schema,
203
+ fields: fields.map((field, index) => {
204
+ const source = field.displayCondition?.questionId;
205
+ return source === void 0 || (indexById.get(source) ?? index) < index ? field : withoutDisplayCondition(field);
206
+ })
207
+ });
208
+ return { success: true };
197
209
  },
198
210
  [onChange, schema]
199
211
  );
200
- const updateField = useCallback(
201
- (fieldId, updater) => {
202
- const current = schema.fields.find((field) => field.id === fieldId);
203
- if (current === void 0) return;
204
- const updated = updater(current);
205
- if (updated.id !== fieldId) return;
206
- if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type)) return;
207
- const maxTextLength = policy?.maxTextLength;
208
- if (maxTextLength !== void 0 && [updated.title, updated.description].some((text) => text !== void 0 && text.length > maxTextLength)) {
209
- return;
210
- }
211
- onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
212
- },
213
- [onChange, policy, schema]
214
- );
215
212
  const addOption = useCallback(
216
213
  (fieldId) => {
217
214
  const field = schema.fields.find((candidate) => candidate.id === fieldId);
218
- if (field === void 0 || !("options" in field)) {
215
+ if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
216
+ if (!("options" in field))
219
217
  return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
220
- }
221
- if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField) {
218
+ if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
222
219
  return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
223
- }
224
- const existingIds = new Set(
225
- schema.fields.flatMap(
226
- (candidate) => "options" in candidate ? candidate.options.map((option2) => option2.id) : []
227
- )
220
+ const ids = new Set(
221
+ schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
228
222
  );
229
- const optionId = createId("option", existingIds);
230
- if (optionId === void 0) {
231
- return {
232
- success: false,
233
- error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
234
- };
235
- }
236
- const option = { id: optionId, label: `Option ${field.options.length + 1}` };
223
+ const id = createId("option", ids);
224
+ if (id.id === void 0) return failedId("option", id);
225
+ const option = (factories.createOption ?? defaultCreateOption)(field, id.id);
226
+ if (option.id !== id.id || ids.has(option.id))
227
+ return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
237
228
  onChange({
238
229
  ...schema,
239
230
  fields: schema.fields.map(
240
- (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: [...candidate.options, option] } : candidate
231
+ (item) => item.id === fieldId && "options" in item ? { ...item, options: [...item.options, option] } : item
241
232
  )
242
233
  });
243
234
  return { success: true };
244
235
  },
245
- [createId, onChange, policy, schema]
236
+ [createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
246
237
  );
247
238
  const removeOption = useCallback(
248
239
  (fieldId, optionId) => {
249
- const field = schema.fields.find((candidate) => candidate.id === fieldId);
250
- if (field === void 0 || !("options" in field) || field.options.length <= 1) return;
240
+ const field = schema.fields.find((item) => item.id === fieldId);
241
+ if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
242
+ if (!("options" in field) || !field.options.some((option) => option.id === optionId))
243
+ return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
244
+ if (field.options.length <= 1)
245
+ return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
251
246
  onChange({
252
247
  ...schema,
253
248
  fields: schema.fields.map(
254
- (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: candidate.options.filter((option) => option.id !== optionId) } : candidate
249
+ (item) => item.id === fieldId && "options" in item ? { ...item, options: item.options.filter((option) => option.id !== optionId) } : item
255
250
  )
256
251
  });
252
+ return { success: true };
257
253
  },
258
254
  [onChange, schema]
259
255
  );
260
256
  const moveOption = useCallback(
261
257
  (fieldId, optionId, targetIndex) => {
262
- const field = schema.fields.find((candidate) => candidate.id === fieldId);
263
- if (field === void 0 || !("options" in field)) return;
258
+ const field = schema.fields.find((item) => item.id === fieldId);
259
+ if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
260
+ if (!("options" in field))
261
+ return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
264
262
  const options = move(
265
263
  field.options,
266
264
  field.options.findIndex((option) => option.id === optionId),
267
265
  targetIndex
268
266
  );
269
- if (options === void 0) return;
267
+ if (options === void 0)
268
+ return { success: false, error: { type: "invalid_operation", message: "Invalid option position." } };
270
269
  onChange({
271
270
  ...schema,
272
271
  fields: schema.fields.map(
273
- (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options } : candidate
272
+ (item) => item.id === fieldId && "options" in item ? { ...item, options } : item
274
273
  )
275
274
  });
275
+ return { success: true };
276
276
  },
277
277
  [onChange, schema]
278
278
  );
279
+ const changeFieldType = useCallback(
280
+ (fieldId, type) => {
281
+ const field = schema.fields.find((item) => item.id === fieldId);
282
+ if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
283
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
284
+ return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
285
+ let transformed = transformFieldType(field, type);
286
+ if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
287
+ const ids = new Set(
288
+ schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
289
+ );
290
+ const id = createId("option", ids);
291
+ if (id.id === void 0) return failedId("option", id);
292
+ const option = (factories.createOption ?? defaultCreateOption)(transformed, id.id);
293
+ if (option.id !== id.id || ids.has(option.id))
294
+ return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
295
+ transformed = { ...transformed, options: [option] };
296
+ }
297
+ onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
298
+ return { success: true };
299
+ },
300
+ [createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
301
+ );
279
302
  const addPage = useCallback(
280
303
  (questionId) => {
281
- const existingIds = new Set(schema.pages?.map((page) => page.id) ?? []);
282
- const pageId = createId("page", existingIds);
283
- if (pageId === void 0) return;
284
- if (schema.pages === void 0) {
285
- onChange({ ...schema, pages: [{ id: pageId, questionIds: schema.fields.map((field) => field.id) }] });
286
- return;
287
- }
288
- const movableQuestionId = questionId ?? schema.pages.find((page) => page.questionIds.length > 1)?.questionIds.at(-1);
289
- if (movableQuestionId === void 0) return;
290
- const sourcePage = schema.pages.find((page) => page.questionIds.includes(movableQuestionId));
291
- if (sourcePage === void 0 || sourcePage.questionIds.length <= 1) return;
292
- const pages = [
293
- ...schema.pages.map((page) => ({
294
- ...page,
295
- questionIds: page.questionIds.filter((id) => id !== movableQuestionId)
304
+ const ids = new Set(schema.pages?.map((page2) => page2.id) ?? []);
305
+ const id = createId("page", ids);
306
+ if (id.id === void 0) return failedId("page", id);
307
+ const questionIds = schema.pages === void 0 ? schema.fields.map((field) => field.id) : [questionId ?? schema.pages.find((page2) => page2.questionIds.length > 1)?.questionIds.at(-1)].filter(
308
+ (value) => value !== void 0
309
+ );
310
+ if (questionIds.length === 0)
311
+ return { success: false, error: { type: "invalid_operation", message: "No question can be moved." } };
312
+ const source = schema.pages?.find((page2) => page2.questionIds.includes(questionIds[0] ?? ""));
313
+ if (schema.pages !== void 0 && (source === void 0 || source.questionIds.length <= 1))
314
+ return { success: false, error: { type: "invalid_operation", message: "A page cannot be left empty." } };
315
+ const page = (factories.createPage ?? defaultCreatePage)(id.id, [...questionIds]);
316
+ if (page.id !== id.id || ids.has(page.id))
317
+ return { success: false, error: { type: "invalid_id", kind: "page", id: page.id } };
318
+ const pages = schema.pages === void 0 ? [page] : [
319
+ ...schema.pages.map((item) => ({
320
+ ...item,
321
+ questionIds: item.questionIds.filter((fieldId) => !questionIds.includes(fieldId))
296
322
  })),
297
- { id: pageId, questionIds: [movableQuestionId] }
323
+ page
298
324
  ];
299
325
  onChange({ ...schema, pages });
326
+ return { success: true };
300
327
  },
301
- [createId, onChange, schema]
328
+ [createId, factories.createPage, onChange, schema]
302
329
  );
303
330
  const removePage = useCallback(
304
331
  (pageId) => {
305
- if (schema.pages === void 0) return;
306
- const index = schema.pages.findIndex((page) => page.id === pageId);
307
- const removed = schema.pages[index];
308
- if (removed === void 0) return;
309
- if (schema.pages.length === 1) {
310
- const { pages: _pages, ...singlePage } = schema;
311
- onChange(singlePage);
312
- return;
332
+ const index = schema.pages?.findIndex((page) => page.id === pageId) ?? -1;
333
+ const removed = schema.pages?.[index];
334
+ if (removed === void 0) return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
335
+ const currentPages = schema.pages;
336
+ if (currentPages === void 0)
337
+ return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
338
+ if ((schema.pages?.length ?? 0) === 1) {
339
+ const { pages: _pages, ...single } = schema;
340
+ onChange(single);
341
+ return { success: true };
313
342
  }
314
343
  const targetIndex = index === 0 ? 1 : index - 1;
315
344
  onChange({
316
345
  ...schema,
317
- pages: schema.pages.map(
346
+ pages: currentPages.map(
318
347
  (page, pageIndex) => pageIndex === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
319
348
  ).filter((page) => page.id !== pageId)
320
349
  });
350
+ return { success: true };
321
351
  },
322
352
  [onChange, schema]
323
353
  );
324
- const setLocaleTranslation = useCallback(
325
- (locale, target, property, text) => {
326
- if (locale.trim().length === 0 || policy?.maxTextLength !== void 0 && text.length > policy.maxTextLength)
327
- return;
328
- const supportedLocales = [.../* @__PURE__ */ new Set([...schema.supportedLocales ?? [], locale])];
329
- if (target === "form" && ["title", "description", "completionMessage"].includes(property)) {
330
- onChange({
331
- ...schema,
332
- supportedLocales,
333
- translations: { ...schema.translations, [locale]: { ...schema.translations?.[locale], [property]: text } }
354
+ const movePage = useCallback(
355
+ (pageId, targetIndex) => {
356
+ const sourceIndex = schema.pages?.findIndex((page) => page.id === pageId) ?? -1;
357
+ if (sourceIndex < 0) return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
358
+ const pages = move(schema.pages ?? [], sourceIndex, targetIndex);
359
+ if (pages === void 0)
360
+ return { success: false, error: { type: "invalid_operation", message: "Invalid page position." } };
361
+ const assignment = new Map(pages.flatMap((page, index) => page.questionIds.map((id) => [id, index])));
362
+ onChange({
363
+ ...schema,
364
+ pages: pages.map((page, index) => {
365
+ const source = page.displayCondition?.questionId;
366
+ if (source === void 0 || (assignment.get(source) ?? index) < index) return page;
367
+ const { displayCondition: _condition, ...rest } = page;
368
+ return rest;
369
+ })
370
+ });
371
+ return { success: true };
372
+ },
373
+ [onChange, schema]
374
+ );
375
+ const assignFieldToPage = useCallback(
376
+ (fieldId, pageId) => {
377
+ if (!schema.fields.some((field) => field.id === fieldId))
378
+ return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
379
+ if (pageId === null) {
380
+ if (schema.pages !== void 0) {
381
+ const { pages: _pages, ...single } = schema;
382
+ onChange(single);
383
+ }
384
+ return { success: true };
385
+ }
386
+ if (!schema.pages?.some((page) => page.id === pageId))
387
+ return { success: false, error: { type: "node_not_found", kind: "page", id: pageId } };
388
+ const pages = schema.pages.map((page) => ({
389
+ ...page,
390
+ questionIds: page.id === pageId ? schema.fields.filter((field) => page.questionIds.includes(field.id) || field.id === fieldId).map((field) => field.id) : page.questionIds.filter((id) => id !== fieldId)
391
+ })).filter((page) => page.questionIds.length > 0);
392
+ onChange({ ...schema, pages });
393
+ return { success: true };
394
+ },
395
+ [onChange, schema]
396
+ );
397
+ const setDisplayCondition = useCallback(
398
+ (fieldId, condition) => {
399
+ const index = schema.fields.findIndex((field) => field.id === fieldId);
400
+ if (index < 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
401
+ if (condition !== void 0) {
402
+ const sourceIndex = schema.fields.findIndex((field) => field.id === condition.questionId);
403
+ if (sourceIndex < 0 || sourceIndex >= index)
404
+ return {
405
+ success: false,
406
+ error: { type: "invalid_operation", message: "Conditions require an earlier field." }
407
+ };
408
+ }
409
+ onChange({
410
+ ...schema,
411
+ fields: schema.fields.map(
412
+ (field) => field.id !== fieldId ? field : condition === void 0 ? withoutDisplayCondition(field) : { ...field, displayCondition: condition }
413
+ )
414
+ });
415
+ return { success: true };
416
+ },
417
+ [onChange, schema]
418
+ );
419
+ const setSourceText = useCallback(
420
+ (target, property, text) => {
421
+ const error = textPolicyError(text);
422
+ if (error !== void 0) return error;
423
+ if (target.kind === "form") {
424
+ if (!["title", "description", "completionMessage"].includes(property))
425
+ return {
426
+ success: false,
427
+ error: { type: "invalid_operation", message: `Unsupported form property: ${property}` }
428
+ };
429
+ if (property === "title") onChange({ ...schema, title: text });
430
+ else if (property === "description") {
431
+ const { description: _description, ...withoutDescription } = schema;
432
+ onChange(text.length === 0 ? withoutDescription : { ...schema, description: text });
433
+ } else {
434
+ const { completionMessage: _completionMessage, ...withoutCompletionMessage } = schema;
435
+ onChange(text.length === 0 ? withoutCompletionMessage : { ...schema, completionMessage: text });
436
+ }
437
+ return { success: true };
438
+ }
439
+ if (target.id === void 0)
440
+ return { success: false, error: { type: "invalid_operation", message: "A target ID is required." } };
441
+ if (target.kind === "field") {
442
+ if (!["title", "description"].includes(property))
443
+ return {
444
+ success: false,
445
+ error: { type: "invalid_operation", message: `Unsupported field property: ${property}` }
446
+ };
447
+ return updateField(target.id, (field) => {
448
+ if (property === "title") return { ...field, title: text };
449
+ const { description: _description, ...withoutDescription } = field;
450
+ return text.length === 0 ? withoutDescription : { ...field, description: text };
334
451
  });
335
- return;
336
452
  }
337
- const fields = schema.fields.map((field) => {
338
- if (field.id === target && ["title", "description"].includes(property)) {
453
+ if (target.kind === "option") {
454
+ if (property !== "label")
455
+ return {
456
+ success: false,
457
+ error: { type: "invalid_operation", message: `Unsupported option property: ${property}` }
458
+ };
459
+ for (const field of schema.fields)
460
+ if ("options" in field && field.options.some((option) => option.id === target.id))
461
+ return updateOption(field.id, target.id, (option) => ({ ...option, label: text }));
462
+ return { success: false, error: { type: "node_not_found", kind: "option", id: target.id } };
463
+ }
464
+ if (!["title", "description"].includes(property))
465
+ return {
466
+ success: false,
467
+ error: { type: "invalid_operation", message: `Unsupported page property: ${property}` }
468
+ };
469
+ return updatePage(target.id, (page) => {
470
+ if (property === "title") {
471
+ const { title: _title, ...withoutTitle } = page;
472
+ return text.length === 0 ? withoutTitle : { ...page, title: text };
473
+ }
474
+ const { description: _description, ...withoutDescription } = page;
475
+ return text.length === 0 ? withoutDescription : { ...page, description: text };
476
+ });
477
+ },
478
+ [onChange, schema, textPolicyError, updateField, updateOption, updatePage]
479
+ );
480
+ const setLocaleTranslation = useCallback(
481
+ (locale, target, property, text, options = {}) => {
482
+ const normalized = locale.trim();
483
+ if (normalized.length === 0)
484
+ return { success: false, error: { type: "invalid_operation", message: "Locale must not be empty." } };
485
+ const error = textPolicyError(text);
486
+ if (error !== void 0) return error;
487
+ const supportedLocales = [.../* @__PURE__ */ new Set([...schema.supportedLocales ?? [], normalized])];
488
+ const remove = text.length === 0;
489
+ if (target.kind === "form") {
490
+ if (!["title", "description", "completionMessage"].includes(property))
339
491
  return {
340
- ...field,
341
- translations: { ...field.translations, [locale]: { ...field.translations?.[locale], [property]: text } }
492
+ success: false,
493
+ error: { type: "invalid_operation", message: `Unsupported form property: ${property}` }
342
494
  };
495
+ const key = property;
496
+ const translations = remove ? removeLocalizedProperty(schema.translations, normalized, key) : { ...schema.translations, [normalized]: { ...schema.translations?.[normalized], [key]: text } };
497
+ onChange(
498
+ setTranslationMetadata(
499
+ { ...schema, supportedLocales, ...translations === void 0 ? {} : { translations } },
500
+ normalized,
501
+ property,
502
+ options.metadata,
503
+ remove
504
+ )
505
+ );
506
+ return { success: true };
507
+ }
508
+ if (target.id === void 0)
509
+ return { success: false, error: { type: "invalid_operation", message: "A target ID is required." } };
510
+ let found = false;
511
+ const fields = schema.fields.map((field) => {
512
+ if (target.kind === "field" && field.id === target.id && ["title", "description"].includes(property)) {
513
+ found = true;
514
+ const key = property;
515
+ const translations = remove ? removeLocalizedProperty(field.translations, normalized, key) : { ...field.translations, [normalized]: { ...field.translations?.[normalized], [key]: text } };
516
+ return setTranslationMetadata(
517
+ { ...field, ...translations === void 0 ? {} : { translations } },
518
+ normalized,
519
+ property,
520
+ options.metadata,
521
+ remove
522
+ );
343
523
  }
344
- if (!("options" in field) || property !== "label") return field;
524
+ if (target.kind !== "option" || property !== "label" || !("options" in field)) return field;
345
525
  return {
346
526
  ...field,
347
- options: field.options.map(
348
- (option) => option.id === target ? { ...option, translations: { ...option.translations, [locale]: text } } : option
349
- )
527
+ options: field.options.map((option) => {
528
+ if (option.id !== target.id) return option;
529
+ found = true;
530
+ const translations = remove ? Object.fromEntries(Object.entries(option.translations ?? {}).filter(([key]) => key !== normalized)) : { ...option.translations, [normalized]: text };
531
+ return setTranslationMetadata({ ...option, translations }, normalized, property, options.metadata, remove);
532
+ })
350
533
  };
351
534
  });
352
- const pages = schema.pages?.map(
353
- (page) => page.id === target && ["title", "description"].includes(property) ? {
354
- ...page,
355
- translations: { ...page.translations, [locale]: { ...page.translations?.[locale], [property]: text } }
356
- } : page
357
- );
535
+ const pages = schema.pages?.map((page) => {
536
+ if (target.kind !== "page" || page.id !== target.id || !["title", "description"].includes(property))
537
+ return page;
538
+ found = true;
539
+ const key = property;
540
+ const translations = remove ? removeLocalizedProperty(page.translations, normalized, key) : { ...page.translations, [normalized]: { ...page.translations?.[normalized], [key]: text } };
541
+ return setTranslationMetadata(
542
+ { ...page, ...translations === void 0 ? {} : { translations } },
543
+ normalized,
544
+ property,
545
+ options.metadata,
546
+ remove
547
+ );
548
+ });
549
+ if (!found) return { success: false, error: { type: "node_not_found", kind: target.kind, id: target.id } };
358
550
  onChange({ ...schema, supportedLocales, fields, ...pages === void 0 ? {} : { pages } });
551
+ return { success: true };
359
552
  },
360
- [onChange, policy, schema]
553
+ [onChange, schema, textPolicyError]
554
+ );
555
+ const addLocale = useCallback(
556
+ (locale) => {
557
+ const normalized = locale.trim();
558
+ if (normalized.length === 0)
559
+ return { success: false, error: { type: "invalid_operation", message: "Locale must not be empty." } };
560
+ onChange({
561
+ ...schema,
562
+ supportedLocales: [
563
+ .../* @__PURE__ */ new Set([
564
+ ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
565
+ ...schema.supportedLocales ?? [],
566
+ normalized
567
+ ])
568
+ ]
569
+ });
570
+ return { success: true };
571
+ },
572
+ [onChange, schema]
573
+ );
574
+ const setDefaultLocale = useCallback(
575
+ (locale) => {
576
+ const normalized = locale.trim();
577
+ if (normalized.length === 0)
578
+ return { success: false, error: { type: "invalid_operation", message: "Locale must not be empty." } };
579
+ onChange({
580
+ ...schema,
581
+ defaultLocale: normalized,
582
+ supportedLocales: [.../* @__PURE__ */ new Set([normalized, ...schema.supportedLocales ?? []])]
583
+ });
584
+ return { success: true };
585
+ },
586
+ [onChange, schema]
361
587
  );
362
588
  const validationIssues = useMemo(() => {
363
- const result = validateFormSchema(schema);
364
- const issues = result.valid ? [] : [...result.issues];
365
- addPolicyIssues(schema, policy, issues);
366
- return issues;
589
+ const result = validateFormSchema(schema, policy === void 0 ? {} : { policy });
590
+ return result.valid ? [] : result.issues;
367
591
  }, [policy, schema]);
368
592
  return {
369
593
  schema,
@@ -371,12 +595,21 @@ function useFormBuilder({
371
595
  removeField,
372
596
  moveField,
373
597
  updateField,
598
+ changeFieldType,
374
599
  addOption,
600
+ updateOption,
375
601
  removeOption,
376
602
  moveOption,
377
603
  addPage,
604
+ updatePage,
378
605
  removePage,
606
+ movePage,
607
+ assignFieldToPage,
608
+ setDisplayCondition,
609
+ setSourceText,
379
610
  setLocaleTranslation,
611
+ addLocale,
612
+ setDefaultLocale,
380
613
  validationIssues
381
614
  };
382
615
  }
@@ -402,6 +635,7 @@ var BUILDER_DEFAULTS = {
402
635
  "builder.questionTitle": "\u8CEA\u554F\u6587 / Question Title",
403
636
  "builder.questionTitlePlaceholder": "Example: Tell us what we could improve",
404
637
  "builder.newQuestionTitle": "New question",
638
+ "builder.completionMessage": "Completion message",
405
639
  "builder.type": "Type",
406
640
  "builder.required": "Required",
407
641
  "builder.minimum": "Minimum",
@@ -460,33 +694,6 @@ function fieldTypeKey(type) {
460
694
  function operatorKey(operator) {
461
695
  return `builder.operator.${operator}`;
462
696
  }
463
- function createUniqueId(prefix, existingIds) {
464
- let id;
465
- do {
466
- id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
467
- } while (existingIds.has(id));
468
- return id;
469
- }
470
- function baseField(field, type) {
471
- return {
472
- id: field.id,
473
- type,
474
- title: field.title,
475
- ...field.description === void 0 ? {} : { description: field.description },
476
- ...field.translationKey === void 0 ? {} : { translationKey: field.translationKey },
477
- required: field.required,
478
- ...field.displayCondition === void 0 ? {} : { displayCondition: field.displayCondition }
479
- };
480
- }
481
- function normalizeField(field, type, newOptionLabel) {
482
- const base = baseField(field, type);
483
- if (type === "text" || type === "textarea") return { ...base, type };
484
- if (type === "number") return { ...base, type };
485
- if (type === "rating") return { ...base, type, min: 1, max: 5 };
486
- if (type === "checkbox") return { ...base, type };
487
- const options = "options" in field && field.options.length > 0 ? field.options : [{ id: createUniqueId("opt", /* @__PURE__ */ new Set()), label: newOptionLabel }];
488
- return { ...base, type, options };
489
- }
490
697
  function defaultConditionValue(field) {
491
698
  if (field.type === "checkbox") return true;
492
699
  if (field.type === "number" || field.type === "rating") return field.min ?? 1;
@@ -500,30 +707,6 @@ function conditionOperators(field) {
500
707
  }
501
708
  return ["equals", "not_equals", "not_empty"];
502
709
  }
503
- function withoutDisplayCondition(field) {
504
- const { displayCondition: _displayCondition, ...rest } = field;
505
- return rest;
506
- }
507
- function sanitizeBuilderSchema(schema) {
508
- const sanitized = sanitizeSchema(schema);
509
- const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
510
- const fields = sanitized.fields.map((field, index) => {
511
- const sourceId = field.displayCondition?.questionId;
512
- if (sourceId === void 0) return field;
513
- const sourceIndex = indexById.get(sourceId);
514
- return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
515
- });
516
- return {
517
- ...sanitized,
518
- fields,
519
- ...sanitized.pages === void 0 ? {} : {
520
- pages: sanitized.pages.map((page) => ({
521
- ...page,
522
- questionIds: fields.filter((field) => page.questionIds.includes(field.id)).map((field) => field.id)
523
- }))
524
- }
525
- };
526
- }
527
710
  function conditionWithValue(questionId, operator, value) {
528
711
  return operator === "not_empty" ? { questionId, operator } : { questionId, operator, value };
529
712
  }
@@ -565,20 +748,36 @@ function ConditionValueEditor({
565
748
  }
566
749
  );
567
750
  }
751
+ function resolveInitialFieldType(defaultType, allowedTypes) {
752
+ if (defaultType !== void 0 && (allowedTypes === void 0 || allowedTypes.includes(defaultType))) {
753
+ return defaultType;
754
+ }
755
+ if (allowedTypes !== void 0 && allowedTypes.length > 0) return allowedTypes[0] ?? null;
756
+ if (allowedTypes === void 0 || allowedTypes.includes("text")) return "text";
757
+ return null;
758
+ }
568
759
  function FormBuilder({
569
760
  schema,
570
761
  onChange,
571
762
  locale = "en",
572
763
  translator,
573
764
  translationAdapter,
765
+ translationOptions,
766
+ onTranslationReport,
574
767
  policy,
575
- idFactory
768
+ idFactory,
769
+ factories,
770
+ className = "",
771
+ defaultFieldType,
772
+ onActionError,
773
+ createManualTranslationMetadata
576
774
  }) {
577
775
  const headless = useFormBuilder({
578
776
  schema,
579
777
  onChange,
580
778
  ...policy === void 0 ? {} : { policy },
581
- ...idFactory === void 0 ? {} : { idFactory }
779
+ ...idFactory === void 0 ? {} : { idFactory },
780
+ ...factories === void 0 ? {} : { factories }
582
781
  });
583
782
  const [newPageQuestionId, setNewPageQuestionId] = useState("");
584
783
  const [newLocale, setNewLocale] = useState("");
@@ -589,48 +788,40 @@ function FormBuilder({
589
788
  const translated = translator?.translate(key, locale, params);
590
789
  return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
591
790
  };
592
- const emitSchema = (candidate) => onChange(sanitizeBuilderSchema(candidate));
593
- const updateField = headless.updateField;
594
- const changeType = (fieldId, type) => {
595
- emitSchema({
596
- ...schema,
597
- fields: schema.fields.map((field) => {
598
- if (field.id === fieldId) {
599
- return normalizeField(field, type, translate("builder.newOptionLabel", { index: 1 }));
600
- }
601
- if (field.displayCondition?.questionId === fieldId) {
602
- const { displayCondition: _condition, ...withoutCondition } = field;
603
- return withoutCondition;
604
- }
605
- return field;
606
- })
607
- });
791
+ const executeAction = (result, context) => {
792
+ if (!result.success) onActionError?.(result.error, context);
793
+ return result;
608
794
  };
609
- const removeField = headless.removeField;
795
+ const updateField = (fieldId, updater, params) => executeAction(headless.updateField(fieldId, updater), {
796
+ action: "updateField",
797
+ targetId: fieldId,
798
+ ...params === void 0 ? {} : { params }
799
+ });
800
+ const changeType = (fieldId, type) => executeAction(headless.changeFieldType(fieldId, type), {
801
+ action: "changeFieldType",
802
+ targetId: fieldId,
803
+ params: { type }
804
+ });
805
+ const removeField = (fieldId) => executeAction(headless.removeField(fieldId), { action: "removeField", targetId: fieldId });
806
+ const initialFieldType = resolveInitialFieldType(defaultFieldType, policy?.allowedFieldTypes);
807
+ const maxFieldsReached = policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields;
610
808
  const moveField = (index, offset) => {
611
809
  const target = index + offset;
612
- if (target < 0 || target >= schema.fields.length) return;
613
- const fields = [...schema.fields];
614
- const current = fields[index];
615
- const other = fields[target];
616
- if (current === void 0 || other === void 0) return;
617
- fields[index] = other;
618
- fields[target] = current;
619
- emitSchema({ ...schema, fields });
810
+ const field = schema.fields[index];
811
+ if (field !== void 0) {
812
+ executeAction(headless.moveField(field.id, target), {
813
+ action: "moveField",
814
+ targetId: field.id,
815
+ params: { targetIndex: target }
816
+ });
817
+ }
818
+ };
819
+ const addField = () => {
820
+ if (initialFieldType === null) return;
821
+ executeAction(headless.addField(initialFieldType), { action: "addField" });
620
822
  };
621
- const addField = () => headless.addField("text");
622
823
  const enablePages = () => {
623
- if (schema.pages !== void 0) return;
624
- emitSchema({
625
- ...schema,
626
- pages: [
627
- {
628
- id: createUniqueId("page", /* @__PURE__ */ new Set()),
629
- title: translate("builder.newPage"),
630
- questionIds: schema.fields.map((field) => field.id)
631
- }
632
- ]
633
- });
824
+ if (schema.pages === void 0) executeAction(headless.addPage(), { action: "addPage" });
634
825
  };
635
826
  const pageForField = (fieldId) => schema.pages?.find((page) => page.questionIds.includes(fieldId));
636
827
  const movablePageQuestions = schema.pages?.flatMap((page) => page.questionIds.length > 1 ? page.questionIds : []) ?? [];
@@ -641,73 +832,42 @@ function FormBuilder({
641
832
  }
642
833
  const questionId = movablePageQuestions.includes(newPageQuestionId) ? newPageQuestionId : movablePageQuestions[0];
643
834
  if (questionId === void 0) return;
644
- const pageId = createUniqueId("page", new Set(schema.pages.map((page) => page.id)));
645
- emitSchema({
646
- ...schema,
647
- pages: [
648
- ...schema.pages.map((page) => ({
649
- ...page,
650
- questionIds: page.questionIds.filter((id) => id !== questionId)
651
- })),
652
- { id: pageId, title: translate("builder.newPage"), questionIds: [questionId] }
653
- ]
835
+ executeAction(headless.addPage(questionId), {
836
+ action: "addPage",
837
+ targetId: questionId,
838
+ params: { questionId }
654
839
  });
655
840
  setNewPageQuestionId("");
656
841
  };
657
842
  const removePage = (pageIndex) => {
658
- if (schema.pages === void 0) return;
659
- const removed = schema.pages[pageIndex];
660
- if (removed === void 0) return;
661
- if (schema.pages.length === 1) {
662
- const { pages: _pages, ...singlePage } = schema;
663
- emitSchema(singlePage);
664
- return;
665
- }
666
- const targetIndex = pageIndex === 0 ? 1 : pageIndex - 1;
667
- emitSchema({
668
- ...schema,
669
- pages: schema.pages.map(
670
- (page, index) => index === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
671
- ).filter((_page, index) => index !== pageIndex)
672
- });
843
+ const page = schema.pages?.[pageIndex];
844
+ if (page !== void 0) executeAction(headless.removePage(page.id), { action: "removePage", targetId: page.id });
673
845
  };
674
846
  const movePage = (pageIndex, offset) => {
675
- if (schema.pages === void 0) return;
676
847
  const target = pageIndex + offset;
677
- if (target < 0 || target >= schema.pages.length) return;
678
- const pages = [...schema.pages];
679
- const current = pages[pageIndex];
680
- const other = pages[target];
681
- if (current === void 0 || other === void 0) return;
682
- pages[pageIndex] = other;
683
- pages[target] = current;
684
- emitSchema({ ...schema, pages });
848
+ const page = schema.pages?.[pageIndex];
849
+ if (page !== void 0) {
850
+ executeAction(headless.movePage(page.id, target), {
851
+ action: "movePage",
852
+ targetId: page.id,
853
+ params: { targetIndex: target }
854
+ });
855
+ }
685
856
  };
686
857
  const updatePage = (pageId, update) => {
687
- if (schema.pages === void 0) return;
688
- emitSchema({ ...schema, pages: schema.pages.map((page) => page.id === pageId ? update(page) : page) });
858
+ headless.updatePage(pageId, update);
689
859
  };
690
860
  const assignFieldToPage = (fieldId, pageId) => {
691
- if (schema.pages === void 0) return;
692
- emitSchema({
693
- ...schema,
694
- pages: schema.pages.map((page) => ({
695
- ...page,
696
- questionIds: page.id === pageId ? schema.fields.filter((field) => page.questionIds.includes(field.id) || field.id === fieldId).map((field) => field.id) : page.questionIds.filter((id) => id !== fieldId)
697
- })).filter((page) => page.questionIds.length > 0)
861
+ executeAction(headless.assignFieldToPage(fieldId, pageId), {
862
+ action: "assignFieldToPage",
863
+ targetId: fieldId,
864
+ params: { pageId }
698
865
  });
699
866
  };
700
867
  const addLocale = () => {
701
868
  const normalized = newLocale.trim();
702
869
  if (normalized.length === 0) return;
703
- const supportedLocales = [
704
- .../* @__PURE__ */ new Set([
705
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
706
- ...schema.supportedLocales ?? [],
707
- normalized
708
- ])
709
- ];
710
- emitSchema({ ...schema, supportedLocales });
870
+ headless.addLocale(normalized);
711
871
  setEditingLocale(normalized);
712
872
  setNewLocale("");
713
873
  };
@@ -717,48 +877,377 @@ function FormBuilder({
717
877
  setTranslationError(null);
718
878
  try {
719
879
  const populated = await populateSchemaTranslations(schema, [editingLocale], translationAdapter, {
720
- overwrite: "all"
880
+ overwrite: "missing-only",
881
+ ...translationOptions
721
882
  });
722
883
  onChange(populated.schema);
884
+ onTranslationReport?.(populated.report);
723
885
  } catch (cause) {
724
886
  setTranslationError(cause instanceof Error ? cause.message : String(cause));
725
887
  } finally {
726
888
  setIsTranslating(false);
727
889
  }
728
890
  };
729
- const updateFormTranslation = (key, value) => {
730
- if (editingLocale.length === 0) return;
731
- const current = schema.translations?.[editingLocale];
732
- const next = key === "title" ? value.length === 0 ? { description: current?.description } : { ...current, title: value } : value.length === 0 ? { title: current?.title } : { ...current, description: value };
733
- emitSchema({
734
- ...schema,
735
- translations: {
736
- ...schema.translations,
737
- [editingLocale]: {
738
- ...next.title === void 0 ? {} : { title: next.title },
739
- ...next.description === void 0 ? {} : { description: next.description }
740
- }
891
+ const updateManualTranslation = (context) => {
892
+ const metadata = createManualTranslationMetadata?.(context);
893
+ const target = context.kind === "form" ? { kind: "form" } : { kind: context.kind, id: context.nodeId };
894
+ executeAction(
895
+ headless.setLocaleTranslation(
896
+ context.locale,
897
+ target,
898
+ context.property,
899
+ context.translatedText,
900
+ metadata === void 0 ? void 0 : { metadata }
901
+ ),
902
+ {
903
+ action: "setLocaleTranslation",
904
+ targetId: context.nodeId,
905
+ params: { locale: context.locale, kind: context.kind, property: context.property }
741
906
  }
907
+ );
908
+ };
909
+ const updateFormTranslation = (property, translatedText) => {
910
+ if (editingLocale.length === 0) return;
911
+ updateManualTranslation({
912
+ locale: editingLocale,
913
+ kind: "form",
914
+ nodeId: schema.id,
915
+ property,
916
+ sourceText: schema[property] ?? "",
917
+ translatedText,
918
+ ...schema.translationMetadata?.[editingLocale]?.[property] === void 0 ? {} : { existingTranslationMetadata: schema.translationMetadata[editingLocale]?.[property] }
742
919
  });
743
920
  };
744
- return /* @__PURE__ */ jsxs("section", { className: "form-engine-builder", "aria-label": translate("builder.formBuilder"), children: [
745
- /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__pages", "aria-labelledby": "builder-pages-heading", children: [
746
- /* @__PURE__ */ jsx("h2", { id: "builder-pages-heading", children: translate("builder.pages") }),
747
- schema.pages === void 0 ? /* @__PURE__ */ jsx("button", { type: "button", onClick: enablePages, children: translate("builder.enablePages") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
748
- schema.pages.map((page, pageIndex) => {
749
- const priorQuestionIds = new Set(schema.pages?.slice(0, pageIndex).flatMap((item) => item.questionIds));
750
- const availableSources = schema.fields.filter((field) => priorQuestionIds.has(field.id));
751
- const source = schema.fields.find((field) => field.id === page.displayCondition?.questionId);
752
- return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__page", children: [
753
- /* @__PURE__ */ jsx("legend", { children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }),
921
+ const setSourceText = (target, property, text) => executeAction(headless.setSourceText(target, property, text), {
922
+ action: "setSourceText",
923
+ ...target.id === void 0 ? {} : { targetId: target.id },
924
+ params: { kind: target.kind, property }
925
+ });
926
+ const updateOption = (fieldId, optionId, label) => executeAction(
927
+ headless.updateOption(fieldId, optionId, (option) => ({ ...option, label })),
928
+ { action: "updateOption", targetId: optionId, params: { fieldId } }
929
+ );
930
+ const addOption = (fieldId) => executeAction(headless.addOption(fieldId), { action: "addOption", targetId: fieldId });
931
+ const removeOption = (fieldId, optionId) => executeAction(headless.removeOption(fieldId, optionId), {
932
+ action: "removeOption",
933
+ targetId: optionId,
934
+ params: { fieldId }
935
+ });
936
+ const moveOption = (fieldId, optionId, targetIndex) => executeAction(headless.moveOption(fieldId, optionId, targetIndex), {
937
+ action: "moveOption",
938
+ targetId: optionId,
939
+ params: { fieldId, targetIndex }
940
+ });
941
+ const setDisplayCondition = (fieldId, condition) => executeAction(headless.setDisplayCondition(fieldId, condition), {
942
+ action: "setDisplayCondition",
943
+ targetId: fieldId,
944
+ ...condition === void 0 ? {} : { params: { condition } }
945
+ });
946
+ return /* @__PURE__ */ jsxs(
947
+ "section",
948
+ {
949
+ className: `form-engine-builder ${className}`.trim(),
950
+ "aria-label": translate("builder.formBuilder"),
951
+ onClickCapture: (event) => {
952
+ if (!(event.target instanceof HTMLElement)) return;
953
+ const actionTarget = event.target.closest("[data-builder-action]");
954
+ if (actionTarget?.dataset.builderAction === "addField" && maxFieldsReached && initialFieldType !== null)
955
+ addField();
956
+ if (actionTarget?.dataset.builderAction !== "addOption") return;
957
+ const fieldId = actionTarget.dataset.targetId;
958
+ const field = schema.fields.find((candidate) => candidate.id === fieldId);
959
+ if (fieldId !== void 0 && field !== void 0 && "options" in field && policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField) {
960
+ addOption(fieldId);
961
+ }
962
+ },
963
+ children: [
964
+ /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__pages", "aria-labelledby": "builder-pages-heading", children: [
965
+ /* @__PURE__ */ jsx("h2", { id: "builder-pages-heading", children: translate("builder.pages") }),
966
+ schema.pages === void 0 ? /* @__PURE__ */ jsx("button", { type: "button", onClick: enablePages, children: translate("builder.enablePages") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
967
+ schema.pages.map((page, pageIndex) => {
968
+ const priorQuestionIds = new Set(schema.pages?.slice(0, pageIndex).flatMap((item) => item.questionIds));
969
+ const availableSources = schema.fields.filter((field) => priorQuestionIds.has(field.id));
970
+ const source = schema.fields.find((field) => field.id === page.displayCondition?.questionId);
971
+ return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__page", children: [
972
+ /* @__PURE__ */ jsx("legend", { children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }),
973
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__toolbar", children: [
974
+ /* @__PURE__ */ jsx(
975
+ "button",
976
+ {
977
+ type: "button",
978
+ disabled: pageIndex === 0,
979
+ "aria-label": translate("builder.moveUp", { title: page.title ?? page.id }),
980
+ onClick: () => movePage(pageIndex, -1),
981
+ children: "\u2191"
982
+ }
983
+ ),
984
+ /* @__PURE__ */ jsx(
985
+ "button",
986
+ {
987
+ type: "button",
988
+ disabled: pageIndex === (schema.pages?.length ?? 0) - 1,
989
+ "aria-label": translate("builder.moveDown", { title: page.title ?? page.id }),
990
+ onClick: () => movePage(pageIndex, 1),
991
+ children: "\u2193"
992
+ }
993
+ ),
994
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removePage(pageIndex), children: translate("builder.deleteAction") })
995
+ ] }),
996
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
997
+ /* @__PURE__ */ jsxs("label", { children: [
998
+ translate("builder.pageTitle"),
999
+ /* @__PURE__ */ jsx(
1000
+ "input",
1001
+ {
1002
+ value: page.title ?? "",
1003
+ onChange: (event) => {
1004
+ const value = event.currentTarget.value;
1005
+ updatePage(page.id, (current) => {
1006
+ if (value.length > 0) return { ...current, title: value };
1007
+ const { title: _title, ...withoutTitle } = current;
1008
+ return withoutTitle;
1009
+ });
1010
+ }
1011
+ }
1012
+ )
1013
+ ] }),
1014
+ /* @__PURE__ */ jsxs("label", { children: [
1015
+ translate("builder.pageDescription"),
1016
+ /* @__PURE__ */ jsx(
1017
+ "input",
1018
+ {
1019
+ value: page.description ?? "",
1020
+ onChange: (event) => {
1021
+ const value = event.currentTarget.value;
1022
+ updatePage(page.id, (current) => {
1023
+ if (value.length > 0) return { ...current, description: value };
1024
+ const { description: _description, ...withoutDescription } = current;
1025
+ return withoutDescription;
1026
+ });
1027
+ }
1028
+ }
1029
+ )
1030
+ ] })
1031
+ ] }),
1032
+ editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
1033
+ /* @__PURE__ */ jsx("strong", { children: editingLocale }),
1034
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1035
+ /* @__PURE__ */ jsxs("label", { children: [
1036
+ translate("builder.pageTitle"),
1037
+ /* @__PURE__ */ jsx(
1038
+ "input",
1039
+ {
1040
+ value: page.translations?.[editingLocale]?.title ?? "",
1041
+ onChange: (event) => updateManualTranslation({
1042
+ locale: editingLocale,
1043
+ kind: "page",
1044
+ nodeId: page.id,
1045
+ property: "title",
1046
+ sourceText: page.title ?? "",
1047
+ translatedText: event.currentTarget.value,
1048
+ ...page.translationMetadata?.[editingLocale]?.title === void 0 ? {} : {
1049
+ existingTranslationMetadata: page.translationMetadata[editingLocale]?.title
1050
+ }
1051
+ })
1052
+ }
1053
+ )
1054
+ ] }),
1055
+ /* @__PURE__ */ jsxs("label", { children: [
1056
+ translate("builder.pageDescription"),
1057
+ /* @__PURE__ */ jsx(
1058
+ "input",
1059
+ {
1060
+ value: page.translations?.[editingLocale]?.description ?? "",
1061
+ onChange: (event) => updateManualTranslation({
1062
+ locale: editingLocale,
1063
+ kind: "page",
1064
+ nodeId: page.id,
1065
+ property: "description",
1066
+ sourceText: page.description ?? "",
1067
+ translatedText: event.currentTarget.value,
1068
+ ...page.translationMetadata?.[editingLocale]?.description === void 0 ? {} : {
1069
+ existingTranslationMetadata: page.translationMetadata[editingLocale]?.description
1070
+ }
1071
+ })
1072
+ }
1073
+ )
1074
+ ] })
1075
+ ] })
1076
+ ] }),
1077
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__condition", children: [
1078
+ /* @__PURE__ */ jsxs("label", { children: [
1079
+ translate("builder.pageCondition"),
1080
+ /* @__PURE__ */ jsxs(
1081
+ "select",
1082
+ {
1083
+ value: page.displayCondition?.questionId ?? "",
1084
+ onChange: (event) => {
1085
+ const selected = schema.fields.find((field) => field.id === event.currentTarget.value);
1086
+ updatePage(page.id, (current) => {
1087
+ if (selected === void 0) {
1088
+ const { displayCondition: _condition, ...withoutCondition } = current;
1089
+ return withoutCondition;
1090
+ }
1091
+ return {
1092
+ ...current,
1093
+ displayCondition: conditionWithValue(
1094
+ selected.id,
1095
+ conditionOperators(selected)[0] ?? "not_empty",
1096
+ defaultConditionValue(selected)
1097
+ )
1098
+ };
1099
+ });
1100
+ },
1101
+ children: [
1102
+ /* @__PURE__ */ jsx("option", { value: "", children: translate("builder.alwaysVisible") }),
1103
+ availableSources.map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
1104
+ ]
1105
+ }
1106
+ )
1107
+ ] }),
1108
+ page.displayCondition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1109
+ /* @__PURE__ */ jsx(
1110
+ "select",
1111
+ {
1112
+ "aria-label": translate("builder.conditionOperator"),
1113
+ value: page.displayCondition.operator,
1114
+ onChange: (event) => {
1115
+ const operator = event.currentTarget.value;
1116
+ updatePage(page.id, (current) => ({
1117
+ ...current,
1118
+ displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
1119
+ }));
1120
+ },
1121
+ children: conditionOperators(source).map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
1122
+ }
1123
+ ),
1124
+ /* @__PURE__ */ jsx(
1125
+ ConditionValueEditor,
1126
+ {
1127
+ source,
1128
+ condition: page.displayCondition,
1129
+ onChange: (condition) => updatePage(page.id, (current) => ({ ...current, displayCondition: condition })),
1130
+ translate
1131
+ }
1132
+ )
1133
+ ] }) : null
1134
+ ] })
1135
+ ] }, page.id);
1136
+ }),
1137
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__page-add", children: [
1138
+ /* @__PURE__ */ jsxs("label", { children: [
1139
+ translate("builder.pageQuestion"),
1140
+ /* @__PURE__ */ jsxs(
1141
+ "select",
1142
+ {
1143
+ value: newPageQuestionId,
1144
+ disabled: movablePageQuestions.length === 0,
1145
+ onChange: (event) => setNewPageQuestionId(event.currentTarget.value),
1146
+ children: [
1147
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
1148
+ schema.fields.filter((field) => movablePageQuestions.includes(field.id)).map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
1149
+ ]
1150
+ }
1151
+ )
1152
+ ] }),
1153
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: movablePageQuestions.length === 0, onClick: addPage, children: translate("builder.addPage") })
1154
+ ] })
1155
+ ] })
1156
+ ] }),
1157
+ /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__localization", "aria-labelledby": "builder-localization-heading", children: [
1158
+ /* @__PURE__ */ jsx("h2", { id: "builder-localization-heading", children: translate("builder.localization") }),
1159
+ /* @__PURE__ */ jsxs("label", { children: [
1160
+ translate("builder.completionMessage"),
1161
+ /* @__PURE__ */ jsx(
1162
+ "input",
1163
+ {
1164
+ value: schema.completionMessage ?? "",
1165
+ onChange: (event) => setSourceText({ kind: "form" }, "completionMessage", event.currentTarget.value)
1166
+ }
1167
+ )
1168
+ ] }),
1169
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1170
+ /* @__PURE__ */ jsxs("label", { children: [
1171
+ translate("builder.defaultLocale"),
1172
+ /* @__PURE__ */ jsx(
1173
+ "input",
1174
+ {
1175
+ value: schema.defaultLocale ?? "",
1176
+ onChange: (event) => headless.setDefaultLocale(event.currentTarget.value)
1177
+ }
1178
+ )
1179
+ ] }),
1180
+ /* @__PURE__ */ jsxs("label", { children: [
1181
+ translate("builder.addLocale"),
1182
+ /* @__PURE__ */ jsx("input", { value: newLocale, onChange: (event) => setNewLocale(event.currentTarget.value) })
1183
+ ] }),
1184
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: addLocale, children: translate("builder.addLocale") }),
1185
+ /* @__PURE__ */ jsxs("label", { children: [
1186
+ translate("builder.editLocale"),
1187
+ /* @__PURE__ */ jsxs("select", { value: editingLocale, onChange: (event) => setEditingLocale(event.currentTarget.value), children: [
1188
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
1189
+ (schema.supportedLocales ?? []).filter((item) => item !== schema.defaultLocale).map((item) => /* @__PURE__ */ jsx("option", { value: item, children: item }, item))
1190
+ ] })
1191
+ ] }),
1192
+ /* @__PURE__ */ jsx(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ disabled: translationAdapter === void 0 || editingLocale.length === 0 || isTranslating,
1197
+ onClick: () => void translateAll(),
1198
+ children: translate("builder.autoTranslate")
1199
+ }
1200
+ )
1201
+ ] }),
1202
+ translationAdapter === void 0 ? /* @__PURE__ */ jsx("p", { children: translate("builder.translationUnavailable") }) : null,
1203
+ translationError === null ? null : /* @__PURE__ */ jsx("p", { className: "form-engine-builder__error", children: translationError }),
1204
+ editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1205
+ /* @__PURE__ */ jsxs("label", { children: [
1206
+ translate("builder.questionTitle"),
1207
+ /* @__PURE__ */ jsx(
1208
+ "input",
1209
+ {
1210
+ value: schema.translations?.[editingLocale]?.title ?? "",
1211
+ onChange: (event) => updateFormTranslation("title", event.currentTarget.value)
1212
+ }
1213
+ )
1214
+ ] }),
1215
+ /* @__PURE__ */ jsxs("label", { children: [
1216
+ translate("builder.pageDescription"),
1217
+ /* @__PURE__ */ jsx(
1218
+ "input",
1219
+ {
1220
+ value: schema.translations?.[editingLocale]?.description ?? "",
1221
+ onChange: (event) => updateFormTranslation("description", event.currentTarget.value)
1222
+ }
1223
+ )
1224
+ ] }),
1225
+ /* @__PURE__ */ jsxs("label", { children: [
1226
+ translate("builder.completionMessage"),
1227
+ /* @__PURE__ */ jsx(
1228
+ "input",
1229
+ {
1230
+ value: schema.translations?.[editingLocale]?.completionMessage ?? "",
1231
+ onChange: (event) => updateFormTranslation("completionMessage", event.currentTarget.value)
1232
+ }
1233
+ )
1234
+ ] })
1235
+ ] })
1236
+ ] }),
1237
+ /* @__PURE__ */ jsx("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
1238
+ const condition = field.displayCondition;
1239
+ const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
1240
+ const availableSources = schema.fields.slice(0, index);
1241
+ return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__question", children: [
1242
+ /* @__PURE__ */ jsx("legend", { children: field.title }),
754
1243
  /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__toolbar", children: [
755
1244
  /* @__PURE__ */ jsx(
756
1245
  "button",
757
1246
  {
758
1247
  type: "button",
759
- disabled: pageIndex === 0,
760
- "aria-label": translate("builder.moveUp", { title: page.title ?? page.id }),
761
- onClick: () => movePage(pageIndex, -1),
1248
+ disabled: index === 0,
1249
+ onClick: () => moveField(index, -1),
1250
+ "aria-label": translate("builder.moveUp", { title: field.title }),
762
1251
  children: "\u2191"
763
1252
  }
764
1253
  ),
@@ -766,72 +1255,94 @@ function FormBuilder({
766
1255
  "button",
767
1256
  {
768
1257
  type: "button",
769
- disabled: pageIndex === (schema.pages?.length ?? 0) - 1,
770
- "aria-label": translate("builder.moveDown", { title: page.title ?? page.id }),
771
- onClick: () => movePage(pageIndex, 1),
1258
+ disabled: index === schema.fields.length - 1,
1259
+ onClick: () => moveField(index, 1),
1260
+ "aria-label": translate("builder.moveDown", { title: field.title }),
772
1261
  children: "\u2193"
773
1262
  }
774
1263
  ),
775
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removePage(pageIndex), children: translate("builder.deleteAction") })
1264
+ /* @__PURE__ */ jsx(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ disabled: schema.fields.length === 1,
1269
+ onClick: () => removeField(field.id),
1270
+ "aria-label": translate("builder.delete", { title: field.title }),
1271
+ children: translate("builder.deleteAction")
1272
+ }
1273
+ )
776
1274
  ] }),
777
1275
  /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
778
1276
  /* @__PURE__ */ jsxs("label", { children: [
779
- translate("builder.pageTitle"),
1277
+ translate("builder.questionTitle"),
780
1278
  /* @__PURE__ */ jsx(
781
1279
  "input",
782
1280
  {
783
- value: page.title ?? "",
784
- onChange: (event) => {
785
- const value = event.currentTarget.value;
786
- updatePage(page.id, (current) => {
787
- if (value.length > 0) return { ...current, title: value };
788
- const { title: _title, ...withoutTitle } = current;
789
- return withoutTitle;
790
- });
791
- }
1281
+ value: field.title,
1282
+ placeholder: translate("builder.questionTitlePlaceholder"),
1283
+ onChange: (event) => updateField(field.id, (current) => ({
1284
+ ...current,
1285
+ title: event.currentTarget.value.trim().length === 0 ? current.title : event.currentTarget.value
1286
+ }))
792
1287
  }
793
1288
  )
794
1289
  ] }),
795
1290
  /* @__PURE__ */ jsxs("label", { children: [
796
- translate("builder.pageDescription"),
1291
+ translate("builder.type"),
797
1292
  /* @__PURE__ */ jsx(
798
- "input",
1293
+ "select",
799
1294
  {
800
- value: page.description ?? "",
801
- onChange: (event) => {
802
- const value = event.currentTarget.value;
803
- updatePage(page.id, (current) => {
804
- if (value.length > 0) return { ...current, description: value };
805
- const { description: _description, ...withoutDescription } = current;
806
- return withoutDescription;
807
- });
808
- }
1295
+ value: field.type,
1296
+ onChange: (event) => changeType(field.id, event.currentTarget.value),
1297
+ children: FIELD_TYPES.filter(
1298
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
1299
+ ).map((type) => /* @__PURE__ */ jsx("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
809
1300
  }
810
1301
  )
1302
+ ] }),
1303
+ /* @__PURE__ */ jsxs("label", { className: "form-engine-builder__check", children: [
1304
+ /* @__PURE__ */ jsx(
1305
+ "input",
1306
+ {
1307
+ type: "checkbox",
1308
+ checked: field.required === true,
1309
+ onChange: (event) => updateField(field.id, (current) => ({ ...current, required: event.currentTarget.checked }))
1310
+ }
1311
+ ),
1312
+ translate("builder.required")
811
1313
  ] })
812
1314
  ] }),
1315
+ schema.pages === void 0 ? null : /* @__PURE__ */ jsxs("label", { children: [
1316
+ translate("builder.questionPage"),
1317
+ /* @__PURE__ */ jsx(
1318
+ "select",
1319
+ {
1320
+ value: pageForField(field.id)?.id ?? "",
1321
+ onChange: (event) => assignFieldToPage(field.id, event.currentTarget.value),
1322
+ children: schema.pages.map((page, pageIndex) => /* @__PURE__ */ jsx("option", { value: page.id, children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }, page.id))
1323
+ }
1324
+ )
1325
+ ] }),
813
1326
  editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
814
1327
  /* @__PURE__ */ jsx("strong", { children: editingLocale }),
815
1328
  /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
816
1329
  /* @__PURE__ */ jsxs("label", { children: [
817
- translate("builder.pageTitle"),
1330
+ translate("builder.questionTitle"),
818
1331
  /* @__PURE__ */ jsx(
819
1332
  "input",
820
1333
  {
821
- value: page.translations?.[editingLocale]?.title ?? "",
822
- onChange: (event) => {
823
- const value = event.currentTarget.value;
824
- updatePage(page.id, (current) => ({
825
- ...current,
826
- translations: {
827
- ...current.translations,
828
- [editingLocale]: {
829
- ...value.length === 0 ? {} : { title: value },
830
- ...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
831
- }
832
- }
833
- }));
834
- }
1334
+ value: field.translations?.[editingLocale]?.title ?? "",
1335
+ onChange: (event) => updateManualTranslation({
1336
+ locale: editingLocale,
1337
+ kind: "field",
1338
+ nodeId: field.id,
1339
+ property: "title",
1340
+ sourceText: field.title,
1341
+ translatedText: event.currentTarget.value,
1342
+ ...field.translationMetadata?.[editingLocale]?.title === void 0 ? {} : {
1343
+ existingTranslationMetadata: field.translationMetadata[editingLocale]?.title
1344
+ }
1345
+ })
835
1346
  }
836
1347
  )
837
1348
  ] }),
@@ -840,68 +1351,175 @@ function FormBuilder({
840
1351
  /* @__PURE__ */ jsx(
841
1352
  "input",
842
1353
  {
843
- value: page.translations?.[editingLocale]?.description ?? "",
844
- onChange: (event) => {
845
- const value = event.currentTarget.value;
846
- updatePage(page.id, (current) => ({
847
- ...current,
848
- translations: {
849
- ...current.translations,
850
- [editingLocale]: {
851
- ...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
852
- ...value.length === 0 ? {} : { description: value }
853
- }
854
- }
855
- }));
856
- }
1354
+ value: field.translations?.[editingLocale]?.description ?? "",
1355
+ onChange: (event) => updateManualTranslation({
1356
+ locale: editingLocale,
1357
+ kind: "field",
1358
+ nodeId: field.id,
1359
+ property: "description",
1360
+ sourceText: field.description ?? "",
1361
+ translatedText: event.currentTarget.value,
1362
+ ...field.translationMetadata?.[editingLocale]?.description === void 0 ? {} : {
1363
+ existingTranslationMetadata: field.translationMetadata[editingLocale]?.description
1364
+ }
1365
+ })
857
1366
  }
858
1367
  )
859
1368
  ] })
860
- ] })
1369
+ ] }),
1370
+ "options" in field ? field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("label", { children: [
1371
+ translate("builder.optionLabel", { index: optionIndex + 1 }),
1372
+ " (",
1373
+ editingLocale,
1374
+ ")",
1375
+ /* @__PURE__ */ jsx(
1376
+ "input",
1377
+ {
1378
+ value: option.translations?.[editingLocale] ?? "",
1379
+ onChange: (event) => updateManualTranslation({
1380
+ locale: editingLocale,
1381
+ kind: "option",
1382
+ nodeId: option.id,
1383
+ property: "label",
1384
+ sourceText: option.label,
1385
+ translatedText: event.currentTarget.value,
1386
+ ...option.translationMetadata?.[editingLocale]?.label === void 0 ? {} : {
1387
+ existingTranslationMetadata: option.translationMetadata[editingLocale]?.label
1388
+ }
1389
+ })
1390
+ }
1391
+ )
1392
+ ] }, option.id)) : null
861
1393
  ] }),
1394
+ field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1395
+ /* @__PURE__ */ jsxs("label", { children: [
1396
+ translate("builder.minimum"),
1397
+ /* @__PURE__ */ jsx(
1398
+ "input",
1399
+ {
1400
+ type: "number",
1401
+ value: field.min ?? 1,
1402
+ onChange: (event) => {
1403
+ const min = event.currentTarget.valueAsNumber;
1404
+ if (!Number.isInteger(min)) return;
1405
+ updateField(
1406
+ field.id,
1407
+ (current) => current.type === "rating" ? { ...current, min, max: Math.max(min, current.max ?? 5) } : current
1408
+ );
1409
+ }
1410
+ }
1411
+ )
1412
+ ] }),
1413
+ /* @__PURE__ */ jsxs("label", { children: [
1414
+ translate("builder.maximum"),
1415
+ /* @__PURE__ */ jsx(
1416
+ "input",
1417
+ {
1418
+ type: "number",
1419
+ value: field.max ?? 5,
1420
+ onChange: (event) => {
1421
+ const max = event.currentTarget.valueAsNumber;
1422
+ if (!Number.isInteger(max)) return;
1423
+ updateField(
1424
+ field.id,
1425
+ (current) => current.type === "rating" ? { ...current, min: Math.min(current.min ?? 1, max), max } : current
1426
+ );
1427
+ }
1428
+ }
1429
+ )
1430
+ ] })
1431
+ ] }) : null,
1432
+ "options" in field ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__options", children: [
1433
+ /* @__PURE__ */ jsx("strong", { children: translate("builder.options") }),
1434
+ field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__option", children: [
1435
+ /* @__PURE__ */ jsx(
1436
+ "input",
1437
+ {
1438
+ "aria-label": translate("builder.optionLabel", { index: optionIndex + 1 }),
1439
+ value: option.label,
1440
+ placeholder: translate("builder.optionLabelPlaceholder"),
1441
+ onChange: (event) => event.currentTarget.value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, event.currentTarget.value)
1442
+ }
1443
+ ),
1444
+ /* @__PURE__ */ jsx(
1445
+ "button",
1446
+ {
1447
+ type: "button",
1448
+ disabled: optionIndex === 0,
1449
+ "aria-label": translate("builder.moveUp", { title: option.label }),
1450
+ onClick: () => moveOption(field.id, option.id, optionIndex - 1),
1451
+ children: "\u2191"
1452
+ }
1453
+ ),
1454
+ /* @__PURE__ */ jsx(
1455
+ "button",
1456
+ {
1457
+ type: "button",
1458
+ disabled: optionIndex === field.options.length - 1,
1459
+ "aria-label": translate("builder.moveDown", { title: option.label }),
1460
+ onClick: () => moveOption(field.id, option.id, optionIndex + 1),
1461
+ children: "\u2193"
1462
+ }
1463
+ ),
1464
+ /* @__PURE__ */ jsx(
1465
+ "button",
1466
+ {
1467
+ type: "button",
1468
+ disabled: field.options.length === 1,
1469
+ onClick: () => removeOption(field.id, option.id),
1470
+ children: translate("builder.remove")
1471
+ }
1472
+ )
1473
+ ] }, option.id)),
1474
+ /* @__PURE__ */ jsx(
1475
+ "button",
1476
+ {
1477
+ type: "button",
1478
+ "data-builder-action": "addOption",
1479
+ "data-target-id": field.id,
1480
+ disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
1481
+ onClick: () => addOption(field.id),
1482
+ children: translate("builder.addOption")
1483
+ }
1484
+ )
1485
+ ] }) : null,
862
1486
  /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__condition", children: [
863
1487
  /* @__PURE__ */ jsxs("label", { children: [
864
- translate("builder.pageCondition"),
1488
+ translate("builder.displayCondition"),
865
1489
  /* @__PURE__ */ jsxs(
866
1490
  "select",
867
1491
  {
868
- value: page.displayCondition?.questionId ?? "",
1492
+ value: condition?.questionId ?? "",
869
1493
  onChange: (event) => {
870
- const selected = schema.fields.find((field) => field.id === event.currentTarget.value);
871
- updatePage(page.id, (current) => {
872
- if (selected === void 0) {
873
- const { displayCondition: _condition, ...withoutCondition } = current;
874
- return withoutCondition;
875
- }
876
- return {
877
- ...current,
878
- displayCondition: conditionWithValue(
879
- selected.id,
880
- conditionOperators(selected)[0] ?? "not_empty",
881
- defaultConditionValue(selected)
882
- )
883
- };
884
- });
1494
+ const selected = schema.fields.find((item) => item.id === event.currentTarget.value);
1495
+ setDisplayCondition(
1496
+ field.id,
1497
+ selected === void 0 ? void 0 : conditionWithValue(
1498
+ selected.id,
1499
+ conditionOperators(selected)[0] ?? "not_empty",
1500
+ defaultConditionValue(selected)
1501
+ )
1502
+ );
885
1503
  },
886
1504
  children: [
887
1505
  /* @__PURE__ */ jsx("option", { value: "", children: translate("builder.alwaysVisible") }),
888
- availableSources.map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
1506
+ availableSources.map((candidate) => /* @__PURE__ */ jsx("option", { value: candidate.id, children: candidate.title }, candidate.id))
889
1507
  ]
890
1508
  }
891
1509
  )
892
1510
  ] }),
893
- page.displayCondition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1511
+ condition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
894
1512
  /* @__PURE__ */ jsx(
895
1513
  "select",
896
1514
  {
897
1515
  "aria-label": translate("builder.conditionOperator"),
898
- value: page.displayCondition.operator,
1516
+ value: condition.operator,
899
1517
  onChange: (event) => {
900
1518
  const operator = event.currentTarget.value;
901
- updatePage(page.id, (current) => ({
902
- ...current,
903
- displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
904
- }));
1519
+ setDisplayCondition(
1520
+ field.id,
1521
+ conditionWithValue(source.id, operator, defaultConditionValue(source))
1522
+ );
905
1523
  },
906
1524
  children: conditionOperators(source).map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
907
1525
  }
@@ -910,422 +1528,29 @@ function FormBuilder({
910
1528
  ConditionValueEditor,
911
1529
  {
912
1530
  source,
913
- condition: page.displayCondition,
914
- onChange: (condition) => updatePage(page.id, (current) => ({ ...current, displayCondition: condition })),
1531
+ condition,
1532
+ onChange: (next) => setDisplayCondition(field.id, next),
915
1533
  translate
916
1534
  }
917
1535
  )
918
1536
  ] }) : null
919
1537
  ] })
920
- ] }, page.id);
921
- }),
922
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__page-add", children: [
923
- /* @__PURE__ */ jsxs("label", { children: [
924
- translate("builder.pageQuestion"),
925
- /* @__PURE__ */ jsxs(
926
- "select",
927
- {
928
- value: newPageQuestionId,
929
- disabled: movablePageQuestions.length === 0,
930
- onChange: (event) => setNewPageQuestionId(event.currentTarget.value),
931
- children: [
932
- /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
933
- schema.fields.filter((field) => movablePageQuestions.includes(field.id)).map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
934
- ]
935
- }
936
- )
937
- ] }),
938
- /* @__PURE__ */ jsx("button", { type: "button", disabled: movablePageQuestions.length === 0, onClick: addPage, children: translate("builder.addPage") })
939
- ] })
940
- ] })
941
- ] }),
942
- /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__localization", "aria-labelledby": "builder-localization-heading", children: [
943
- /* @__PURE__ */ jsx("h2", { id: "builder-localization-heading", children: translate("builder.localization") }),
944
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
945
- /* @__PURE__ */ jsxs("label", { children: [
946
- translate("builder.defaultLocale"),
947
- /* @__PURE__ */ jsx(
948
- "input",
949
- {
950
- value: schema.defaultLocale ?? "",
951
- onChange: (event) => {
952
- const value = event.currentTarget.value.trim();
953
- emitSchema(
954
- value.length === 0 ? schema : {
955
- ...schema,
956
- defaultLocale: value,
957
- supportedLocales: [.../* @__PURE__ */ new Set([value, ...schema.supportedLocales ?? []])]
958
- }
959
- );
960
- }
961
- }
962
- )
963
- ] }),
964
- /* @__PURE__ */ jsxs("label", { children: [
965
- translate("builder.addLocale"),
966
- /* @__PURE__ */ jsx("input", { value: newLocale, onChange: (event) => setNewLocale(event.currentTarget.value) })
967
- ] }),
968
- /* @__PURE__ */ jsx("button", { type: "button", onClick: addLocale, children: translate("builder.addLocale") }),
969
- /* @__PURE__ */ jsxs("label", { children: [
970
- translate("builder.editLocale"),
971
- /* @__PURE__ */ jsxs("select", { value: editingLocale, onChange: (event) => setEditingLocale(event.currentTarget.value), children: [
972
- /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
973
- (schema.supportedLocales ?? []).filter((item) => item !== schema.defaultLocale).map((item) => /* @__PURE__ */ jsx("option", { value: item, children: item }, item))
974
- ] })
975
- ] }),
1538
+ ] }, field.id);
1539
+ }) }),
976
1540
  /* @__PURE__ */ jsx(
977
1541
  "button",
978
1542
  {
1543
+ className: "form-engine-builder__add",
979
1544
  type: "button",
980
- disabled: translationAdapter === void 0 || editingLocale.length === 0 || isTranslating,
981
- onClick: () => void translateAll(),
982
- children: translate("builder.autoTranslate")
1545
+ "data-builder-action": "addField",
1546
+ disabled: initialFieldType === null || maxFieldsReached,
1547
+ onClick: addField,
1548
+ children: translate("builder.addQuestion")
983
1549
  }
984
1550
  )
985
- ] }),
986
- translationAdapter === void 0 ? /* @__PURE__ */ jsx("p", { children: translate("builder.translationUnavailable") }) : null,
987
- translationError === null ? null : /* @__PURE__ */ jsx("p", { className: "form-engine-builder__error", children: translationError }),
988
- editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
989
- /* @__PURE__ */ jsxs("label", { children: [
990
- translate("builder.questionTitle"),
991
- /* @__PURE__ */ jsx(
992
- "input",
993
- {
994
- value: schema.translations?.[editingLocale]?.title ?? "",
995
- onChange: (event) => updateFormTranslation("title", event.currentTarget.value)
996
- }
997
- )
998
- ] }),
999
- /* @__PURE__ */ jsxs("label", { children: [
1000
- translate("builder.pageDescription"),
1001
- /* @__PURE__ */ jsx(
1002
- "input",
1003
- {
1004
- value: schema.translations?.[editingLocale]?.description ?? "",
1005
- onChange: (event) => updateFormTranslation("description", event.currentTarget.value)
1006
- }
1007
- )
1008
- ] })
1009
- ] })
1010
- ] }),
1011
- /* @__PURE__ */ jsx("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
1012
- const condition = field.displayCondition;
1013
- const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
1014
- const availableSources = schema.fields.slice(0, index);
1015
- return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__question", children: [
1016
- /* @__PURE__ */ jsx("legend", { children: field.title }),
1017
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__toolbar", children: [
1018
- /* @__PURE__ */ jsx(
1019
- "button",
1020
- {
1021
- type: "button",
1022
- disabled: index === 0,
1023
- onClick: () => moveField(index, -1),
1024
- "aria-label": translate("builder.moveUp", { title: field.title }),
1025
- children: "\u2191"
1026
- }
1027
- ),
1028
- /* @__PURE__ */ jsx(
1029
- "button",
1030
- {
1031
- type: "button",
1032
- disabled: index === schema.fields.length - 1,
1033
- onClick: () => moveField(index, 1),
1034
- "aria-label": translate("builder.moveDown", { title: field.title }),
1035
- children: "\u2193"
1036
- }
1037
- ),
1038
- /* @__PURE__ */ jsx(
1039
- "button",
1040
- {
1041
- type: "button",
1042
- disabled: schema.fields.length === 1,
1043
- onClick: () => removeField(field.id),
1044
- "aria-label": translate("builder.delete", { title: field.title }),
1045
- children: translate("builder.deleteAction")
1046
- }
1047
- )
1048
- ] }),
1049
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1050
- /* @__PURE__ */ jsxs("label", { children: [
1051
- translate("builder.questionTitle"),
1052
- /* @__PURE__ */ jsx(
1053
- "input",
1054
- {
1055
- value: field.title,
1056
- placeholder: translate("builder.questionTitlePlaceholder"),
1057
- onChange: (event) => updateField(field.id, (current) => ({
1058
- ...current,
1059
- title: event.currentTarget.value.trim().length === 0 ? current.title : event.currentTarget.value
1060
- }))
1061
- }
1062
- )
1063
- ] }),
1064
- /* @__PURE__ */ jsxs("label", { children: [
1065
- translate("builder.type"),
1066
- /* @__PURE__ */ jsx(
1067
- "select",
1068
- {
1069
- value: field.type,
1070
- onChange: (event) => changeType(field.id, event.currentTarget.value),
1071
- children: FIELD_TYPES.filter(
1072
- (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
1073
- ).map((type) => /* @__PURE__ */ jsx("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
1074
- }
1075
- )
1076
- ] }),
1077
- /* @__PURE__ */ jsxs("label", { className: "form-engine-builder__check", children: [
1078
- /* @__PURE__ */ jsx(
1079
- "input",
1080
- {
1081
- type: "checkbox",
1082
- checked: field.required === true,
1083
- onChange: (event) => updateField(field.id, (current) => ({ ...current, required: event.currentTarget.checked }))
1084
- }
1085
- ),
1086
- translate("builder.required")
1087
- ] })
1088
- ] }),
1089
- schema.pages === void 0 ? null : /* @__PURE__ */ jsxs("label", { children: [
1090
- translate("builder.questionPage"),
1091
- /* @__PURE__ */ jsx(
1092
- "select",
1093
- {
1094
- value: pageForField(field.id)?.id ?? "",
1095
- onChange: (event) => assignFieldToPage(field.id, event.currentTarget.value),
1096
- children: schema.pages.map((page, pageIndex) => /* @__PURE__ */ jsx("option", { value: page.id, children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }, page.id))
1097
- }
1098
- )
1099
- ] }),
1100
- editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
1101
- /* @__PURE__ */ jsx("strong", { children: editingLocale }),
1102
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1103
- /* @__PURE__ */ jsxs("label", { children: [
1104
- translate("builder.questionTitle"),
1105
- /* @__PURE__ */ jsx(
1106
- "input",
1107
- {
1108
- value: field.translations?.[editingLocale]?.title ?? "",
1109
- onChange: (event) => {
1110
- const value = event.currentTarget.value;
1111
- updateField(field.id, (current) => ({
1112
- ...current,
1113
- translations: {
1114
- ...current.translations,
1115
- [editingLocale]: {
1116
- ...value.length === 0 ? {} : { title: value },
1117
- ...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
1118
- }
1119
- }
1120
- }));
1121
- }
1122
- }
1123
- )
1124
- ] }),
1125
- /* @__PURE__ */ jsxs("label", { children: [
1126
- translate("builder.pageDescription"),
1127
- /* @__PURE__ */ jsx(
1128
- "input",
1129
- {
1130
- value: field.translations?.[editingLocale]?.description ?? "",
1131
- onChange: (event) => {
1132
- const value = event.currentTarget.value;
1133
- updateField(field.id, (current) => ({
1134
- ...current,
1135
- translations: {
1136
- ...current.translations,
1137
- [editingLocale]: {
1138
- ...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
1139
- ...value.length === 0 ? {} : { description: value }
1140
- }
1141
- }
1142
- }));
1143
- }
1144
- }
1145
- )
1146
- ] })
1147
- ] }),
1148
- "options" in field ? field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("label", { children: [
1149
- translate("builder.optionLabel", { index: optionIndex + 1 }),
1150
- " (",
1151
- editingLocale,
1152
- ")",
1153
- /* @__PURE__ */ jsx(
1154
- "input",
1155
- {
1156
- value: option.translations?.[editingLocale] ?? "",
1157
- onChange: (event) => {
1158
- const value = event.currentTarget.value;
1159
- updateField(field.id, (current) => {
1160
- if (!("options" in current)) return current;
1161
- return {
1162
- ...current,
1163
- options: current.options.map(
1164
- (candidate) => candidate.id === option.id ? {
1165
- ...candidate,
1166
- translations: Object.fromEntries([
1167
- ...Object.entries(candidate.translations ?? {}).filter(
1168
- ([localeKey]) => localeKey !== editingLocale
1169
- ),
1170
- ...value.length === 0 ? [] : [[editingLocale, value]]
1171
- ])
1172
- } : candidate
1173
- )
1174
- };
1175
- });
1176
- }
1177
- }
1178
- )
1179
- ] }, option.id)) : null
1180
- ] }),
1181
- field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1182
- /* @__PURE__ */ jsxs("label", { children: [
1183
- translate("builder.minimum"),
1184
- /* @__PURE__ */ jsx(
1185
- "input",
1186
- {
1187
- type: "number",
1188
- value: field.min ?? 1,
1189
- onChange: (event) => {
1190
- const min = event.currentTarget.valueAsNumber;
1191
- if (!Number.isInteger(min)) return;
1192
- updateField(
1193
- field.id,
1194
- (current) => current.type === "rating" ? { ...current, min, max: Math.max(min, current.max ?? 5) } : current
1195
- );
1196
- }
1197
- }
1198
- )
1199
- ] }),
1200
- /* @__PURE__ */ jsxs("label", { children: [
1201
- translate("builder.maximum"),
1202
- /* @__PURE__ */ jsx(
1203
- "input",
1204
- {
1205
- type: "number",
1206
- value: field.max ?? 5,
1207
- onChange: (event) => {
1208
- const max = event.currentTarget.valueAsNumber;
1209
- if (!Number.isInteger(max)) return;
1210
- updateField(
1211
- field.id,
1212
- (current) => current.type === "rating" ? { ...current, min: Math.min(current.min ?? 1, max), max } : current
1213
- );
1214
- }
1215
- }
1216
- )
1217
- ] })
1218
- ] }) : null,
1219
- "options" in field ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__options", children: [
1220
- /* @__PURE__ */ jsx("strong", { children: translate("builder.options") }),
1221
- field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__option", children: [
1222
- /* @__PURE__ */ jsx(
1223
- "input",
1224
- {
1225
- "aria-label": translate("builder.optionLabel", { index: optionIndex + 1 }),
1226
- value: option.label,
1227
- placeholder: translate("builder.optionLabelPlaceholder"),
1228
- onChange: (event) => updateField(field.id, (current) => {
1229
- if (!("options" in current)) return current;
1230
- const label = event.currentTarget.value;
1231
- if (label.trim().length === 0) return current;
1232
- return {
1233
- ...current,
1234
- options: current.options.map(
1235
- (item, itemIndex) => itemIndex === optionIndex ? { ...item, label } : item
1236
- )
1237
- };
1238
- })
1239
- }
1240
- ),
1241
- /* @__PURE__ */ jsx(
1242
- "button",
1243
- {
1244
- type: "button",
1245
- disabled: field.options.length === 1,
1246
- onClick: () => headless.removeOption(field.id, option.id),
1247
- children: translate("builder.remove")
1248
- }
1249
- )
1250
- ] }, option.id)),
1251
- /* @__PURE__ */ jsx(
1252
- "button",
1253
- {
1254
- type: "button",
1255
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
1256
- onClick: () => headless.addOption(field.id),
1257
- children: translate("builder.addOption")
1258
- }
1259
- )
1260
- ] }) : null,
1261
- /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__condition", children: [
1262
- /* @__PURE__ */ jsxs("label", { children: [
1263
- translate("builder.displayCondition"),
1264
- /* @__PURE__ */ jsxs(
1265
- "select",
1266
- {
1267
- value: condition?.questionId ?? "",
1268
- onChange: (event) => {
1269
- const selected = schema.fields.find((item) => item.id === event.currentTarget.value);
1270
- updateField(field.id, (current) => {
1271
- if (selected === void 0) {
1272
- const { displayCondition: _condition, ...withoutCondition } = current;
1273
- return withoutCondition;
1274
- }
1275
- const operator = conditionOperators(selected)[0] ?? "not_empty";
1276
- return {
1277
- ...current,
1278
- displayCondition: conditionWithValue(selected.id, operator, defaultConditionValue(selected))
1279
- };
1280
- });
1281
- },
1282
- children: [
1283
- /* @__PURE__ */ jsx("option", { value: "", children: translate("builder.alwaysVisible") }),
1284
- availableSources.map((candidate) => /* @__PURE__ */ jsx("option", { value: candidate.id, children: candidate.title }, candidate.id))
1285
- ]
1286
- }
1287
- )
1288
- ] }),
1289
- condition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1290
- /* @__PURE__ */ jsx(
1291
- "select",
1292
- {
1293
- "aria-label": translate("builder.conditionOperator"),
1294
- value: condition.operator,
1295
- onChange: (event) => {
1296
- const operator = event.currentTarget.value;
1297
- updateField(field.id, (current) => ({
1298
- ...current,
1299
- displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
1300
- }));
1301
- },
1302
- children: conditionOperators(source).map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
1303
- }
1304
- ),
1305
- /* @__PURE__ */ jsx(
1306
- ConditionValueEditor,
1307
- {
1308
- source,
1309
- condition,
1310
- onChange: (next) => updateField(field.id, (current) => ({ ...current, displayCondition: next })),
1311
- translate
1312
- }
1313
- )
1314
- ] }) : null
1315
- ] })
1316
- ] }, field.id);
1317
- }) }),
1318
- /* @__PURE__ */ jsx(
1319
- "button",
1320
- {
1321
- className: "form-engine-builder__add",
1322
- type: "button",
1323
- disabled: policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields,
1324
- onClick: addField,
1325
- children: translate("builder.addQuestion")
1326
- }
1327
- )
1328
- ] });
1551
+ ]
1552
+ }
1553
+ );
1329
1554
  }
1330
1555
 
1331
1556
  // src/context.tsx
@@ -1859,8 +2084,14 @@ function ContextFormRenderer({
1859
2084
  ] }),
1860
2085
  draftRestored ? /* @__PURE__ */ jsx3("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
1861
2086
  ] }),
1862
- activePage?.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
1863
- activePage?.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description }),
2087
+ activePage === void 0 ? null : slots.renderPageHeader?.({
2088
+ page: activePage,
2089
+ pageIndex: activeVisibleIndex,
2090
+ totalPages: visiblePageIndexes.length
2091
+ }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-page-header", children: [
2092
+ activePage.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
2093
+ activePage.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description })
2094
+ ] }),
1864
2095
  /* @__PURE__ */ jsx3("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
1865
2096
  const error = form.errors[field.id];
1866
2097
  const props = {
@@ -1928,7 +2159,7 @@ function ContextFormRenderer({
1928
2159
  form.submitStatus === "success" ? slots.renderCompletion?.({
1929
2160
  message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
1930
2161
  }) ?? /* @__PURE__ */ jsx3("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
1931
- form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
2162
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (errorMessageKey === void 0 ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
1932
2163
  ] })
1933
2164
  ] });
1934
2165
  }
@@ -1976,6 +2207,7 @@ export {
1976
2207
  FormBuilder,
1977
2208
  FormProvider,
1978
2209
  FormRenderer,
2210
+ resolveInitialFieldType,
1979
2211
  useField,
1980
2212
  useForm,
1981
2213
  useFormBuilder