@bluprynt/forms-core 3.0.0 → 4.1.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.mjs CHANGED
@@ -86,6 +86,18 @@ const resolveRelativeDate = (relative, now) => {
86
86
  *
87
87
  * **Date handling**: condition values that are relative date expressions
88
88
  * (e.g. `"+7d"`) are resolved against `ctx.now` before comparison.
89
+ *
90
+ * **Multiselect handling**: when `ctx.fieldTypes` reports the referenced field
91
+ * as `multiselect`, its array value is compared as a set instead of a scalar:
92
+ * `set` means "at least one option chosen", `eq`/`ne` compare set membership
93
+ * ignoring order and duplicates, `in`/`notin` test whether *any* chosen option
94
+ * appears in the condition's list, and the ordering operators are always
95
+ * `false`. Fields of every other type -- including `array` -- keep the original
96
+ * scalar semantics.
97
+ *
98
+ * **Object-valued fields** (`file`, `sustainability`) go through the scalar path
99
+ * too, so only `set`/`notset` are meaningful: any object present counts as
100
+ * `set`, and `eq`/`ne`/`in`/`notin` compare by reference and so never match.
89
101
  */
90
102
  var ConditionEvaluator = class {
91
103
  /**
@@ -104,6 +116,7 @@ var ConditionEvaluator = class {
104
116
  evalSimple(cond, ctx) {
105
117
  if (ctx.visibilityMap && ctx.visibilityMap.get(cond.field) === false) return cond.op === "notset";
106
118
  const fieldValue = ctx.values[String(cond.field)];
119
+ if (ctx.fieldTypes?.get(cond.field) === "multiselect") return this.evalMultiselect(cond, fieldValue);
107
120
  switch (cond.op) {
108
121
  case "set": return fieldValue !== null && fieldValue !== void 0 && fieldValue !== "";
109
122
  case "notset": return fieldValue === null || fieldValue === void 0 || fieldValue === "";
@@ -118,6 +131,32 @@ var ConditionEvaluator = class {
118
131
  default: return false;
119
132
  }
120
133
  }
134
+ evalMultiselect(cond, fieldValue) {
135
+ const selected = Array.isArray(fieldValue) ? fieldValue : [];
136
+ const candidates = cond.value;
137
+ switch (cond.op) {
138
+ case "set": return selected.length > 0;
139
+ case "notset": return selected.length === 0;
140
+ case "eq": return this.sameSet(selected, candidates);
141
+ case "ne": return !this.sameSet(selected, candidates);
142
+ case "in": return Array.isArray(candidates) && selected.some((item) => candidates.includes(item));
143
+ case "notin": return Array.isArray(candidates) && !selected.some((item) => candidates.includes(item));
144
+ default: return false;
145
+ }
146
+ }
147
+ /**
148
+ * Compares the chosen options against a condition value as sets, ignoring
149
+ * order and duplicates. A scalar condition value is read as a one-element
150
+ * set, so `{ op: 'eq', value: 'a' }` means "`a` is the only option chosen".
151
+ */
152
+ sameSet(selected, expected) {
153
+ const expectedItems = Array.isArray(expected) ? expected : [expected];
154
+ const selectedSet = new Set(selected);
155
+ const expectedSet = new Set(expectedItems);
156
+ if (selectedSet.size !== expectedSet.size) return false;
157
+ for (const item of expectedSet) if (!selectedSet.has(item)) return false;
158
+ return true;
159
+ }
121
160
  resolveIfDate(value, now) {
122
161
  if (isRelativeDate(value)) return resolveRelativeDate(value, now);
123
162
  return value;
@@ -378,6 +417,93 @@ var DependencyGraph = class DependencyGraph {
378
417
  }
379
418
  };
380
419
  //#endregion
420
+ //#region src/validators/array-obj-validator.ts
421
+ const isRow = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
422
+ /**
423
+ * Validates an `array_obj` field: the row list itself, then every sub-field of
424
+ * every row.
425
+ *
426
+ * Sub-field errors keep the container's `fieldId` -- matching the key the
427
+ * `fieldErrors` map stores them under -- and carry `itemIndex` (the row) plus
428
+ * `itemFieldId` (the sub-field). A row-level `TYPE` error carries `itemIndex`
429
+ * only.
430
+ */
431
+ var ArrayObjValidator = class {
432
+ validate(ctx) {
433
+ const { fieldId, value, now } = ctx;
434
+ const { items, validateField } = ctx;
435
+ const validation = ctx.validation;
436
+ const errors = [];
437
+ if (value === null || value === void 0) return errors;
438
+ if (!Array.isArray(value)) {
439
+ errors.push({
440
+ fieldId,
441
+ rule: "TYPE",
442
+ message: "Must be an array",
443
+ params: { expectedType: "array" }
444
+ });
445
+ return errors;
446
+ }
447
+ if (validation?.minItems !== void 0 && value.length < validation.minItems) errors.push({
448
+ fieldId,
449
+ rule: "MIN_ITEMS",
450
+ message: `Must have at least ${validation.minItems} items`,
451
+ params: {
452
+ minItems: validation.minItems,
453
+ actual: value.length
454
+ }
455
+ });
456
+ if (validation?.maxItems !== void 0 && value.length > validation.maxItems) errors.push({
457
+ fieldId,
458
+ rule: "MAX_ITEMS",
459
+ message: `Must have at most ${validation.maxItems} items`,
460
+ params: {
461
+ maxItems: validation.maxItems,
462
+ actual: value.length
463
+ }
464
+ });
465
+ if (!items) return errors;
466
+ for (let i = 0; i < value.length; i++) {
467
+ const raw = value[i];
468
+ if (raw !== null && raw !== void 0 && !isRow(raw)) {
469
+ errors.push({
470
+ fieldId,
471
+ rule: "TYPE",
472
+ message: "Must be an object",
473
+ params: { expectedType: "object" },
474
+ itemIndex: i
475
+ });
476
+ continue;
477
+ }
478
+ const row = isRow(raw) ? raw : {};
479
+ for (const subField of items) {
480
+ if (subField.type === "static") continue;
481
+ const fakeEntry = {
482
+ id: subField.id,
483
+ type: subField.type,
484
+ condition: void 0,
485
+ validation: subField.validation,
486
+ parentId: fieldId,
487
+ options: subField.options,
488
+ item: void 0,
489
+ items: void 0,
490
+ label: subField.label,
491
+ title: void 0
492
+ };
493
+ const subErrors = validateField(subField.id, row[String(subField.id)], fakeEntry, now);
494
+ errors.push(...subErrors.map((err) => ({
495
+ ...err,
496
+ fieldId,
497
+ itemIndex: i,
498
+ itemFieldId: subField.id,
499
+ message: `${subField.label}: ${err.message}`
500
+ })));
501
+ }
502
+ }
503
+ return errors;
504
+ }
505
+ };
506
+ //#endregion
381
507
  //#region src/validators/array-validator.ts
382
508
  var ArrayValidator = class {
383
509
  validate(ctx) {
@@ -413,7 +539,7 @@ var ArrayValidator = class {
413
539
  actual: value.length
414
540
  }
415
541
  });
416
- if (item) for (let i = 0; i < value.length; i++) {
542
+ if (item && item.type !== "static") for (let i = 0; i < value.length; i++) {
417
543
  const fakeEntry = {
418
544
  id: fieldId,
419
545
  type: item.type,
@@ -422,6 +548,7 @@ var ArrayValidator = class {
422
548
  parentId: void 0,
423
549
  options: item.options,
424
550
  item: void 0,
551
+ items: void 0,
425
552
  label: item.label,
426
553
  title: void 0
427
554
  };
@@ -435,6 +562,32 @@ var ArrayValidator = class {
435
562
  }
436
563
  };
437
564
  //#endregion
565
+ //#region src/validators/blockchain-validator.ts
566
+ var BlockchainValidator = class {
567
+ validate(ctx) {
568
+ const { fieldId, value } = ctx;
569
+ const validation = ctx.validation;
570
+ const errors = [];
571
+ const isEmpty = value === null || value === void 0 || value === "";
572
+ if (validation?.required && isEmpty) {
573
+ errors.push({
574
+ fieldId,
575
+ rule: "REQUIRED",
576
+ message: "Value is required"
577
+ });
578
+ return errors;
579
+ }
580
+ if (isEmpty) return errors;
581
+ if (typeof value !== "string") errors.push({
582
+ fieldId,
583
+ rule: "TYPE",
584
+ message: "Must be a blockchain address string",
585
+ params: { expectedType: "blockchain" }
586
+ });
587
+ return errors;
588
+ }
589
+ };
590
+ //#endregion
438
591
  //#region src/validators/boolean-validator.ts
439
592
  var BooleanValidator = class {
440
593
  validate(ctx) {
@@ -532,6 +685,51 @@ var FileValidator = class {
532
685
  }
533
686
  };
534
687
  //#endregion
688
+ //#region src/validators/multiselect-validator.ts
689
+ /**
690
+ * Validates `multiselect` fields, whose value is an array of the chosen
691
+ * options' `value`s.
692
+ *
693
+ * An absent value and an empty array both count as "nothing selected", so both
694
+ * fail `required`. Option membership is reported as a single field-level
695
+ * `INVALID_OPTION` error rather than one error per offending element: the
696
+ * viewer filters out errors carrying an `itemIndex` on the non-array render
697
+ * path, so indexed errors would never be displayed.
698
+ */
699
+ var MultiselectValidator = class {
700
+ validate(ctx) {
701
+ const { fieldId, value } = ctx;
702
+ const validation = ctx.validation;
703
+ const options = ctx.options;
704
+ const errors = [];
705
+ const isEmpty = value === null || value === void 0 || Array.isArray(value) && value.length === 0;
706
+ if (validation?.required && isEmpty) {
707
+ errors.push({
708
+ fieldId,
709
+ rule: "REQUIRED",
710
+ message: "Value is required"
711
+ });
712
+ return errors;
713
+ }
714
+ if (isEmpty) return errors;
715
+ if (!Array.isArray(value)) {
716
+ errors.push({
717
+ fieldId,
718
+ rule: "TYPE",
719
+ message: "Must be an array",
720
+ params: { expectedType: "multiselect" }
721
+ });
722
+ return errors;
723
+ }
724
+ if (options && value.some((selected) => !options.some((opt) => opt.value === selected))) errors.push({
725
+ fieldId,
726
+ rule: "INVALID_OPTION",
727
+ message: "Value is not a valid option"
728
+ });
729
+ return errors;
730
+ }
731
+ };
732
+ //#endregion
535
733
  //#region src/validators/number-validator.ts
536
734
  var NumberValidator = class {
537
735
  validate(ctx) {
@@ -659,6 +857,53 @@ var StringValidator = class {
659
857
  }
660
858
  };
661
859
  //#endregion
860
+ //#region src/validators/sustainability-validator.ts
861
+ const OPTIONAL_TEXT_PROPERTIES = ["ccri", "cmc"];
862
+ var SustainabilityValidator = class {
863
+ validate(ctx) {
864
+ const { fieldId, value } = ctx;
865
+ const validation = ctx.validation;
866
+ const errors = [];
867
+ if (value === null || value === void 0) {
868
+ if (validation?.required) errors.push({
869
+ fieldId,
870
+ rule: "REQUIRED",
871
+ message: "Value is required"
872
+ });
873
+ return errors;
874
+ }
875
+ if (typeof value !== "object" || Array.isArray(value)) {
876
+ errors.push({
877
+ fieldId,
878
+ rule: "TYPE",
879
+ message: "Must be a valid sustainability object",
880
+ params: { expectedType: "sustainability" }
881
+ });
882
+ return errors;
883
+ }
884
+ const record = value;
885
+ const address = record.address ?? "";
886
+ if (typeof address !== "string" || OPTIONAL_TEXT_PROPERTIES.some((key) => !isOptionalText(record[key]))) {
887
+ errors.push({
888
+ fieldId,
889
+ rule: "TYPE",
890
+ message: "Must be a valid sustainability object",
891
+ params: { expectedType: "sustainability" }
892
+ });
893
+ return errors;
894
+ }
895
+ if (validation?.required && address === "") errors.push({
896
+ fieldId,
897
+ rule: "REQUIRED",
898
+ message: "Value is required"
899
+ });
900
+ return errors;
901
+ }
902
+ };
903
+ function isOptionalText(value) {
904
+ return value === void 0 || value === null || typeof value === "string";
905
+ }
906
+ //#endregion
662
907
  //#region src/field-validator.ts
663
908
  /**
664
909
  * Validates form values against the schema's validation rules.
@@ -675,9 +920,19 @@ var StringValidator = class {
675
920
  * - `date` -- `required`, `minDate`, `maxDate`. Relative date boundaries
676
921
  * are resolved against `now`.
677
922
  * - `select` -- `required`, plus the value must be one of the defined options.
923
+ * - `multiselect` -- `required` (an absent value and an empty array both fail),
924
+ * plus every selected value must be one of the defined options.
925
+ * - `static` -- never validated; static blocks hold no value.
926
+ * - `blockchain` -- `required` only. The CAIP address format is never checked.
927
+ * - `sustainability` -- `required` (the value's `address` must be a non-empty
928
+ * string) plus a shape check; `ccri` and `cmc` are optional text.
678
929
  * - `array` -- `minItems`, `maxItems`, plus each item is validated
679
930
  * individually according to the array's {@link ArrayItemDef}. Item-level
680
931
  * errors carry an `itemIndex`.
932
+ * - `array_obj` -- `minItems`, `maxItems`, plus every sub-field of every row is
933
+ * validated according to the field's {@link ArrayObjItemDef} list. Sub-field
934
+ * errors carry an `itemIndex` (the row) and an `itemFieldId` (the sub-field),
935
+ * while `fieldId` stays the container's id.
681
936
  *
682
937
  * For all types, if `required` fails, no further rules are checked for that
683
938
  * field (early return). If the value is empty/absent and `required` is not
@@ -697,8 +952,12 @@ var FieldValidator = class {
697
952
  boolean: new BooleanValidator(),
698
953
  date: new DateValidator(),
699
954
  select: new SelectValidator(),
955
+ multiselect: new MultiselectValidator(),
700
956
  array: new ArrayValidator(),
701
- file: new FileValidator()
957
+ array_obj: new ArrayObjValidator(),
958
+ file: new FileValidator(),
959
+ blockchain: new BlockchainValidator(),
960
+ sustainability: new SustainabilityValidator()
702
961
  };
703
962
  }
704
963
  /**
@@ -739,6 +998,17 @@ var FieldValidator = class {
739
998
  };
740
999
  return validator.validate(ctx);
741
1000
  }
1001
+ if (entry.type === "array_obj") {
1002
+ const ctx = {
1003
+ fieldId,
1004
+ value,
1005
+ validation: entry.validation,
1006
+ now,
1007
+ items: entry.items,
1008
+ validateField: this.validateField.bind(this)
1009
+ };
1010
+ return validator.validate(ctx);
1011
+ }
742
1012
  return validator.validate({
743
1013
  fieldId,
744
1014
  value,
@@ -793,11 +1063,17 @@ var FormDefinitionEditor = class {
793
1063
  }
794
1064
  /**
795
1065
  * Returns the next available numeric id (max existing + 1).
1066
+ *
1067
+ * `array_obj` sub-field ids share the form-wide id space, so they count
1068
+ * here too -- otherwise a new top-level field could collide with one.
796
1069
  */
797
1070
  nextId() {
798
1071
  let max = 0;
799
1072
  this.walkAll(this.definition.content, (item) => {
800
1073
  if (item.id > max) max = item.id;
1074
+ if (item.type === "array_obj") {
1075
+ for (const subField of item.items ?? []) if (subField.id > max) max = subField.id;
1076
+ }
801
1077
  });
802
1078
  return max + 1;
803
1079
  }
@@ -936,6 +1212,7 @@ var FormDefinitionEditor = class {
936
1212
  const item = this.findItem(id);
937
1213
  if (!item) throw new Error(`Item with id ${id} not found`);
938
1214
  if (item.type === "section") throw new Error("Sections do not have validation");
1215
+ if (item.type === "static") throw new Error("Static fields do not have validation");
939
1216
  const field = item;
940
1217
  if (validation === void 0) delete field.validation;
941
1218
  else field.validation = validation;
@@ -952,12 +1229,12 @@ var FormDefinitionEditor = class {
952
1229
  return this;
953
1230
  }
954
1231
  /**
955
- * Sets the select options for a `select` field.
1232
+ * Sets the options for a `select` or `multiselect` field.
956
1233
  */
957
1234
  setOptions(id, options) {
958
1235
  const item = this.findItem(id);
959
1236
  if (!item) throw new Error(`Item with id ${id} not found`);
960
- if (item.type !== "select") throw new Error(`Field ${id} is not a select field`);
1237
+ if (item.type !== "select" && item.type !== "multiselect") throw new Error(`Field ${id} is not a select or multiselect field`);
961
1238
  const field = item;
962
1239
  field.options = options;
963
1240
  return this;
@@ -974,6 +1251,94 @@ var FormDefinitionEditor = class {
974
1251
  return this;
975
1252
  }
976
1253
  /**
1254
+ * Replaces the whole sub-field list of an `array_obj` field.
1255
+ *
1256
+ * @throws If the id is not found or does not refer to an `array_obj` field.
1257
+ */
1258
+ setArrayItems(id, itemDefs) {
1259
+ this.assertArrayObjField(id).items = itemDefs;
1260
+ return this;
1261
+ }
1262
+ /**
1263
+ * Returns the sub-field list of an `array_obj` field.
1264
+ *
1265
+ * @throws If the id is not found or does not refer to an `array_obj` field.
1266
+ */
1267
+ getArrayItems(id) {
1268
+ return this.assertArrayObjField(id).items ?? [];
1269
+ }
1270
+ /**
1271
+ * Sets or clears the layout hint of an `array_obj` field.
1272
+ *
1273
+ * Presentation metadata only -- the engine never reads it. Passing
1274
+ * `undefined` removes the key, which reads as `'list'`.
1275
+ *
1276
+ * @throws If the id is not found or does not refer to an `array_obj` field.
1277
+ */
1278
+ setArrayKind(id, kind) {
1279
+ const field = this.assertArrayObjField(id);
1280
+ if (kind === void 0) delete field.kind;
1281
+ else field.kind = kind;
1282
+ return this;
1283
+ }
1284
+ /**
1285
+ * Appends a sub-field to an `array_obj` field. The sub-field's `id` is
1286
+ * auto-assigned when omitted.
1287
+ *
1288
+ * @returns The id of the sub-field that was added.
1289
+ * @throws If the id is not found, does not refer to an `array_obj` field,
1290
+ * or the requested sub-field id is already taken.
1291
+ */
1292
+ addArrayItemField(id, subField) {
1293
+ const field = this.assertArrayObjField(id);
1294
+ const subFieldId = subField.id ?? this.nextId();
1295
+ this.assertIdAvailable(subFieldId);
1296
+ field.items?.push({
1297
+ ...subField,
1298
+ id: subFieldId
1299
+ });
1300
+ return subFieldId;
1301
+ }
1302
+ /**
1303
+ * Merges properties into an existing `array_obj` sub-field.
1304
+ *
1305
+ * Like {@link updateField}, omitted properties are kept rather than
1306
+ * removed -- pass a property explicitly as `undefined` to clear it.
1307
+ *
1308
+ * @throws If the field or the sub-field is not found.
1309
+ */
1310
+ updateArrayItemField(id, subFieldId, patch) {
1311
+ const subField = this.assertArrayObjField(id).items?.find((f) => f.id === subFieldId);
1312
+ if (!subField) throw new Error(`Sub-field with id ${subFieldId} not found in field ${id}`);
1313
+ Object.assign(subField, patch, { id: subFieldId });
1314
+ return this;
1315
+ }
1316
+ /**
1317
+ * Removes a sub-field from an `array_obj` field.
1318
+ *
1319
+ * @throws If the field or the sub-field is not found.
1320
+ */
1321
+ removeArrayItemField(id, subFieldId) {
1322
+ const field = this.assertArrayObjField(id);
1323
+ const index = field.items?.findIndex((f) => f.id === subFieldId) ?? -1;
1324
+ if (index < 0) throw new Error(`Sub-field with id ${subFieldId} not found in field ${id}`);
1325
+ field.items?.splice(index, 1);
1326
+ return this;
1327
+ }
1328
+ /**
1329
+ * Moves a sub-field of an `array_obj` field to another position.
1330
+ *
1331
+ * @throws If the field is not found or either index is out of range.
1332
+ */
1333
+ moveArrayItemField(id, fromIndex, toIndex) {
1334
+ const items = this.assertArrayObjField(id).items ?? [];
1335
+ if (fromIndex < 0 || fromIndex >= items.length) throw new Error(`Index ${fromIndex} out of range`);
1336
+ if (toIndex < 0 || toIndex >= items.length) throw new Error(`Index ${toIndex} out of range`);
1337
+ const [moved] = items.splice(fromIndex, 1);
1338
+ if (moved) items.splice(toIndex, 0, moved);
1339
+ return this;
1340
+ }
1341
+ /**
977
1342
  * Sets the label for a field.
978
1343
  */
979
1344
  setLabel(id, label) {
@@ -985,6 +1350,18 @@ var FormDefinitionEditor = class {
985
1350
  return this;
986
1351
  }
987
1352
  /**
1353
+ * Sets the displayed text of a `static` field.
1354
+ *
1355
+ * @throws If the id is not found or does not refer to a static field.
1356
+ */
1357
+ setText(id, text) {
1358
+ const item = this.findItem(id);
1359
+ if (!item) throw new Error(`Item with id ${id} not found`);
1360
+ if (item.type !== "static") throw new Error(`Field ${id} is not a static field`);
1361
+ item.text = text;
1362
+ return this;
1363
+ }
1364
+ /**
988
1365
  * Sets the description for a field or section.
989
1366
  */
990
1367
  setFieldDescription(id, description) {
@@ -995,6 +1372,41 @@ var FormDefinitionEditor = class {
995
1372
  return this;
996
1373
  }
997
1374
  /**
1375
+ * Sets the placeholder for a field or section.
1376
+ *
1377
+ * Passing `undefined` removes the property. Unlike {@link updateField},
1378
+ * which merges and therefore cannot clear, this genuinely deletes the key.
1379
+ */
1380
+ setPlaceholder(id, placeholder) {
1381
+ return this.setMetadata(id, "placeholder", placeholder);
1382
+ }
1383
+ /**
1384
+ * Sets the answer guidelines for a field or section.
1385
+ *
1386
+ * Passing `undefined` removes the property.
1387
+ */
1388
+ setAnswerGuidelines(id, answerGuidelines) {
1389
+ return this.setMetadata(id, "answer_guidelines", answerGuidelines);
1390
+ }
1391
+ /**
1392
+ * Sets the external reference id for a field or section.
1393
+ *
1394
+ * Passing `undefined` removes the property.
1395
+ */
1396
+ setReferenceId(id, referenceId) {
1397
+ return this.setMetadata(id, "reference_id", referenceId);
1398
+ }
1399
+ /**
1400
+ * Assigns or deletes one optional string property on a field or section.
1401
+ */
1402
+ setMetadata(id, property, value) {
1403
+ const item = this.findItem(id);
1404
+ if (!item) throw new Error(`Item with id ${id} not found`);
1405
+ if (value === void 0) delete item[property];
1406
+ else item[property] = value;
1407
+ return this;
1408
+ }
1409
+ /**
998
1410
  * Returns a deep clone of the current form definition.
999
1411
  */
1000
1412
  toJSON() {
@@ -1009,6 +1421,30 @@ var FormDefinitionEditor = class {
1009
1421
  }
1010
1422
  assertIdAvailable(id) {
1011
1423
  if (this.findItem(id)) throw new Error(`Item with id ${id} already exists`);
1424
+ if (this.findArrayObjItem(id)) throw new Error(`Item with id ${id} already exists`);
1425
+ }
1426
+ /**
1427
+ * Finds an `array_obj` sub-field anywhere in the definition by its id.
1428
+ */
1429
+ findArrayObjItem(id) {
1430
+ let found;
1431
+ this.walkAll(this.definition.content, (item) => {
1432
+ if (found || item.type !== "array_obj") return;
1433
+ const subField = (item.items ?? []).find((f) => f.id === id);
1434
+ if (subField) found = {
1435
+ field: item,
1436
+ subField
1437
+ };
1438
+ });
1439
+ return found;
1440
+ }
1441
+ assertArrayObjField(id) {
1442
+ const item = this.findItem(id);
1443
+ if (!item) throw new Error(`Item with id ${id} not found`);
1444
+ if (item.type !== "array_obj") throw new Error(`Field ${id} is not an array_obj field`);
1445
+ const field = item;
1446
+ if (!field.items) field.items = [];
1447
+ return field;
1012
1448
  }
1013
1449
  insertItem(item, parentId, index) {
1014
1450
  const target = this.getTargetContent(parentId);
@@ -1220,6 +1656,11 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1220
1656
  "properties": { "required": { "type": "boolean" } },
1221
1657
  "additionalProperties": false
1222
1658
  },
1659
+ "multiselectValidation": {
1660
+ "type": "object",
1661
+ "properties": { "required": { "type": "boolean" } },
1662
+ "additionalProperties": false
1663
+ },
1223
1664
  "arrayValidation": {
1224
1665
  "type": "object",
1225
1666
  "properties": {
@@ -1234,11 +1675,35 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1234
1675
  },
1235
1676
  "additionalProperties": false
1236
1677
  },
1678
+ "arrayObjValidation": {
1679
+ "type": "object",
1680
+ "properties": {
1681
+ "minItems": {
1682
+ "type": "integer",
1683
+ "minimum": 0
1684
+ },
1685
+ "maxItems": {
1686
+ "type": "integer",
1687
+ "minimum": 1
1688
+ }
1689
+ },
1690
+ "additionalProperties": false
1691
+ },
1237
1692
  "fileValidation": {
1238
1693
  "type": "object",
1239
1694
  "properties": { "required": { "type": "boolean" } },
1240
1695
  "additionalProperties": false
1241
1696
  },
1697
+ "blockchainValidation": {
1698
+ "type": "object",
1699
+ "properties": { "required": { "type": "boolean" } },
1700
+ "additionalProperties": false
1701
+ },
1702
+ "sustainabilityValidation": {
1703
+ "type": "object",
1704
+ "properties": { "required": { "type": "boolean" } },
1705
+ "additionalProperties": false
1706
+ },
1242
1707
  "selectOption": {
1243
1708
  "type": "object",
1244
1709
  "required": ["value", "label"],
@@ -1255,6 +1720,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1255
1720
  "type": { "const": "string" },
1256
1721
  "label": { "type": "string" },
1257
1722
  "description": { "type": "string" },
1723
+ "placeholder": { "type": "string" },
1724
+ "answer_guidelines": { "type": "string" },
1725
+ "reference_id": { "type": "string" },
1258
1726
  "validation": { "$ref": "#/$defs/stringValidation" }
1259
1727
  },
1260
1728
  "additionalProperties": false
@@ -1266,6 +1734,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1266
1734
  "type": { "const": "number" },
1267
1735
  "label": { "type": "string" },
1268
1736
  "description": { "type": "string" },
1737
+ "placeholder": { "type": "string" },
1738
+ "answer_guidelines": { "type": "string" },
1739
+ "reference_id": { "type": "string" },
1269
1740
  "validation": { "$ref": "#/$defs/numberValidation" }
1270
1741
  },
1271
1742
  "additionalProperties": false
@@ -1277,6 +1748,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1277
1748
  "type": { "const": "boolean" },
1278
1749
  "label": { "type": "string" },
1279
1750
  "description": { "type": "string" },
1751
+ "placeholder": { "type": "string" },
1752
+ "answer_guidelines": { "type": "string" },
1753
+ "reference_id": { "type": "string" },
1280
1754
  "validation": { "$ref": "#/$defs/booleanValidation" }
1281
1755
  },
1282
1756
  "additionalProperties": false
@@ -1288,6 +1762,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1288
1762
  "type": { "const": "date" },
1289
1763
  "label": { "type": "string" },
1290
1764
  "description": { "type": "string" },
1765
+ "placeholder": { "type": "string" },
1766
+ "answer_guidelines": { "type": "string" },
1767
+ "reference_id": { "type": "string" },
1291
1768
  "validation": { "$ref": "#/$defs/dateValidation" }
1292
1769
  },
1293
1770
  "additionalProperties": false
@@ -1303,6 +1780,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1303
1780
  "type": { "const": "select" },
1304
1781
  "label": { "type": "string" },
1305
1782
  "description": { "type": "string" },
1783
+ "placeholder": { "type": "string" },
1784
+ "answer_guidelines": { "type": "string" },
1785
+ "reference_id": { "type": "string" },
1306
1786
  "options": {
1307
1787
  "type": "array",
1308
1788
  "minItems": 1,
@@ -1319,21 +1799,337 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1319
1799
  "type": { "const": "file" },
1320
1800
  "label": { "type": "string" },
1321
1801
  "description": { "type": "string" },
1802
+ "placeholder": { "type": "string" },
1803
+ "answer_guidelines": { "type": "string" },
1804
+ "reference_id": { "type": "string" },
1322
1805
  "validation": { "$ref": "#/$defs/fileValidation" }
1323
1806
  },
1324
1807
  "additionalProperties": false
1325
1808
  },
1326
- "arrayItem": { "oneOf": [
1327
- { "$ref": "#/$defs/arrayItemString" },
1328
- { "$ref": "#/$defs/arrayItemNumber" },
1329
- { "$ref": "#/$defs/arrayItemBoolean" },
1330
- { "$ref": "#/$defs/arrayItemDate" },
1331
- { "$ref": "#/$defs/arrayItemSelect" },
1332
- { "$ref": "#/$defs/arrayItemFile" }
1333
- ] },
1334
- "stringField": {
1809
+ "arrayItemBlockchain": {
1335
1810
  "type": "object",
1336
- "required": [
1811
+ "required": ["type", "label"],
1812
+ "properties": {
1813
+ "type": { "const": "blockchain" },
1814
+ "label": { "type": "string" },
1815
+ "description": { "type": "string" },
1816
+ "placeholder": { "type": "string" },
1817
+ "answer_guidelines": { "type": "string" },
1818
+ "reference_id": { "type": "string" },
1819
+ "validation": { "$ref": "#/$defs/blockchainValidation" }
1820
+ },
1821
+ "additionalProperties": false
1822
+ },
1823
+ "arrayItemSustainability": {
1824
+ "type": "object",
1825
+ "required": ["type", "label"],
1826
+ "properties": {
1827
+ "type": { "const": "sustainability" },
1828
+ "label": { "type": "string" },
1829
+ "description": { "type": "string" },
1830
+ "placeholder": { "type": "string" },
1831
+ "answer_guidelines": { "type": "string" },
1832
+ "reference_id": { "type": "string" },
1833
+ "validation": { "$ref": "#/$defs/sustainabilityValidation" }
1834
+ },
1835
+ "additionalProperties": false
1836
+ },
1837
+ "arrayItemMultiselect": {
1838
+ "type": "object",
1839
+ "required": [
1840
+ "type",
1841
+ "label",
1842
+ "options"
1843
+ ],
1844
+ "properties": {
1845
+ "type": { "const": "multiselect" },
1846
+ "label": { "type": "string" },
1847
+ "description": { "type": "string" },
1848
+ "placeholder": { "type": "string" },
1849
+ "answer_guidelines": { "type": "string" },
1850
+ "reference_id": { "type": "string" },
1851
+ "options": {
1852
+ "type": "array",
1853
+ "minItems": 1,
1854
+ "items": { "$ref": "#/$defs/selectOption" }
1855
+ },
1856
+ "validation": { "$ref": "#/$defs/multiselectValidation" }
1857
+ },
1858
+ "additionalProperties": false
1859
+ },
1860
+ "arrayItemStatic": {
1861
+ "type": "object",
1862
+ "required": ["type", "text"],
1863
+ "properties": {
1864
+ "type": { "const": "static" },
1865
+ "text": { "type": "string" },
1866
+ "label": { "type": "string" },
1867
+ "description": { "type": "string" },
1868
+ "placeholder": { "type": "string" },
1869
+ "answer_guidelines": { "type": "string" },
1870
+ "reference_id": { "type": "string" }
1871
+ },
1872
+ "additionalProperties": false
1873
+ },
1874
+ "arrayItem": { "oneOf": [
1875
+ { "$ref": "#/$defs/arrayItemString" },
1876
+ { "$ref": "#/$defs/arrayItemNumber" },
1877
+ { "$ref": "#/$defs/arrayItemBoolean" },
1878
+ { "$ref": "#/$defs/arrayItemDate" },
1879
+ { "$ref": "#/$defs/arrayItemSelect" },
1880
+ { "$ref": "#/$defs/arrayItemFile" },
1881
+ { "$ref": "#/$defs/arrayItemBlockchain" },
1882
+ { "$ref": "#/$defs/arrayItemSustainability" },
1883
+ { "$ref": "#/$defs/arrayItemMultiselect" },
1884
+ { "$ref": "#/$defs/arrayItemStatic" }
1885
+ ] },
1886
+ "arrayObjItemString": {
1887
+ "type": "object",
1888
+ "required": [
1889
+ "id",
1890
+ "type",
1891
+ "label"
1892
+ ],
1893
+ "properties": {
1894
+ "id": {
1895
+ "type": "integer",
1896
+ "minimum": 1
1897
+ },
1898
+ "type": { "const": "string" },
1899
+ "label": { "type": "string" },
1900
+ "description": { "type": "string" },
1901
+ "placeholder": { "type": "string" },
1902
+ "answer_guidelines": { "type": "string" },
1903
+ "reference_id": { "type": "string" },
1904
+ "validation": { "$ref": "#/$defs/stringValidation" }
1905
+ },
1906
+ "additionalProperties": false
1907
+ },
1908
+ "arrayObjItemNumber": {
1909
+ "type": "object",
1910
+ "required": [
1911
+ "id",
1912
+ "type",
1913
+ "label"
1914
+ ],
1915
+ "properties": {
1916
+ "id": {
1917
+ "type": "integer",
1918
+ "minimum": 1
1919
+ },
1920
+ "type": { "const": "number" },
1921
+ "label": { "type": "string" },
1922
+ "description": { "type": "string" },
1923
+ "placeholder": { "type": "string" },
1924
+ "answer_guidelines": { "type": "string" },
1925
+ "reference_id": { "type": "string" },
1926
+ "validation": { "$ref": "#/$defs/numberValidation" }
1927
+ },
1928
+ "additionalProperties": false
1929
+ },
1930
+ "arrayObjItemBoolean": {
1931
+ "type": "object",
1932
+ "required": [
1933
+ "id",
1934
+ "type",
1935
+ "label"
1936
+ ],
1937
+ "properties": {
1938
+ "id": {
1939
+ "type": "integer",
1940
+ "minimum": 1
1941
+ },
1942
+ "type": { "const": "boolean" },
1943
+ "label": { "type": "string" },
1944
+ "description": { "type": "string" },
1945
+ "placeholder": { "type": "string" },
1946
+ "answer_guidelines": { "type": "string" },
1947
+ "reference_id": { "type": "string" },
1948
+ "validation": { "$ref": "#/$defs/booleanValidation" }
1949
+ },
1950
+ "additionalProperties": false
1951
+ },
1952
+ "arrayObjItemDate": {
1953
+ "type": "object",
1954
+ "required": [
1955
+ "id",
1956
+ "type",
1957
+ "label"
1958
+ ],
1959
+ "properties": {
1960
+ "id": {
1961
+ "type": "integer",
1962
+ "minimum": 1
1963
+ },
1964
+ "type": { "const": "date" },
1965
+ "label": { "type": "string" },
1966
+ "description": { "type": "string" },
1967
+ "placeholder": { "type": "string" },
1968
+ "answer_guidelines": { "type": "string" },
1969
+ "reference_id": { "type": "string" },
1970
+ "validation": { "$ref": "#/$defs/dateValidation" }
1971
+ },
1972
+ "additionalProperties": false
1973
+ },
1974
+ "arrayObjItemSelect": {
1975
+ "type": "object",
1976
+ "required": [
1977
+ "id",
1978
+ "type",
1979
+ "label",
1980
+ "options"
1981
+ ],
1982
+ "properties": {
1983
+ "id": {
1984
+ "type": "integer",
1985
+ "minimum": 1
1986
+ },
1987
+ "type": { "const": "select" },
1988
+ "label": { "type": "string" },
1989
+ "description": { "type": "string" },
1990
+ "placeholder": { "type": "string" },
1991
+ "answer_guidelines": { "type": "string" },
1992
+ "reference_id": { "type": "string" },
1993
+ "options": {
1994
+ "type": "array",
1995
+ "minItems": 1,
1996
+ "items": { "$ref": "#/$defs/selectOption" }
1997
+ },
1998
+ "validation": { "$ref": "#/$defs/selectValidation" }
1999
+ },
2000
+ "additionalProperties": false
2001
+ },
2002
+ "arrayObjItemMultiselect": {
2003
+ "type": "object",
2004
+ "required": [
2005
+ "id",
2006
+ "type",
2007
+ "label",
2008
+ "options"
2009
+ ],
2010
+ "properties": {
2011
+ "id": {
2012
+ "type": "integer",
2013
+ "minimum": 1
2014
+ },
2015
+ "type": { "const": "multiselect" },
2016
+ "label": { "type": "string" },
2017
+ "description": { "type": "string" },
2018
+ "placeholder": { "type": "string" },
2019
+ "answer_guidelines": { "type": "string" },
2020
+ "reference_id": { "type": "string" },
2021
+ "options": {
2022
+ "type": "array",
2023
+ "minItems": 1,
2024
+ "items": { "$ref": "#/$defs/selectOption" }
2025
+ },
2026
+ "validation": { "$ref": "#/$defs/multiselectValidation" }
2027
+ },
2028
+ "additionalProperties": false
2029
+ },
2030
+ "arrayObjItemFile": {
2031
+ "type": "object",
2032
+ "required": [
2033
+ "id",
2034
+ "type",
2035
+ "label"
2036
+ ],
2037
+ "properties": {
2038
+ "id": {
2039
+ "type": "integer",
2040
+ "minimum": 1
2041
+ },
2042
+ "type": { "const": "file" },
2043
+ "label": { "type": "string" },
2044
+ "description": { "type": "string" },
2045
+ "placeholder": { "type": "string" },
2046
+ "answer_guidelines": { "type": "string" },
2047
+ "reference_id": { "type": "string" },
2048
+ "validation": { "$ref": "#/$defs/fileValidation" }
2049
+ },
2050
+ "additionalProperties": false
2051
+ },
2052
+ "arrayObjItemBlockchain": {
2053
+ "type": "object",
2054
+ "required": [
2055
+ "id",
2056
+ "type",
2057
+ "label"
2058
+ ],
2059
+ "properties": {
2060
+ "id": {
2061
+ "type": "integer",
2062
+ "minimum": 1
2063
+ },
2064
+ "type": { "const": "blockchain" },
2065
+ "label": { "type": "string" },
2066
+ "description": { "type": "string" },
2067
+ "placeholder": { "type": "string" },
2068
+ "answer_guidelines": { "type": "string" },
2069
+ "reference_id": { "type": "string" },
2070
+ "validation": { "$ref": "#/$defs/blockchainValidation" }
2071
+ },
2072
+ "additionalProperties": false
2073
+ },
2074
+ "arrayObjItemSustainability": {
2075
+ "type": "object",
2076
+ "required": [
2077
+ "id",
2078
+ "type",
2079
+ "label"
2080
+ ],
2081
+ "properties": {
2082
+ "id": {
2083
+ "type": "integer",
2084
+ "minimum": 1
2085
+ },
2086
+ "type": { "const": "sustainability" },
2087
+ "label": { "type": "string" },
2088
+ "description": { "type": "string" },
2089
+ "placeholder": { "type": "string" },
2090
+ "answer_guidelines": { "type": "string" },
2091
+ "reference_id": { "type": "string" },
2092
+ "validation": { "$ref": "#/$defs/sustainabilityValidation" }
2093
+ },
2094
+ "additionalProperties": false
2095
+ },
2096
+ "arrayObjItemStatic": {
2097
+ "type": "object",
2098
+ "required": [
2099
+ "id",
2100
+ "type",
2101
+ "text"
2102
+ ],
2103
+ "properties": {
2104
+ "id": {
2105
+ "type": "integer",
2106
+ "minimum": 1
2107
+ },
2108
+ "type": { "const": "static" },
2109
+ "text": { "type": "string" },
2110
+ "label": { "type": "string" },
2111
+ "description": { "type": "string" },
2112
+ "placeholder": { "type": "string" },
2113
+ "answer_guidelines": { "type": "string" },
2114
+ "reference_id": { "type": "string" }
2115
+ },
2116
+ "additionalProperties": false
2117
+ },
2118
+ "arrayObjItem": { "oneOf": [
2119
+ { "$ref": "#/$defs/arrayObjItemString" },
2120
+ { "$ref": "#/$defs/arrayObjItemNumber" },
2121
+ { "$ref": "#/$defs/arrayObjItemBoolean" },
2122
+ { "$ref": "#/$defs/arrayObjItemDate" },
2123
+ { "$ref": "#/$defs/arrayObjItemSelect" },
2124
+ { "$ref": "#/$defs/arrayObjItemMultiselect" },
2125
+ { "$ref": "#/$defs/arrayObjItemFile" },
2126
+ { "$ref": "#/$defs/arrayObjItemBlockchain" },
2127
+ { "$ref": "#/$defs/arrayObjItemSustainability" },
2128
+ { "$ref": "#/$defs/arrayObjItemStatic" }
2129
+ ] },
2130
+ "stringField": {
2131
+ "type": "object",
2132
+ "required": [
1337
2133
  "id",
1338
2134
  "type",
1339
2135
  "label"
@@ -1346,6 +2142,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1346
2142
  "type": { "const": "string" },
1347
2143
  "label": { "type": "string" },
1348
2144
  "description": { "type": "string" },
2145
+ "placeholder": { "type": "string" },
2146
+ "answer_guidelines": { "type": "string" },
2147
+ "reference_id": { "type": "string" },
1349
2148
  "condition": { "$ref": "#/$defs/condition" },
1350
2149
  "validation": { "$ref": "#/$defs/stringValidation" }
1351
2150
  },
@@ -1366,6 +2165,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1366
2165
  "type": { "const": "number" },
1367
2166
  "label": { "type": "string" },
1368
2167
  "description": { "type": "string" },
2168
+ "placeholder": { "type": "string" },
2169
+ "answer_guidelines": { "type": "string" },
2170
+ "reference_id": { "type": "string" },
1369
2171
  "condition": { "$ref": "#/$defs/condition" },
1370
2172
  "validation": { "$ref": "#/$defs/numberValidation" }
1371
2173
  },
@@ -1386,6 +2188,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1386
2188
  "type": { "const": "boolean" },
1387
2189
  "label": { "type": "string" },
1388
2190
  "description": { "type": "string" },
2191
+ "placeholder": { "type": "string" },
2192
+ "answer_guidelines": { "type": "string" },
2193
+ "reference_id": { "type": "string" },
1389
2194
  "condition": { "$ref": "#/$defs/condition" },
1390
2195
  "validation": { "$ref": "#/$defs/booleanValidation" }
1391
2196
  },
@@ -1406,6 +2211,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1406
2211
  "type": { "const": "date" },
1407
2212
  "label": { "type": "string" },
1408
2213
  "description": { "type": "string" },
2214
+ "placeholder": { "type": "string" },
2215
+ "answer_guidelines": { "type": "string" },
2216
+ "reference_id": { "type": "string" },
1409
2217
  "condition": { "$ref": "#/$defs/condition" },
1410
2218
  "validation": { "$ref": "#/$defs/dateValidation" }
1411
2219
  },
@@ -1427,6 +2235,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1427
2235
  "type": { "const": "select" },
1428
2236
  "label": { "type": "string" },
1429
2237
  "description": { "type": "string" },
2238
+ "placeholder": { "type": "string" },
2239
+ "answer_guidelines": { "type": "string" },
2240
+ "reference_id": { "type": "string" },
1430
2241
  "condition": { "$ref": "#/$defs/condition" },
1431
2242
  "options": {
1432
2243
  "type": "array",
@@ -1437,6 +2248,58 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1437
2248
  },
1438
2249
  "additionalProperties": false
1439
2250
  },
2251
+ "multiselectField": {
2252
+ "type": "object",
2253
+ "required": [
2254
+ "id",
2255
+ "type",
2256
+ "label",
2257
+ "options"
2258
+ ],
2259
+ "properties": {
2260
+ "id": {
2261
+ "type": "integer",
2262
+ "minimum": 1
2263
+ },
2264
+ "type": { "const": "multiselect" },
2265
+ "label": { "type": "string" },
2266
+ "description": { "type": "string" },
2267
+ "placeholder": { "type": "string" },
2268
+ "answer_guidelines": { "type": "string" },
2269
+ "reference_id": { "type": "string" },
2270
+ "condition": { "$ref": "#/$defs/condition" },
2271
+ "options": {
2272
+ "type": "array",
2273
+ "minItems": 1,
2274
+ "items": { "$ref": "#/$defs/selectOption" }
2275
+ },
2276
+ "validation": { "$ref": "#/$defs/multiselectValidation" }
2277
+ },
2278
+ "additionalProperties": false
2279
+ },
2280
+ "staticField": {
2281
+ "type": "object",
2282
+ "required": [
2283
+ "id",
2284
+ "type",
2285
+ "text"
2286
+ ],
2287
+ "properties": {
2288
+ "id": {
2289
+ "type": "integer",
2290
+ "minimum": 1
2291
+ },
2292
+ "type": { "const": "static" },
2293
+ "text": { "type": "string" },
2294
+ "label": { "type": "string" },
2295
+ "description": { "type": "string" },
2296
+ "placeholder": { "type": "string" },
2297
+ "answer_guidelines": { "type": "string" },
2298
+ "reference_id": { "type": "string" },
2299
+ "condition": { "$ref": "#/$defs/condition" }
2300
+ },
2301
+ "additionalProperties": false
2302
+ },
1440
2303
  "arrayField": {
1441
2304
  "type": "object",
1442
2305
  "required": [
@@ -1453,12 +2316,48 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1453
2316
  "type": { "const": "array" },
1454
2317
  "label": { "type": "string" },
1455
2318
  "description": { "type": "string" },
2319
+ "placeholder": { "type": "string" },
2320
+ "answer_guidelines": { "type": "string" },
2321
+ "reference_id": { "type": "string" },
1456
2322
  "condition": { "$ref": "#/$defs/condition" },
1457
2323
  "item": { "$ref": "#/$defs/arrayItem" },
1458
2324
  "validation": { "$ref": "#/$defs/arrayValidation" }
1459
2325
  },
1460
2326
  "additionalProperties": false
1461
2327
  },
2328
+ "arrayObjField": {
2329
+ "type": "object",
2330
+ "required": [
2331
+ "id",
2332
+ "type",
2333
+ "label",
2334
+ "items"
2335
+ ],
2336
+ "properties": {
2337
+ "id": {
2338
+ "type": "integer",
2339
+ "minimum": 1
2340
+ },
2341
+ "type": { "const": "array_obj" },
2342
+ "label": { "type": "string" },
2343
+ "description": { "type": "string" },
2344
+ "placeholder": { "type": "string" },
2345
+ "answer_guidelines": { "type": "string" },
2346
+ "reference_id": { "type": "string" },
2347
+ "condition": { "$ref": "#/$defs/condition" },
2348
+ "items": {
2349
+ "type": "array",
2350
+ "minItems": 1,
2351
+ "items": { "$ref": "#/$defs/arrayObjItem" }
2352
+ },
2353
+ "kind": {
2354
+ "type": "string",
2355
+ "enum": ["list", "table"]
2356
+ },
2357
+ "validation": { "$ref": "#/$defs/arrayObjValidation" }
2358
+ },
2359
+ "additionalProperties": false
2360
+ },
1462
2361
  "fileField": {
1463
2362
  "type": "object",
1464
2363
  "required": [
@@ -1474,19 +2373,73 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1474
2373
  "type": { "const": "file" },
1475
2374
  "label": { "type": "string" },
1476
2375
  "description": { "type": "string" },
2376
+ "placeholder": { "type": "string" },
2377
+ "answer_guidelines": { "type": "string" },
2378
+ "reference_id": { "type": "string" },
1477
2379
  "condition": { "$ref": "#/$defs/condition" },
1478
2380
  "validation": { "$ref": "#/$defs/fileValidation" }
1479
2381
  },
1480
2382
  "additionalProperties": false
1481
2383
  },
2384
+ "blockchainField": {
2385
+ "type": "object",
2386
+ "required": [
2387
+ "id",
2388
+ "type",
2389
+ "label"
2390
+ ],
2391
+ "properties": {
2392
+ "id": {
2393
+ "type": "integer",
2394
+ "minimum": 1
2395
+ },
2396
+ "type": { "const": "blockchain" },
2397
+ "label": { "type": "string" },
2398
+ "description": { "type": "string" },
2399
+ "placeholder": { "type": "string" },
2400
+ "answer_guidelines": { "type": "string" },
2401
+ "reference_id": { "type": "string" },
2402
+ "condition": { "$ref": "#/$defs/condition" },
2403
+ "validation": { "$ref": "#/$defs/blockchainValidation" }
2404
+ },
2405
+ "additionalProperties": false
2406
+ },
2407
+ "sustainabilityField": {
2408
+ "type": "object",
2409
+ "required": [
2410
+ "id",
2411
+ "type",
2412
+ "label"
2413
+ ],
2414
+ "properties": {
2415
+ "id": {
2416
+ "type": "integer",
2417
+ "minimum": 1
2418
+ },
2419
+ "type": { "const": "sustainability" },
2420
+ "label": { "type": "string" },
2421
+ "description": { "type": "string" },
2422
+ "placeholder": { "type": "string" },
2423
+ "answer_guidelines": { "type": "string" },
2424
+ "reference_id": { "type": "string" },
2425
+ "condition": { "$ref": "#/$defs/condition" },
2426
+ "validation": { "$ref": "#/$defs/sustainabilityValidation" }
2427
+ },
2428
+ "additionalProperties": false
2429
+ },
1482
2430
  "fieldItem": { "oneOf": [
1483
2431
  { "$ref": "#/$defs/stringField" },
1484
2432
  { "$ref": "#/$defs/numberField" },
1485
2433
  { "$ref": "#/$defs/booleanField" },
1486
2434
  { "$ref": "#/$defs/dateField" },
1487
2435
  { "$ref": "#/$defs/selectField" },
2436
+ { "$ref": "#/$defs/multiselectField" },
1488
2437
  { "$ref": "#/$defs/arrayField" },
1489
- { "$ref": "#/$defs/fileField" }
2438
+ { "$ref": "#/$defs/arrayObjField" },
2439
+ { "$ref": "#/$defs/fileField" },
2440
+ { "$ref": "#/$defs/staticField" },
2441
+ { "$ref": "#/$defs/blockchainField" },
2442
+ { "$ref": "#/$defs/sustainabilityField" }
1490
2443
  ] },
1491
2444
  "section": {
1492
2445
  "type": "object",
@@ -1504,6 +2457,9 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1504
2457
  "type": { "const": "section" },
1505
2458
  "title": { "type": "string" },
1506
2459
  "description": { "type": "string" },
2460
+ "placeholder": { "type": "string" },
2461
+ "answer_guidelines": { "type": "string" },
2462
+ "reference_id": { "type": "string" },
1507
2463
  "condition": { "$ref": "#/$defs/condition" },
1508
2464
  "content": {
1509
2465
  "type": "array",
@@ -1516,6 +2472,228 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1516
2472
  "contentItem": { "oneOf": [{ "$ref": "#/$defs/fieldItem" }, { "$ref": "#/$defs/section" }] }
1517
2473
  }
1518
2474
  });
2475
+ const itemRequiredProperties = new Map([
2476
+ ["string", new Set([
2477
+ "id",
2478
+ "type",
2479
+ "label"
2480
+ ])],
2481
+ ["number", new Set([
2482
+ "id",
2483
+ "type",
2484
+ "label"
2485
+ ])],
2486
+ ["boolean", new Set([
2487
+ "id",
2488
+ "type",
2489
+ "label"
2490
+ ])],
2491
+ ["date", new Set([
2492
+ "id",
2493
+ "type",
2494
+ "label"
2495
+ ])],
2496
+ ["select", new Set([
2497
+ "id",
2498
+ "type",
2499
+ "label",
2500
+ "options"
2501
+ ])],
2502
+ ["multiselect", new Set([
2503
+ "id",
2504
+ "type",
2505
+ "label",
2506
+ "options"
2507
+ ])],
2508
+ ["array", new Set([
2509
+ "id",
2510
+ "type",
2511
+ "label",
2512
+ "item"
2513
+ ])],
2514
+ ["array_obj", new Set([
2515
+ "id",
2516
+ "type",
2517
+ "label",
2518
+ "items"
2519
+ ])],
2520
+ ["file", new Set([
2521
+ "id",
2522
+ "type",
2523
+ "label"
2524
+ ])],
2525
+ ["blockchain", new Set([
2526
+ "id",
2527
+ "type",
2528
+ "label"
2529
+ ])],
2530
+ ["sustainability", new Set([
2531
+ "id",
2532
+ "type",
2533
+ "label"
2534
+ ])],
2535
+ ["static", new Set([
2536
+ "id",
2537
+ "type",
2538
+ "text"
2539
+ ])],
2540
+ ["section", new Set([
2541
+ "id",
2542
+ "type",
2543
+ "title",
2544
+ "content"
2545
+ ])]
2546
+ ]);
2547
+ const itemAllowedProperties = new Map([
2548
+ ["string", new Set([
2549
+ "id",
2550
+ "type",
2551
+ "label",
2552
+ "description",
2553
+ "placeholder",
2554
+ "answer_guidelines",
2555
+ "reference_id",
2556
+ "condition",
2557
+ "validation"
2558
+ ])],
2559
+ ["number", new Set([
2560
+ "id",
2561
+ "type",
2562
+ "label",
2563
+ "description",
2564
+ "placeholder",
2565
+ "answer_guidelines",
2566
+ "reference_id",
2567
+ "condition",
2568
+ "validation"
2569
+ ])],
2570
+ ["boolean", new Set([
2571
+ "id",
2572
+ "type",
2573
+ "label",
2574
+ "description",
2575
+ "placeholder",
2576
+ "answer_guidelines",
2577
+ "reference_id",
2578
+ "condition",
2579
+ "validation"
2580
+ ])],
2581
+ ["date", new Set([
2582
+ "id",
2583
+ "type",
2584
+ "label",
2585
+ "description",
2586
+ "placeholder",
2587
+ "answer_guidelines",
2588
+ "reference_id",
2589
+ "condition",
2590
+ "validation"
2591
+ ])],
2592
+ ["select", new Set([
2593
+ "id",
2594
+ "type",
2595
+ "label",
2596
+ "description",
2597
+ "placeholder",
2598
+ "answer_guidelines",
2599
+ "reference_id",
2600
+ "condition",
2601
+ "options",
2602
+ "validation"
2603
+ ])],
2604
+ ["multiselect", new Set([
2605
+ "id",
2606
+ "type",
2607
+ "label",
2608
+ "description",
2609
+ "placeholder",
2610
+ "answer_guidelines",
2611
+ "reference_id",
2612
+ "condition",
2613
+ "options",
2614
+ "validation"
2615
+ ])],
2616
+ ["array", new Set([
2617
+ "id",
2618
+ "type",
2619
+ "label",
2620
+ "description",
2621
+ "placeholder",
2622
+ "answer_guidelines",
2623
+ "reference_id",
2624
+ "condition",
2625
+ "item",
2626
+ "validation"
2627
+ ])],
2628
+ ["array_obj", new Set([
2629
+ "id",
2630
+ "type",
2631
+ "label",
2632
+ "description",
2633
+ "placeholder",
2634
+ "answer_guidelines",
2635
+ "reference_id",
2636
+ "condition",
2637
+ "items",
2638
+ "kind",
2639
+ "validation"
2640
+ ])],
2641
+ ["file", new Set([
2642
+ "id",
2643
+ "type",
2644
+ "label",
2645
+ "description",
2646
+ "placeholder",
2647
+ "answer_guidelines",
2648
+ "reference_id",
2649
+ "condition",
2650
+ "validation"
2651
+ ])],
2652
+ ["blockchain", new Set([
2653
+ "id",
2654
+ "type",
2655
+ "label",
2656
+ "description",
2657
+ "placeholder",
2658
+ "answer_guidelines",
2659
+ "reference_id",
2660
+ "condition",
2661
+ "validation"
2662
+ ])],
2663
+ ["sustainability", new Set([
2664
+ "id",
2665
+ "type",
2666
+ "label",
2667
+ "description",
2668
+ "placeholder",
2669
+ "answer_guidelines",
2670
+ "reference_id",
2671
+ "condition",
2672
+ "validation"
2673
+ ])],
2674
+ ["static", new Set([
2675
+ "id",
2676
+ "type",
2677
+ "text",
2678
+ "label",
2679
+ "description",
2680
+ "placeholder",
2681
+ "answer_guidelines",
2682
+ "reference_id",
2683
+ "condition"
2684
+ ])],
2685
+ ["section", new Set([
2686
+ "id",
2687
+ "type",
2688
+ "title",
2689
+ "description",
2690
+ "placeholder",
2691
+ "answer_guidelines",
2692
+ "reference_id",
2693
+ "condition",
2694
+ "content"
2695
+ ])]
2696
+ ]);
1519
2697
  /**
1520
2698
  * Validates form definitions at both the structural (JSON Schema) and
1521
2699
  * semantic levels.
@@ -1529,12 +2707,17 @@ const validateFn = new Ajv2020({ allErrors: true }).compile({
1529
2707
  * ### Semantic validation (`validate`)
1530
2708
  * Checks for logical issues that go beyond JSON schema validity:
1531
2709
  * 1. **Duplicate IDs** (`DUPLICATE_ID`) -- every content item id must be unique.
2710
+ * `array_obj` sub-field ids share that id space and are checked with it.
1532
2711
  * 2. **Nesting depth** (`NESTING_DEPTH`) -- sections may not be nested more
1533
2712
  * than 3 levels deep.
1534
2713
  * 3. **Unknown field references** (`UNKNOWN_FIELD_REF`) -- conditions must
1535
2714
  * only reference field ids that exist in the registry.
1536
- * 4. **Condition references section** (`CONDITION_REFS_SECTION`) -- conditions
1537
- * must not reference section ids, because sections have no values.
2715
+ * 4. **Condition references a valueless item** (`CONDITION_REFS_SECTION`,
2716
+ * `CONDITION_REFS_STATIC`) -- conditions must not reference section or
2717
+ * static ids, because neither holds a value.
2718
+ * 4b. **Condition references an `array_obj` sub-field** (`CONDITION_REFS_ARRAY_OBJ_ITEM`)
2719
+ * -- a sub-field holds one value per row, so a form-level condition cannot
2720
+ * say which row it means.
1538
2721
  * 5. **Constraint contradictions** (`INVALID_MIN_MAX`) -- e.g. `minLength > maxLength`,
1539
2722
  * `min > max`, `minDate > maxDate` (absolute dates only), `minItems > maxItems`.
1540
2723
  * 6. **Invalid regex** (`INVALID_REGEX`) -- string field `pattern` values must
@@ -1549,18 +2732,177 @@ var FormDefinitionValidator = class {
1549
2732
  */
1550
2733
  validateSchema(input) {
1551
2734
  if (validateFn(input)) return [];
1552
- return (validateFn.errors ?? []).map((err) => {
2735
+ return this.formatSchemaErrors(validateFn.errors ?? [], input);
2736
+ }
2737
+ formatSchemaErrors(errors, input) {
2738
+ const issues = this.collectSchemaIssues(errors, input);
2739
+ const dedupedIssues = this.deduplicateSchemaIssues(issues);
2740
+ const specificIssuePaths = dedupedIssues.filter((issue) => issue.keyword !== "oneOf").map((issue) => issue.path);
2741
+ return dedupedIssues.filter((issue) => !this.isRedundantOneOfIssue(issue, specificIssuePaths)).map((issue) => ({
2742
+ code: "SCHEMA_INVALID",
2743
+ message: issue.message,
2744
+ params: {
2745
+ path: issue.path,
2746
+ keyword: issue.keyword,
2747
+ ...issue.property ? { property: issue.property } : {}
2748
+ }
2749
+ }));
2750
+ }
2751
+ collectSchemaIssues(errors, input) {
2752
+ const additionalByPath = /* @__PURE__ */ new Map();
2753
+ const issues = [];
2754
+ for (const err of errors) {
1553
2755
  const path = err.instancePath || "/";
1554
- const message = err.message ?? "Unknown error";
1555
- if (err.keyword === "additionalProperties") return {
1556
- code: "SCHEMA_INVALID",
1557
- message: `${path}: ${message}: '${err.params.additionalProperty}'`
2756
+ if (!this.shouldKeepSchemaError(err, input)) continue;
2757
+ if (err.keyword === "additionalProperties") {
2758
+ const property = err.params.additionalProperty;
2759
+ if (!property) continue;
2760
+ if (!this.shouldKeepAdditionalPropertyError(path, property, input)) continue;
2761
+ const properties = additionalByPath.get(path) ?? /* @__PURE__ */ new Set();
2762
+ properties.add(property);
2763
+ additionalByPath.set(path, properties);
2764
+ continue;
2765
+ }
2766
+ issues.push(this.formatSchemaIssue(err));
2767
+ }
2768
+ for (const [path, properties] of additionalByPath) issues.push(this.formatAdditionalPropertiesIssue(path, [...properties].sort()));
2769
+ return issues;
2770
+ }
2771
+ shouldKeepSchemaError(err, input) {
2772
+ if (err.keyword === "const" && this.getLastPathSegment(err.instancePath) === "type") {
2773
+ const value = this.getValueAtPath(input, this.getParentPath(err.instancePath));
2774
+ return !(this.isRecord(value) && typeof value.type === "string" && itemAllowedProperties.has(value.type));
2775
+ }
2776
+ if (err.keyword !== "required") return true;
2777
+ const missingProperty = err.params.missingProperty;
2778
+ if (!missingProperty) return true;
2779
+ const value = this.getValueAtPath(input, err.instancePath);
2780
+ if (this.isRecord(value) && value.type === void 0 && this.isContentItemPath(err.instancePath)) return missingProperty === "id" || missingProperty === "type";
2781
+ if (!this.isRecord(value) || typeof value.type !== "string") return true;
2782
+ const requiredProperties = itemRequiredProperties.get(value.type);
2783
+ return requiredProperties ? requiredProperties.has(missingProperty) : true;
2784
+ }
2785
+ shouldKeepAdditionalPropertyError(path, property, input) {
2786
+ const value = this.getValueAtPath(input, path);
2787
+ if (!this.isRecord(value) || typeof value.type !== "string") return true;
2788
+ const allowedProperties = itemAllowedProperties.get(value.type);
2789
+ return allowedProperties ? !allowedProperties.has(property) : true;
2790
+ }
2791
+ formatSchemaIssue(err) {
2792
+ const path = err.instancePath || "/";
2793
+ switch (err.keyword) {
2794
+ case "required": {
2795
+ const property = err.params.missingProperty ?? "unknown";
2796
+ return {
2797
+ path,
2798
+ keyword: err.keyword,
2799
+ property,
2800
+ message: `${this.formatPath(path)} is missing required property "${property}".`
2801
+ };
2802
+ }
2803
+ case "const": {
2804
+ const propertyPath = this.formatPath(path);
2805
+ const parentPath = this.getParentPath(path);
2806
+ const property = this.getLastPathSegment(path);
2807
+ return {
2808
+ path,
2809
+ keyword: err.keyword,
2810
+ property,
2811
+ message: property === "type" ? `${this.formatPath(parentPath)} has an invalid type.` : `${propertyPath} has an invalid value.`
2812
+ };
2813
+ }
2814
+ case "type": {
2815
+ const params = err.params;
2816
+ return {
2817
+ path,
2818
+ keyword: err.keyword,
2819
+ property: this.getLastPathSegment(path),
2820
+ message: `${this.formatPath(path)} must be ${this.formatArticle(params.type)} ${params.type ?? "valid value"}.`
2821
+ };
2822
+ }
2823
+ case "oneOf": return {
2824
+ path,
2825
+ keyword: err.keyword,
2826
+ message: `${this.formatPath(path)} is invalid.`
1558
2827
  };
1559
- return {
1560
- code: "SCHEMA_INVALID",
1561
- message: `${path}: ${message}`
2828
+ default: return {
2829
+ path,
2830
+ keyword: err.keyword,
2831
+ property: this.getLastPathSegment(path),
2832
+ message: `${this.formatPath(path)} ${err.message ?? "is invalid"}.`
1562
2833
  };
1563
- });
2834
+ }
2835
+ }
2836
+ formatAdditionalPropertiesIssue(path, properties) {
2837
+ const propertyList = properties.map((property) => `"${property}"`).join(", ");
2838
+ const noun = properties.length === 1 ? "property" : "properties";
2839
+ return {
2840
+ path,
2841
+ keyword: "additionalProperties",
2842
+ property: properties.join(","),
2843
+ message: `${this.formatPath(path)} has unsupported ${noun}: ${propertyList}.`
2844
+ };
2845
+ }
2846
+ deduplicateSchemaIssues(issues) {
2847
+ const seen = /* @__PURE__ */ new Set();
2848
+ const result = [];
2849
+ for (const issue of issues) {
2850
+ const key = `${issue.path}:${issue.keyword}:${issue.property ?? ""}:${issue.message}`;
2851
+ if (seen.has(key)) continue;
2852
+ seen.add(key);
2853
+ result.push(issue);
2854
+ }
2855
+ return result;
2856
+ }
2857
+ isRedundantOneOfIssue(issue, specificIssuePaths) {
2858
+ if (issue.keyword !== "oneOf") return false;
2859
+ return specificIssuePaths.some((path) => path === issue.path || path.startsWith(`${issue.path}/`));
2860
+ }
2861
+ formatPath(path) {
2862
+ if (!path || path === "/") return "Form definition";
2863
+ const segments = path.split("/").filter(Boolean);
2864
+ const parts = [];
2865
+ for (let index = 0; index < segments.length; index += 1) {
2866
+ const segment = segments[index];
2867
+ const nextSegment = segments[index + 1];
2868
+ if (segment === void 0) continue;
2869
+ if (segment === "content" && nextSegment !== void 0 && /^\d+$/.test(nextSegment)) {
2870
+ parts.push(`${parts.length === 0 ? "Content" : "content"} item ${Number(nextSegment) + 1}`);
2871
+ index += 1;
2872
+ continue;
2873
+ }
2874
+ if (segment === "validation" && parts.length > 0) {
2875
+ parts[parts.length - 1] = `${parts[parts.length - 1]} validation`;
2876
+ continue;
2877
+ }
2878
+ parts.push(segment);
2879
+ }
2880
+ return parts.join(" > ");
2881
+ }
2882
+ getValueAtPath(input, path) {
2883
+ if (!path) return input;
2884
+ return path.split("/").filter(Boolean).reduce((value, segment) => {
2885
+ if (Array.isArray(value)) return value[Number(segment)];
2886
+ if (this.isRecord(value)) return value[segment];
2887
+ }, input);
2888
+ }
2889
+ getParentPath(path) {
2890
+ const segments = path.split("/").filter(Boolean);
2891
+ return segments.length > 1 ? `/${segments.slice(0, -1).join("/")}` : "/";
2892
+ }
2893
+ getLastPathSegment(path) {
2894
+ return path.split("/").filter(Boolean).at(-1);
2895
+ }
2896
+ isContentItemPath(path) {
2897
+ const segments = path.split("/").filter(Boolean);
2898
+ return segments.at(-2) === "content" && /^\d+$/.test(segments.at(-1) ?? "");
2899
+ }
2900
+ formatArticle(value) {
2901
+ if (!value) return "a";
2902
+ return /^[aeiou]/i.test(value) ? "an" : "a";
2903
+ }
2904
+ isRecord(value) {
2905
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1564
2906
  }
1565
2907
  /**
1566
2908
  * Validates a form definition semantically.
@@ -1573,23 +2915,52 @@ var FormDefinitionValidator = class {
1573
2915
  const issues = [];
1574
2916
  this.checkDuplicateIds(definition.content, issues);
1575
2917
  this.checkNestingDepth(definition.content, 0, issues);
1576
- this.checkConditionRefs(registry, issues);
1577
- this.checkConditionRefsSection(registry, issues);
2918
+ const arrayObjItemIds = this.collectArrayObjItemIds(definition.content);
2919
+ this.checkConditionRefsArrayObjItem(registry, arrayObjItemIds, issues);
2920
+ this.checkConditionRefs(registry, arrayObjItemIds, issues);
2921
+ this.checkConditionRefsValueless(registry, issues);
1578
2922
  this.checkConstraintContradictions(registry, issues);
1579
2923
  this.checkInvalidRegex(registry, issues);
1580
2924
  return issues;
1581
2925
  }
1582
2926
  checkDuplicateIds(content, issues) {
1583
2927
  const seen = /* @__PURE__ */ new Set();
1584
- this.walkItems(content, (item) => {
1585
- if (seen.has(item.id)) issues.push({
2928
+ const record = (id) => {
2929
+ if (seen.has(id)) issues.push({
1586
2930
  code: "DUPLICATE_ID",
1587
- message: `Duplicate id: ${item.id}`,
1588
- itemId: item.id
2931
+ message: `Duplicate id: ${id}`,
2932
+ itemId: id
1589
2933
  });
1590
- else seen.add(item.id);
2934
+ else seen.add(id);
2935
+ };
2936
+ this.walkItems(content, (item) => {
2937
+ record(item.id);
2938
+ if (item.type === "array_obj") for (const subField of item.items ?? []) record(subField.id);
1591
2939
  });
1592
2940
  }
2941
+ collectArrayObjItemIds(content) {
2942
+ const ids = /* @__PURE__ */ new Map();
2943
+ this.walkItems(content, (item) => {
2944
+ if (item.type !== "array_obj") return;
2945
+ for (const subField of item.items ?? []) if (!ids.has(subField.id)) ids.set(subField.id, item.id);
2946
+ });
2947
+ return ids;
2948
+ }
2949
+ checkConditionRefsArrayObjItem(registry, arrayObjItemIds, issues) {
2950
+ if (arrayObjItemIds.size === 0) return;
2951
+ for (const [id, entry] of registry) {
2952
+ if (!entry.condition) continue;
2953
+ for (const ref of DependencyGraph.extractFieldRefs(entry.condition)) {
2954
+ const containerId = arrayObjItemIds.get(ref);
2955
+ if (containerId === void 0) continue;
2956
+ issues.push({
2957
+ code: "CONDITION_REFS_ARRAY_OBJ_ITEM",
2958
+ message: `Condition references sub-field ${ref} of array_obj field ${containerId}, whose value is per-row and not addressable (in item ${id})`,
2959
+ itemId: id
2960
+ });
2961
+ }
2962
+ }
2963
+ }
1593
2964
  checkNestingDepth(content, depth, issues) {
1594
2965
  for (const item of content) if (item.type === "section") if (depth >= 3) issues.push({
1595
2966
  code: "NESTING_DEPTH",
@@ -1598,28 +2969,37 @@ var FormDefinitionValidator = class {
1598
2969
  });
1599
2970
  else this.checkNestingDepth(item.content, depth + 1, issues);
1600
2971
  }
1601
- checkConditionRefs(registry, issues) {
2972
+ checkConditionRefs(registry, arrayObjItemIds, issues) {
1602
2973
  for (const [id, entry] of registry) {
1603
2974
  if (!entry.condition) continue;
1604
2975
  const refs = DependencyGraph.extractFieldRefs(entry.condition);
1605
- for (const ref of refs) if (!registry.has(ref)) issues.push({
1606
- code: "UNKNOWN_FIELD_REF",
1607
- message: `Condition references unknown field: ${ref} (in item ${id})`,
1608
- itemId: id
1609
- });
2976
+ for (const ref of refs) {
2977
+ if (arrayObjItemIds.has(ref)) continue;
2978
+ if (!registry.has(ref)) issues.push({
2979
+ code: "UNKNOWN_FIELD_REF",
2980
+ message: `Condition references unknown field: ${ref} (in item ${id})`,
2981
+ itemId: id
2982
+ });
2983
+ }
1610
2984
  }
1611
2985
  }
1612
- checkConditionRefsSection(registry, issues) {
2986
+ checkConditionRefsValueless(registry, issues) {
1613
2987
  for (const [id, entry] of registry) {
1614
2988
  if (!entry.condition) continue;
1615
2989
  const refs = DependencyGraph.extractFieldRefs(entry.condition);
1616
2990
  for (const ref of refs) {
1617
2991
  const refEntry = registry.get(ref);
1618
- if (refEntry && refEntry.type === "section") issues.push({
2992
+ if (!refEntry) continue;
2993
+ if (refEntry.type === "section") issues.push({
1619
2994
  code: "CONDITION_REFS_SECTION",
1620
2995
  message: `Condition references section ${ref}, which has no value (in item ${id})`,
1621
2996
  itemId: id
1622
2997
  });
2998
+ if (refEntry.type === "static") issues.push({
2999
+ code: "CONDITION_REFS_STATIC",
3000
+ message: `Condition references static field ${ref}, which has no value (in item ${id})`,
3001
+ itemId: id
3002
+ });
1623
3003
  }
1624
3004
  }
1625
3005
  }
@@ -1660,7 +3040,8 @@ var FormDefinitionValidator = class {
1660
3040
  }
1661
3041
  break;
1662
3042
  }
1663
- case "array": {
3043
+ case "array":
3044
+ case "array_obj": {
1664
3045
  const v = entry.validation;
1665
3046
  if (v.minItems !== void 0 && v.maxItems !== void 0 && v.maxItems < v.minItems) issues.push({
1666
3047
  code: "INVALID_MIN_MAX",
@@ -1746,6 +3127,7 @@ var VisibilityResolver = class {
1746
3127
  registry;
1747
3128
  conditionEvaluator;
1748
3129
  topologicalOrder;
3130
+ fieldTypes;
1749
3131
  /**
1750
3132
  * @param registry - The engine's field registry.
1751
3133
  * @param conditionEvaluator - Evaluator for condition trees.
@@ -1755,6 +3137,7 @@ var VisibilityResolver = class {
1755
3137
  this.registry = registry;
1756
3138
  this.conditionEvaluator = conditionEvaluator;
1757
3139
  this.topologicalOrder = topologicalOrder;
3140
+ this.fieldTypes = new Map([...registry].map(([id, entry]) => [id, entry.type]));
1758
3141
  }
1759
3142
  /**
1760
3143
  * Determines whether a single field or section is visible.
@@ -1781,7 +3164,8 @@ var VisibilityResolver = class {
1781
3164
  if (entry.condition) {
1782
3165
  if (!this.conditionEvaluator.evalCondition(entry.condition, {
1783
3166
  values,
1784
- now
3167
+ now,
3168
+ fieldTypes: this.fieldTypes
1785
3169
  })) return false;
1786
3170
  }
1787
3171
  if (entry.parentId !== void 0) return this.isVisible(entry.parentId, values, now);
@@ -1819,7 +3203,8 @@ var VisibilityResolver = class {
1819
3203
  const visible = this.conditionEvaluator.evalCondition(entry.condition, {
1820
3204
  values,
1821
3205
  visibilityMap: result,
1822
- now
3206
+ now,
3207
+ fieldTypes: this.fieldTypes
1823
3208
  });
1824
3209
  result.set(id, visible);
1825
3210
  } else result.set(id, true);
@@ -2086,14 +3471,16 @@ var FormEngine = class FormEngine {
2086
3471
  }
2087
3472
  static walkContent(content, parentId, registry, contentOrder) {
2088
3473
  for (const item of content) {
3474
+ const isValueField = item.type !== "section" && item.type !== "static";
2089
3475
  const entry = {
2090
3476
  id: item.id,
2091
3477
  type: item.type,
2092
3478
  condition: item.condition,
2093
- validation: item.type !== "section" ? item.validation : void 0,
3479
+ validation: isValueField ? item.validation : void 0,
2094
3480
  parentId,
2095
- options: item.type === "select" ? item.options : void 0,
3481
+ options: item.type === "select" || item.type === "multiselect" ? item.options : void 0,
2096
3482
  item: item.type === "array" ? item.item : void 0,
3483
+ items: item.type === "array_obj" ? item.items : void 0,
2097
3484
  label: item.type !== "section" ? item.label : void 0,
2098
3485
  title: item.type === "section" ? item.title : void 0
2099
3486
  };
@@ -2104,6 +3491,108 @@ var FormEngine = class FormEngine {
2104
3491
  }
2105
3492
  };
2106
3493
  //#endregion
3494
+ //#region src/suggestions.ts
3495
+ /**
3496
+ * Pure helpers for reading and writing the `suggestions` block of a
3497
+ * {@link FormDocument}.
3498
+ *
3499
+ * Every function is total over legacy documents: `doc.suggestions` may be
3500
+ * `undefined` (documents written before suggestions existed) and each helper
3501
+ * treats that as "no suggestions". Writers never mutate their input -- they
3502
+ * return a new document -- and they drop the `suggestions` key entirely once it
3503
+ * would be empty, so a document that never had suggestions round-trips
3504
+ * unchanged.
3505
+ *
3506
+ * These helpers deliberately do **not** check that `fieldId` refers to an
3507
+ * existing, non-section field. That check belongs to the general-purpose
3508
+ * authoring API ({@link FormValuesEditor}); callers that already resolved a
3509
+ * field -- such as the viewer's render loop -- would only pay for it twice.
3510
+ */
3511
+ /**
3512
+ * Returns the suggestion recorded for a field.
3513
+ *
3514
+ * @param doc - The form document to read from.
3515
+ * @param fieldId - Numeric id of the field.
3516
+ * @returns The suggestion, or `undefined` when the field has none.
3517
+ */
3518
+ const getSuggestion = (doc, fieldId) => doc.suggestions?.[String(fieldId)];
3519
+ /**
3520
+ * Records a suggestion for a field, replacing any existing one.
3521
+ *
3522
+ * @param doc - The form document to update.
3523
+ * @param fieldId - Numeric id of the field.
3524
+ * @param suggestion - The suggestion to store. Deep-copied before storing.
3525
+ * @returns A new document carrying the suggestion.
3526
+ * @throws If `confidence` is not a finite number within `0..1`.
3527
+ */
3528
+ const setSuggestion = (doc, fieldId, suggestion) => {
3529
+ const { confidence } = suggestion;
3530
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw new Error(`Suggestion confidence must be a number between 0 and 1, received ${confidence}`);
3531
+ return {
3532
+ ...doc,
3533
+ suggestions: {
3534
+ ...doc.suggestions,
3535
+ [String(fieldId)]: deepCopy(suggestion)
3536
+ }
3537
+ };
3538
+ };
3539
+ /**
3540
+ * Removes the suggestion recorded for a field, leaving the field's value alone.
3541
+ *
3542
+ * When the removed entry was the last one, the `suggestions` key is dropped
3543
+ * from the document rather than left as an empty object.
3544
+ *
3545
+ * @param doc - The form document to update.
3546
+ * @param fieldId - Numeric id of the field.
3547
+ * @returns A new document without that suggestion, or `doc` itself when there
3548
+ * was nothing to remove.
3549
+ */
3550
+ const removeSuggestion = (doc, fieldId) => {
3551
+ const key = String(fieldId);
3552
+ if (!doc.suggestions || !(key in doc.suggestions)) return doc;
3553
+ const { [key]: _removed, ...rest } = doc.suggestions;
3554
+ if (Object.keys(rest).length === 0) {
3555
+ const { suggestions: _dropped, ...withoutSuggestions } = doc;
3556
+ return withoutSuggestions;
3557
+ }
3558
+ return {
3559
+ ...doc,
3560
+ suggestions: rest
3561
+ };
3562
+ };
3563
+ /**
3564
+ * Copies a field's suggested value into the document's values, keeping the
3565
+ * suggestion in place so the origin of the answer stays visible.
3566
+ *
3567
+ * The value is deep-copied: array and object suggestions must not become
3568
+ * aliases of the stored value, or later in-place edits (e.g. appending an array
3569
+ * item) would silently rewrite the suggestion too.
3570
+ *
3571
+ * @param doc - The form document to update.
3572
+ * @param fieldId - Numeric id of the field.
3573
+ * @returns A new document with the value applied, or `doc` itself when the
3574
+ * field has no suggestion.
3575
+ */
3576
+ const applySuggestion = (doc, fieldId) => {
3577
+ const suggestion = getSuggestion(doc, fieldId);
3578
+ if (!suggestion) return doc;
3579
+ return {
3580
+ ...doc,
3581
+ values: {
3582
+ ...doc.values,
3583
+ [String(fieldId)]: deepCopy(suggestion.value)
3584
+ }
3585
+ };
3586
+ };
3587
+ /**
3588
+ * Returns all suggestions on a document as a plain object, never `undefined`.
3589
+ *
3590
+ * @param doc - The form document to read from.
3591
+ * @returns A copy of the suggestions map. Empty when the document has none.
3592
+ */
3593
+ const getSuggestions = (doc) => ({ ...doc.suggestions });
3594
+ const deepCopy = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
3595
+ //#endregion
2107
3596
  //#region src/form-values-editor.ts
2108
3597
  /**
2109
3598
  * Mutable editor for building and modifying form values against a {@link FormDefinition}.
@@ -2175,13 +3664,16 @@ var FormValuesEditor = class {
2175
3664
  * If the field currently has no value, it is initialized to an empty array
2176
3665
  * before appending.
2177
3666
  *
2178
- * @param fieldId - Numeric id of the array field.
2179
- * @param value - The value to append. Defaults to `undefined`.
3667
+ * @param fieldId - Numeric id of the `array` or `array_obj` field.
3668
+ * @param value - The value to append. Defaults to `undefined` for an
3669
+ * `array` field and to an empty row (`{}`) for an `array_obj` field.
2180
3670
  * @returns `this` for chaining.
2181
3671
  * @throws If `fieldId` is not an array field.
2182
3672
  */
2183
3673
  addArrayItem(fieldId, value) {
2184
- this.getOrInitArray(fieldId).push(value);
3674
+ const arr = this.getOrInitArray(fieldId);
3675
+ const entry = this.engine.getFieldDef(fieldId);
3676
+ arr.push(value === void 0 && entry?.type === "array_obj" ? {} : value);
2185
3677
  return this;
2186
3678
  }
2187
3679
  /**
@@ -2231,6 +3723,114 @@ var FormValuesEditor = class {
2231
3723
  return this;
2232
3724
  }
2233
3725
  /**
3726
+ * Returns the value of one sub-field within one row of an `array_obj` field.
3727
+ *
3728
+ * @param fieldId - Numeric id of the `array_obj` field.
3729
+ * @param index - Zero-based row index.
3730
+ * @param subFieldId - Numeric id of the sub-field.
3731
+ * @returns The sub-field value, or `undefined` if not set.
3732
+ * @throws If the field is not an `array_obj`, the row index is out of
3733
+ * bounds, or the sub-field does not belong to the field.
3734
+ */
3735
+ getArrayObjValue(fieldId, index, subFieldId) {
3736
+ return this.assertArrayObjRow(fieldId, index, subFieldId)[String(subFieldId)];
3737
+ }
3738
+ /**
3739
+ * Sets the value of one sub-field within one row of an `array_obj` field.
3740
+ *
3741
+ * @param fieldId - Numeric id of the `array_obj` field.
3742
+ * @param index - Zero-based row index.
3743
+ * @param subFieldId - Numeric id of the sub-field.
3744
+ * @param value - The value to set.
3745
+ * @returns `this` for chaining.
3746
+ * @throws If the field is not an `array_obj`, the row index is out of
3747
+ * bounds, or the sub-field does not belong to the field or is static.
3748
+ */
3749
+ setArrayObjValue(fieldId, index, subFieldId, value) {
3750
+ this.assertArrayObjRow(fieldId, index, subFieldId)[String(subFieldId)] = value;
3751
+ return this;
3752
+ }
3753
+ /**
3754
+ * Removes one sub-field's value from one row of an `array_obj` field.
3755
+ *
3756
+ * @returns `this` for chaining.
3757
+ * @throws Under the same conditions as {@link setArrayObjValue}.
3758
+ */
3759
+ clearArrayObjValue(fieldId, index, subFieldId) {
3760
+ delete this.assertArrayObjRow(fieldId, index, subFieldId)[String(subFieldId)];
3761
+ return this;
3762
+ }
3763
+ /**
3764
+ * Returns the suggestion recorded for a field.
3765
+ *
3766
+ * @param fieldId - Numeric id of the field.
3767
+ * @returns The suggestion, or `undefined` if the field has none.
3768
+ */
3769
+ getSuggestion(fieldId) {
3770
+ return getSuggestion(this.doc, fieldId);
3771
+ }
3772
+ /**
3773
+ * Returns every suggestion on the document, keyed by stringified field id.
3774
+ *
3775
+ * @returns A copy of the suggestions map. Empty when there are none.
3776
+ */
3777
+ getSuggestions() {
3778
+ return getSuggestions(this.doc);
3779
+ }
3780
+ /**
3781
+ * Records a suggestion for a field, replacing any existing one. The field's
3782
+ * value is left untouched.
3783
+ *
3784
+ * @param fieldId - Numeric id of the field.
3785
+ * @param suggestion - The suggestion to store.
3786
+ * @returns `this` for chaining.
3787
+ * @throws If `fieldId` is unknown, references a section, or `confidence`
3788
+ * falls outside `0..1`.
3789
+ */
3790
+ setSuggestion(fieldId, suggestion) {
3791
+ this.assertField(fieldId);
3792
+ this.doc = setSuggestion(this.doc, fieldId, suggestion);
3793
+ return this;
3794
+ }
3795
+ /**
3796
+ * Removes the suggestion recorded for a field, leaving its value untouched.
3797
+ *
3798
+ * @param fieldId - Numeric id of the field.
3799
+ * @returns `this` for chaining.
3800
+ */
3801
+ clearSuggestion(fieldId) {
3802
+ this.doc = removeSuggestion(this.doc, fieldId);
3803
+ return this;
3804
+ }
3805
+ /**
3806
+ * Accepts a field's suggestion: copies the suggested value into the field's
3807
+ * value and **keeps** the suggestion, so its source stays visible.
3808
+ *
3809
+ * No-op when the field has no suggestion.
3810
+ *
3811
+ * @param fieldId - Numeric id of the field.
3812
+ * @returns `this` for chaining.
3813
+ * @throws If `fieldId` is unknown or references a section.
3814
+ */
3815
+ acceptSuggestion(fieldId) {
3816
+ this.assertField(fieldId);
3817
+ this.doc = applySuggestion(this.doc, fieldId);
3818
+ return this;
3819
+ }
3820
+ /**
3821
+ * Rejects a field's suggestion: removes it entirely, leaving the field's
3822
+ * value untouched.
3823
+ *
3824
+ * No-op when the field has no suggestion.
3825
+ *
3826
+ * @param fieldId - Numeric id of the field.
3827
+ * @returns `this` for chaining.
3828
+ */
3829
+ rejectSuggestion(fieldId) {
3830
+ this.doc = removeSuggestion(this.doc, fieldId);
3831
+ return this;
3832
+ }
3833
+ /**
2234
3834
  * Sets the `submittedAt` timestamp on the document.
2235
3835
  *
2236
3836
  * @param submittedAt - ISO 8601 timestamp string.
@@ -2286,6 +3886,7 @@ var FormValuesEditor = class {
2286
3886
  const entry = this.engine.getFieldDef(fieldId);
2287
3887
  if (!entry) throw new Error(`Field with id ${fieldId} not found`);
2288
3888
  if (entry.type === "section") throw new Error(`Item ${fieldId} is a section, not a field`);
3889
+ if (entry.type === "static") throw new Error(`Item ${fieldId} is a static field and holds no value`);
2289
3890
  }
2290
3891
  /**
2291
3892
  * Asserts that `fieldId` is an array field and returns the current array value.
@@ -2294,7 +3895,7 @@ var FormValuesEditor = class {
2294
3895
  assertArray(fieldId) {
2295
3896
  const entry = this.engine.getFieldDef(fieldId);
2296
3897
  if (!entry) throw new Error(`Field with id ${fieldId} not found`);
2297
- if (entry.type !== "array") throw new Error(`Field ${fieldId} is not an array field`);
3898
+ if (entry.type !== "array" && entry.type !== "array_obj") throw new Error(`Field ${fieldId} is not an array field`);
2298
3899
  const val = this.doc.values[String(fieldId)];
2299
3900
  if (!Array.isArray(val)) throw new Error(`Field ${fieldId} does not currently hold an array value`);
2300
3901
  return val;
@@ -2305,7 +3906,7 @@ var FormValuesEditor = class {
2305
3906
  getOrInitArray(fieldId) {
2306
3907
  const entry = this.engine.getFieldDef(fieldId);
2307
3908
  if (!entry) throw new Error(`Field with id ${fieldId} not found`);
2308
- if (entry.type !== "array") throw new Error(`Field ${fieldId} is not an array field`);
3909
+ if (entry.type !== "array" && entry.type !== "array_obj") throw new Error(`Field ${fieldId} is not an array field`);
2309
3910
  const key = String(fieldId);
2310
3911
  let val = this.doc.values[key];
2311
3912
  if (!Array.isArray(val)) {
@@ -2314,8 +3915,28 @@ var FormValuesEditor = class {
2314
3915
  }
2315
3916
  return val;
2316
3917
  }
3918
+ /**
3919
+ * Asserts that `fieldId` is an `array_obj` field holding a row at `index`
3920
+ * with a writable sub-field `subFieldId`, and returns that row.
3921
+ * The row is created in place when it is absent or not an object.
3922
+ */
3923
+ assertArrayObjRow(fieldId, index, subFieldId) {
3924
+ const entry = this.engine.getFieldDef(fieldId);
3925
+ if (!entry) throw new Error(`Field with id ${fieldId} not found`);
3926
+ if (entry.type !== "array_obj") throw new Error(`Field ${fieldId} is not an array_obj field`);
3927
+ const subField = (entry.items ?? []).find((f) => f.id === subFieldId);
3928
+ if (!subField) throw new Error(`Sub-field ${subFieldId} does not belong to field ${fieldId}`);
3929
+ if (subField.type === "static") throw new Error(`Sub-field ${subFieldId} is a static field and holds no value`);
3930
+ const arr = this.assertArray(fieldId);
3931
+ if (index < 0 || index >= arr.length) throw new Error(`Index ${index} is out of bounds for array field ${fieldId} (length ${arr.length})`);
3932
+ const row = arr[index];
3933
+ if (typeof row === "object" && row !== null && !Array.isArray(row)) return row;
3934
+ const created = {};
3935
+ arr[index] = created;
3936
+ return created;
3937
+ }
2317
3938
  };
2318
3939
  //#endregion
2319
- export { ConditionEvaluator, DependencyGraph, DocumentError, FieldValidator, FormDefinitionEditor, FormDefinitionValidator, FormEngine, FormValuesEditor, VisibilityResolver };
3940
+ export { ConditionEvaluator, DependencyGraph, DocumentError, FieldValidator, FormDefinitionEditor, FormDefinitionValidator, FormEngine, FormValuesEditor, VisibilityResolver, applySuggestion, getSuggestion, getSuggestions, removeSuggestion, setSuggestion };
2320
3941
 
2321
3942
  //# sourceMappingURL=index.mjs.map