@form-engine-ts/react 4.1.0 → 4.2.0

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
@@ -169,6 +169,61 @@ function defaultCreateOption(field, id) {
169
169
  function defaultCreatePage(id, questionIds) {
170
170
  return { id, title: "New page", questionIds };
171
171
  }
172
+ function applyFieldConstraintDefaults(field, policy) {
173
+ const constraint = policy?.fieldConstraints?.[field.type];
174
+ if (constraint === void 0) return field;
175
+ const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
176
+ if (field.type === "rating") {
177
+ const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
178
+ const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
179
+ const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
180
+ return {
181
+ ...field,
182
+ required,
183
+ ...min === void 0 ? {} : { min },
184
+ ...max === void 0 ? {} : { max }
185
+ };
186
+ }
187
+ if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
188
+ return {
189
+ ...field,
190
+ required,
191
+ ...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
192
+ };
193
+ }
194
+ return { ...field, required };
195
+ }
196
+ function fieldConstraintError(updated, policy) {
197
+ const constraint = policy?.fieldConstraints?.[updated.type];
198
+ if (constraint === void 0) return void 0;
199
+ if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
200
+ return { type: "field_constraint_immutable" };
201
+ }
202
+ if (updated.type === "rating") {
203
+ if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
204
+ return { type: "field_constraint_immutable" };
205
+ }
206
+ if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
207
+ return { type: "field_constraint_immutable" };
208
+ }
209
+ if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
210
+ return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
211
+ }
212
+ if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
213
+ return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
214
+ }
215
+ }
216
+ if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
217
+ return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
218
+ }
219
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
220
+ return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
221
+ }
222
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
223
+ return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
224
+ }
225
+ return void 0;
226
+ }
172
227
  function move(items, sourceIndex, targetIndex) {
173
228
  if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
174
229
  return void 0;
@@ -224,6 +279,8 @@ function useFormBuilder({
224
279
  const updated = updater(current);
225
280
  if (updated.id !== fieldId)
226
281
  return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
282
+ const constraintError = fieldConstraintError(updated, policy);
283
+ if (constraintError !== void 0) return { success: false, error: constraintError };
227
284
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
228
285
  return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
229
286
  for (const text of [updated.title, updated.description]) {
@@ -235,7 +292,7 @@ function useFormBuilder({
235
292
  onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
236
293
  return { success: true };
237
294
  },
238
- [onChange, policy?.allowedFieldTypes, schema, textPolicyError]
295
+ [onChange, policy, schema, textPolicyError]
239
296
  );
240
297
  const updateOption = (0, import_react.useCallback)(
241
298
  (fieldId, optionId, updater) => {
@@ -308,6 +365,7 @@ function useFormBuilder({
308
365
  return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
309
366
  field = { ...field, options: [option] };
310
367
  }
368
+ field = applyFieldConstraintDefaults(field, policy);
311
369
  const pages = schema.pages?.map((page, index) => ({
312
370
  ...page,
313
371
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
@@ -360,6 +418,12 @@ function useFormBuilder({
360
418
  return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
361
419
  if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
362
420
  return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
421
+ const constraint = policy?.fieldConstraints?.[field.type];
422
+ if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
423
+ return {
424
+ success: false,
425
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
426
+ };
363
427
  const ids = new Set(
364
428
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
365
429
  );
@@ -376,7 +440,7 @@ function useFormBuilder({
376
440
  });
377
441
  return { success: true };
378
442
  },
379
- [createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
443
+ [createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
380
444
  );
381
445
  const removeOption = (0, import_react.useCallback)(
382
446
  (fieldId, optionId) => {
@@ -386,6 +450,12 @@ function useFormBuilder({
386
450
  return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
387
451
  if (field.options.length <= 1)
388
452
  return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
453
+ const constraint = policy?.fieldConstraints?.[field.type];
454
+ if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
455
+ return {
456
+ success: false,
457
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
458
+ };
389
459
  onChange({
390
460
  ...schema,
391
461
  fields: schema.fields.map(
@@ -394,7 +464,7 @@ function useFormBuilder({
394
464
  });
395
465
  return { success: true };
396
466
  },
397
- [onChange, schema]
467
+ [onChange, policy, schema]
398
468
  );
399
469
  const moveOption = (0, import_react.useCallback)(
400
470
  (fieldId, optionId, targetIndex) => {
@@ -425,7 +495,9 @@ function useFormBuilder({
425
495
  if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
426
496
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
427
497
  return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
428
- let transformed = (0, import_core.transformFieldType)(field, type);
498
+ let transformed = applyFieldConstraintDefaults((0, import_core.transformFieldType)(field, type), policy);
499
+ const constraintError = fieldConstraintError(transformed, policy);
500
+ if (constraintError !== void 0) return { success: false, error: constraintError };
429
501
  if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
430
502
  const ids = new Set(
431
503
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
@@ -440,7 +512,7 @@ function useFormBuilder({
440
512
  onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
441
513
  return { success: true };
442
514
  },
443
- [createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
515
+ [createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
444
516
  );
445
517
  const addPage = (0, import_react.useCallback)(
446
518
  (questionId) => {
package/dist/index.d.cts CHANGED
@@ -60,6 +60,12 @@ type BuilderActionError = {
60
60
  } | {
61
61
  readonly type: "max_locales_exceeded";
62
62
  readonly max: number;
63
+ } | {
64
+ readonly type: "field_constraint_immutable";
65
+ } | {
66
+ readonly type: "field_constraint_violation";
67
+ readonly property: string;
68
+ readonly expected: number;
63
69
  } | {
64
70
  readonly type: "node_not_found";
65
71
  readonly kind: BuilderTextTarget["kind"];
package/dist/index.d.ts CHANGED
@@ -60,6 +60,12 @@ type BuilderActionError = {
60
60
  } | {
61
61
  readonly type: "max_locales_exceeded";
62
62
  readonly max: number;
63
+ } | {
64
+ readonly type: "field_constraint_immutable";
65
+ } | {
66
+ readonly type: "field_constraint_violation";
67
+ readonly property: string;
68
+ readonly expected: number;
63
69
  } | {
64
70
  readonly type: "node_not_found";
65
71
  readonly kind: BuilderTextTarget["kind"];
package/dist/index.js CHANGED
@@ -133,6 +133,61 @@ function defaultCreateOption(field, id) {
133
133
  function defaultCreatePage(id, questionIds) {
134
134
  return { id, title: "New page", questionIds };
135
135
  }
136
+ function applyFieldConstraintDefaults(field, policy) {
137
+ const constraint = policy?.fieldConstraints?.[field.type];
138
+ if (constraint === void 0) return field;
139
+ const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
140
+ if (field.type === "rating") {
141
+ const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
142
+ const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
143
+ const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
144
+ return {
145
+ ...field,
146
+ required,
147
+ ...min === void 0 ? {} : { min },
148
+ ...max === void 0 ? {} : { max }
149
+ };
150
+ }
151
+ if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
152
+ return {
153
+ ...field,
154
+ required,
155
+ ...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
156
+ };
157
+ }
158
+ return { ...field, required };
159
+ }
160
+ function fieldConstraintError(updated, policy) {
161
+ const constraint = policy?.fieldConstraints?.[updated.type];
162
+ if (constraint === void 0) return void 0;
163
+ if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
164
+ return { type: "field_constraint_immutable" };
165
+ }
166
+ if (updated.type === "rating") {
167
+ if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
168
+ return { type: "field_constraint_immutable" };
169
+ }
170
+ if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
171
+ return { type: "field_constraint_immutable" };
172
+ }
173
+ if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
174
+ return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
175
+ }
176
+ if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
177
+ return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
178
+ }
179
+ }
180
+ if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
181
+ return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
182
+ }
183
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
184
+ return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
185
+ }
186
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
187
+ return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
188
+ }
189
+ return void 0;
190
+ }
136
191
  function move(items, sourceIndex, targetIndex) {
137
192
  if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
138
193
  return void 0;
@@ -188,6 +243,8 @@ function useFormBuilder({
188
243
  const updated = updater(current);
189
244
  if (updated.id !== fieldId)
190
245
  return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
246
+ const constraintError = fieldConstraintError(updated, policy);
247
+ if (constraintError !== void 0) return { success: false, error: constraintError };
191
248
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
192
249
  return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
193
250
  for (const text of [updated.title, updated.description]) {
@@ -199,7 +256,7 @@ function useFormBuilder({
199
256
  onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
200
257
  return { success: true };
201
258
  },
202
- [onChange, policy?.allowedFieldTypes, schema, textPolicyError]
259
+ [onChange, policy, schema, textPolicyError]
203
260
  );
204
261
  const updateOption = useCallback(
205
262
  (fieldId, optionId, updater) => {
@@ -272,6 +329,7 @@ function useFormBuilder({
272
329
  return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
273
330
  field = { ...field, options: [option] };
274
331
  }
332
+ field = applyFieldConstraintDefaults(field, policy);
275
333
  const pages = schema.pages?.map((page, index) => ({
276
334
  ...page,
277
335
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
@@ -324,6 +382,12 @@ function useFormBuilder({
324
382
  return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
325
383
  if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
326
384
  return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
385
+ const constraint = policy?.fieldConstraints?.[field.type];
386
+ if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
387
+ return {
388
+ success: false,
389
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
390
+ };
327
391
  const ids = new Set(
328
392
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
329
393
  );
@@ -340,7 +404,7 @@ function useFormBuilder({
340
404
  });
341
405
  return { success: true };
342
406
  },
343
- [createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
407
+ [createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
344
408
  );
345
409
  const removeOption = useCallback(
346
410
  (fieldId, optionId) => {
@@ -350,6 +414,12 @@ function useFormBuilder({
350
414
  return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
351
415
  if (field.options.length <= 1)
352
416
  return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
417
+ const constraint = policy?.fieldConstraints?.[field.type];
418
+ if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
419
+ return {
420
+ success: false,
421
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
422
+ };
353
423
  onChange({
354
424
  ...schema,
355
425
  fields: schema.fields.map(
@@ -358,7 +428,7 @@ function useFormBuilder({
358
428
  });
359
429
  return { success: true };
360
430
  },
361
- [onChange, schema]
431
+ [onChange, policy, schema]
362
432
  );
363
433
  const moveOption = useCallback(
364
434
  (fieldId, optionId, targetIndex) => {
@@ -389,7 +459,9 @@ function useFormBuilder({
389
459
  if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
390
460
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
391
461
  return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
392
- let transformed = transformFieldType(field, type);
462
+ let transformed = applyFieldConstraintDefaults(transformFieldType(field, type), policy);
463
+ const constraintError = fieldConstraintError(transformed, policy);
464
+ if (constraintError !== void 0) return { success: false, error: constraintError };
393
465
  if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
394
466
  const ids = new Set(
395
467
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
@@ -404,7 +476,7 @@ function useFormBuilder({
404
476
  onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
405
477
  return { success: true };
406
478
  },
407
- [createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
479
+ [createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
408
480
  );
409
481
  const addPage = useCallback(
410
482
  (questionId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "4.1.0",
46
- "@form-engine-ts/privacy": "4.1.0"
45
+ "@form-engine-ts/core": "4.2.0",
46
+ "@form-engine-ts/privacy": "4.2.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",