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