@webiny/app-admin 6.3.0-beta.2 → 6.3.0-beta.3

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.
Files changed (234) hide show
  1. package/base/Base/DefaultFieldRenderers.js +69 -5
  2. package/base/Base/DefaultFieldRenderers.js.map +1 -1
  3. package/base/Base/DefaultLayoutRenderers.js +5 -1
  4. package/base/Base/DefaultLayoutRenderers.js.map +1 -1
  5. package/base/Base/FieldRenderers/CheckboxesRenderer.d.ts +13 -0
  6. package/base/Base/FieldRenderers/CheckboxesRenderer.js +28 -0
  7. package/base/Base/FieldRenderers/CheckboxesRenderer.js.map +1 -0
  8. package/base/Base/FieldRenderers/CodeEditorRenderer.d.ts +15 -0
  9. package/base/Base/FieldRenderers/CodeEditorRenderer.js +17 -0
  10. package/base/Base/FieldRenderers/CodeEditorRenderer.js.map +1 -0
  11. package/base/Base/FieldRenderers/DateTimeInputsRenderer.d.ts +17 -0
  12. package/base/Base/FieldRenderers/DateTimeInputsRenderer.js +66 -0
  13. package/base/Base/FieldRenderers/DateTimeInputsRenderer.js.map +1 -0
  14. package/base/Base/FieldRenderers/DateTimeRenderer.d.ts +21 -0
  15. package/base/Base/FieldRenderers/DateTimeRenderer.js +46 -0
  16. package/base/Base/FieldRenderers/DateTimeRenderer.js.map +1 -0
  17. package/base/Base/FieldRenderers/FilePickerRenderer.d.ts +12 -0
  18. package/base/Base/FieldRenderers/FilePickerRenderer.js +47 -0
  19. package/base/Base/FieldRenderers/FilePickerRenderer.js.map +1 -0
  20. package/base/Base/FieldRenderers/FileUrlPickerRenderer.d.ts +12 -0
  21. package/base/Base/FieldRenderers/FileUrlPickerRenderer.js +25 -0
  22. package/base/Base/FieldRenderers/FileUrlPickerRenderer.js.map +1 -0
  23. package/base/Base/FieldRenderers/HiddenRenderer.d.ts +12 -0
  24. package/base/Base/FieldRenderers/HiddenRenderer.js +5 -0
  25. package/base/Base/FieldRenderers/HiddenRenderer.js.map +1 -0
  26. package/base/Base/FieldRenderers/HorizontalTabsRenderer.d.ts +5 -0
  27. package/base/Base/FieldRenderers/HorizontalTabsRenderer.js +27 -0
  28. package/base/Base/FieldRenderers/HorizontalTabsRenderer.js.map +1 -0
  29. package/base/Base/FieldRenderers/InputRenderer.d.ts +4 -7
  30. package/base/Base/FieldRenderers/InputRenderer.js +2 -2
  31. package/base/Base/FieldRenderers/InputRenderer.js.map +1 -1
  32. package/base/Base/FieldRenderers/NumberInputRenderer.d.ts +12 -0
  33. package/base/Base/FieldRenderers/NumberInputRenderer.js +23 -0
  34. package/base/Base/FieldRenderers/NumberInputRenderer.js.map +1 -0
  35. package/base/Base/FieldRenderers/NumberInputsRenderer.d.ts +14 -0
  36. package/base/Base/FieldRenderers/NumberInputsRenderer.js +49 -0
  37. package/base/Base/FieldRenderers/NumberInputsRenderer.js.map +1 -0
  38. package/base/Base/FieldRenderers/ObjectRenderer/DynamicZoneRenderer.d.ts +14 -0
  39. package/base/Base/FieldRenderers/ObjectRenderer/DynamicZoneRenderer.js +20 -0
  40. package/base/Base/FieldRenderers/ObjectRenderer/DynamicZoneRenderer.js.map +1 -0
  41. package/base/Base/FieldRenderers/ObjectRenderer/KeyValueTagsRenderer.d.ts +14 -0
  42. package/base/Base/FieldRenderers/ObjectRenderer/KeyValueTagsRenderer.js +65 -0
  43. package/base/Base/FieldRenderers/ObjectRenderer/KeyValueTagsRenderer.js.map +1 -0
  44. package/base/Base/FieldRenderers/ObjectRenderer/MultiValueDynamicZone.d.ts +10 -0
  45. package/base/Base/FieldRenderers/ObjectRenderer/MultiValueDynamicZone.js +109 -0
  46. package/base/Base/FieldRenderers/ObjectRenderer/MultiValueDynamicZone.js.map +1 -0
  47. package/base/Base/FieldRenderers/ObjectRenderer/ObjectAccordionMultipleRenderer.d.ts +17 -0
  48. package/base/Base/FieldRenderers/ObjectRenderer/ObjectAccordionMultipleRenderer.js +55 -0
  49. package/base/Base/FieldRenderers/ObjectRenderer/ObjectAccordionMultipleRenderer.js.map +1 -0
  50. package/base/Base/FieldRenderers/ObjectRenderer/ObjectFieldComponents.d.ts +7 -3
  51. package/base/Base/FieldRenderers/ObjectRenderer/ObjectFieldComponents.js +15 -19
  52. package/base/Base/FieldRenderers/ObjectRenderer/ObjectFieldComponents.js.map +1 -1
  53. package/base/Base/FieldRenderers/ObjectRenderer/ObjectRenderer.d.ts +5 -8
  54. package/base/Base/FieldRenderers/ObjectRenderer/ObjectRenderer.js +7 -50
  55. package/base/Base/FieldRenderers/ObjectRenderer/ObjectRenderer.js.map +1 -1
  56. package/base/Base/FieldRenderers/ObjectRenderer/SingleValueDynamicZone.d.ts +10 -0
  57. package/base/Base/FieldRenderers/ObjectRenderer/SingleValueDynamicZone.js +64 -0
  58. package/base/Base/FieldRenderers/ObjectRenderer/SingleValueDynamicZone.js.map +1 -0
  59. package/base/Base/FieldRenderers/ObjectRenderer/TemplatePicker.d.ts +10 -0
  60. package/base/Base/FieldRenderers/ObjectRenderer/TemplatePicker.js +85 -0
  61. package/base/Base/FieldRenderers/ObjectRenderer/TemplatePicker.js.map +1 -0
  62. package/base/Base/FieldRenderers/PassthroughRenderer.d.ts +3 -6
  63. package/base/Base/FieldRenderers/PassthroughRenderer.js +9 -23
  64. package/base/Base/FieldRenderers/PassthroughRenderer.js.map +1 -1
  65. package/base/Base/FieldRenderers/RadioButtonsRenderer.d.ts +13 -0
  66. package/base/Base/FieldRenderers/RadioButtonsRenderer.js +27 -0
  67. package/base/Base/FieldRenderers/RadioButtonsRenderer.js.map +1 -0
  68. package/base/Base/FieldRenderers/SelectRenderer.d.ts +6 -8
  69. package/base/Base/FieldRenderers/SelectRenderer.js +8 -5
  70. package/base/Base/FieldRenderers/SelectRenderer.js.map +1 -1
  71. package/base/Base/FieldRenderers/SwitchRenderer.d.ts +12 -0
  72. package/base/Base/FieldRenderers/SwitchRenderer.js +19 -0
  73. package/base/Base/FieldRenderers/SwitchRenderer.js.map +1 -0
  74. package/base/Base/FieldRenderers/TagsRenderer.d.ts +12 -0
  75. package/base/Base/FieldRenderers/TagsRenderer.js +21 -0
  76. package/base/Base/FieldRenderers/TagsRenderer.js.map +1 -0
  77. package/base/Base/FieldRenderers/TextInputsRenderer.d.ts +14 -0
  78. package/base/Base/FieldRenderers/TextInputsRenderer.js +48 -0
  79. package/base/Base/FieldRenderers/TextInputsRenderer.js.map +1 -0
  80. package/base/Base/FieldRenderers/TextareaRenderer.d.ts +3 -6
  81. package/base/Base/FieldRenderers/TextareaRenderer.js +3 -4
  82. package/base/Base/FieldRenderers/TextareaRenderer.js.map +1 -1
  83. package/base/Base/FieldRenderers/TextareasRenderer.d.ts +14 -0
  84. package/base/Base/FieldRenderers/TextareasRenderer.js +51 -0
  85. package/base/Base/FieldRenderers/TextareasRenderer.js.map +1 -0
  86. package/base/Base/FieldRenderers/VerticalTabsRenderer.js +2 -2
  87. package/base/Base/FieldRenderers/VerticalTabsRenderer.js.map +1 -1
  88. package/base/Base/Menus.js +5 -64
  89. package/base/Base/Menus.js.map +1 -1
  90. package/base/Base/RoutesConfig.js +6 -0
  91. package/base/Base/RoutesConfig.js.map +1 -1
  92. package/exports/admin/build-params.d.ts +2 -0
  93. package/exports/admin/build-params.js +3 -0
  94. package/exports/admin/build-params.js.map +1 -1
  95. package/exports/admin/form.d.ts +5 -0
  96. package/exports/admin/form.js +8 -0
  97. package/exports/admin/form.js.map +1 -1
  98. package/exports/admin/ui.d.ts +1 -0
  99. package/exports/admin/ui.js +1 -0
  100. package/exports/admin/ui.js.map +1 -1
  101. package/exports/admin.d.ts +3 -1
  102. package/exports/admin.js +3 -1
  103. package/exports/admin.js.map +1 -1
  104. package/features/formModel/ConditionRuleEvaluator.d.ts +9 -0
  105. package/features/formModel/ConditionRuleEvaluator.js +56 -0
  106. package/features/formModel/ConditionRuleEvaluator.js.map +1 -0
  107. package/features/formModel/Field.d.ts +50 -4
  108. package/features/formModel/Field.js +254 -35
  109. package/features/formModel/Field.js.map +1 -1
  110. package/features/formModel/FieldBuilder.d.ts +17 -35
  111. package/features/formModel/FieldBuilder.js +63 -100
  112. package/features/formModel/FieldBuilder.js.map +1 -1
  113. package/features/formModel/FieldBuilder.test.js +127 -13
  114. package/features/formModel/FieldBuilder.test.js.map +1 -1
  115. package/features/formModel/FieldBuilderRegistry.d.ts +4 -0
  116. package/features/formModel/FieldBuilderRegistry.js +31 -0
  117. package/features/formModel/FieldBuilderRegistry.js.map +1 -0
  118. package/features/formModel/FocusManager.d.ts +14 -0
  119. package/features/formModel/FocusManager.js +109 -0
  120. package/features/formModel/FocusManager.js.map +1 -0
  121. package/features/formModel/FormModel.d.ts +27 -31
  122. package/features/formModel/FormModel.js +210 -403
  123. package/features/formModel/FormModel.js.map +1 -1
  124. package/features/formModel/FormModel.test.js +2044 -193
  125. package/features/formModel/FormModel.test.js.map +1 -1
  126. package/features/formModel/FormModelFactory.d.ts +4 -2
  127. package/features/formModel/FormModelFactory.js +13 -3
  128. package/features/formModel/FormModelFactory.js.map +1 -1
  129. package/features/formModel/FormView.d.ts +2 -0
  130. package/features/formModel/FormView.js +44 -37
  131. package/features/formModel/FormView.js.map +1 -1
  132. package/features/formModel/LayoutBuilderFactory.d.ts +61 -0
  133. package/features/formModel/LayoutBuilderFactory.js +386 -0
  134. package/features/formModel/LayoutBuilderFactory.js.map +1 -0
  135. package/features/formModel/LayoutMutator.d.ts +11 -0
  136. package/features/formModel/LayoutMutator.js +136 -0
  137. package/features/formModel/LayoutMutator.js.map +1 -0
  138. package/features/formModel/LayoutResolver.d.ts +26 -0
  139. package/features/formModel/LayoutResolver.js +239 -0
  140. package/features/formModel/LayoutResolver.js.map +1 -0
  141. package/features/formModel/ObjectField.d.ts +55 -4
  142. package/features/formModel/ObjectField.js +499 -82
  143. package/features/formModel/ObjectField.js.map +1 -1
  144. package/features/formModel/Rules.test.d.ts +1 -0
  145. package/features/formModel/Rules.test.js +289 -0
  146. package/features/formModel/Rules.test.js.map +1 -0
  147. package/features/formModel/abstractions.d.ts +402 -52
  148. package/features/formModel/abstractions.js +55 -0
  149. package/features/formModel/abstractions.js.map +1 -1
  150. package/features/formModel/createFieldRenderer.d.ts +20 -0
  151. package/features/formModel/createFieldRenderer.js +15 -0
  152. package/features/formModel/createFieldRenderer.js.map +1 -0
  153. package/features/formModel/demo/FieldRenderersDemoPresenter.d.ts +18 -0
  154. package/features/formModel/demo/FieldRenderersDemoPresenter.js +225 -0
  155. package/features/formModel/demo/FieldRenderersDemoPresenter.js.map +1 -0
  156. package/features/formModel/demo/FormModelDemo.d.ts +4 -0
  157. package/features/formModel/demo/FormModelDemo.js +230 -0
  158. package/features/formModel/demo/FormModelDemo.js.map +1 -0
  159. package/features/formModel/demo/FormModelDemoPresenter.d.ts +22 -0
  160. package/features/formModel/demo/FormModelDemoPresenter.js +121 -0
  161. package/features/formModel/demo/FormModelDemoPresenter.js.map +1 -0
  162. package/features/formModel/demo/FormModelPhase11Presenter.d.ts +25 -0
  163. package/features/formModel/demo/FormModelPhase11Presenter.js +104 -0
  164. package/features/formModel/demo/FormModelPhase11Presenter.js.map +1 -0
  165. package/features/formModel/demo/FormModelPhase8c1Presenter.d.ts +23 -0
  166. package/features/formModel/demo/FormModelPhase8c1Presenter.js +62 -0
  167. package/features/formModel/demo/FormModelPhase8c1Presenter.js.map +1 -0
  168. package/features/formModel/feature.js +12 -0
  169. package/features/formModel/feature.js.map +1 -1
  170. package/features/formModel/fieldTypes/BooleanFieldType.d.ts +19 -0
  171. package/features/formModel/fieldTypes/BooleanFieldType.js +23 -0
  172. package/features/formModel/fieldTypes/BooleanFieldType.js.map +1 -0
  173. package/features/formModel/fieldTypes/DateTimeFieldType.d.ts +173 -0
  174. package/features/formModel/fieldTypes/DateTimeFieldType.js +369 -0
  175. package/features/formModel/fieldTypes/DateTimeFieldType.js.map +1 -0
  176. package/features/formModel/fieldTypes/FileFieldType.d.ts +18 -0
  177. package/features/formModel/fieldTypes/FileFieldType.js +20 -0
  178. package/features/formModel/fieldTypes/FileFieldType.js.map +1 -0
  179. package/features/formModel/fieldTypes/FileUrlFieldType.d.ts +18 -0
  180. package/features/formModel/fieldTypes/FileUrlFieldType.js +20 -0
  181. package/features/formModel/fieldTypes/FileUrlFieldType.js.map +1 -0
  182. package/features/formModel/fieldTypes/NumberFieldType.d.ts +19 -0
  183. package/features/formModel/fieldTypes/NumberFieldType.js +27 -0
  184. package/features/formModel/fieldTypes/NumberFieldType.js.map +1 -0
  185. package/features/formModel/fieldTypes/ObjectFieldType.d.ts +34 -0
  186. package/features/formModel/fieldTypes/ObjectFieldType.js +109 -0
  187. package/features/formModel/fieldTypes/ObjectFieldType.js.map +1 -0
  188. package/features/formModel/fieldTypes/TextFieldType.d.ts +18 -0
  189. package/features/formModel/fieldTypes/TextFieldType.js +20 -0
  190. package/features/formModel/fieldTypes/TextFieldType.js.map +1 -0
  191. package/features/formModel/fieldTypes/index.d.ts +7 -0
  192. package/features/formModel/fieldTypes/index.js +9 -0
  193. package/features/formModel/fieldTypes/index.js.map +1 -0
  194. package/features/formModel/index.d.ts +13 -4
  195. package/features/formModel/index.js +21 -2
  196. package/features/formModel/index.js.map +1 -1
  197. package/features/formModel/renderers.d.ts +15 -1
  198. package/features/formModel/renderers.js +15 -1
  199. package/features/formModel/renderers.js.map +1 -1
  200. package/features/tools/LexicalContext/LexicalContext.d.ts +14 -0
  201. package/features/tools/LexicalContext/LexicalContext.js +22 -0
  202. package/features/tools/LexicalContext/LexicalContext.js.map +1 -0
  203. package/features/tools/LexicalContext/abstractions.d.ts +11 -0
  204. package/features/tools/LexicalContext/abstractions.js +4 -0
  205. package/features/tools/LexicalContext/abstractions.js.map +1 -0
  206. package/features/tools/LexicalContext/index.d.ts +2 -0
  207. package/features/tools/LexicalContext/index.js +3 -0
  208. package/features/tools/LexicalContext/index.js.map +1 -0
  209. package/features/tools/feature.js +2 -0
  210. package/features/tools/feature.js.map +1 -1
  211. package/features/tools/index.d.ts +1 -0
  212. package/features/tools/index.js +1 -0
  213. package/features/tools/index.js.map +1 -1
  214. package/index.d.ts +8 -1
  215. package/index.js +7 -0
  216. package/index.js.map +1 -1
  217. package/package.json +30 -24
  218. package/presentation/installation/components/SystemInstaller/steps/AdminUserStep/createPasswordValidator.js +1 -1
  219. package/presentation/installation/components/SystemInstaller/steps/AdminUserStep/createPasswordValidator.js.map +1 -1
  220. package/presentation/lexicalContext/useLexicalContext.d.ts +3 -0
  221. package/presentation/lexicalContext/useLexicalContext.js +14 -0
  222. package/presentation/lexicalContext/useLexicalContext.js.map +1 -0
  223. package/presentation/textToLexicalTool/TextToLexicalTool.d.ts +3 -0
  224. package/presentation/textToLexicalTool/TextToLexicalTool.js +6 -2
  225. package/presentation/textToLexicalTool/TextToLexicalTool.js.map +1 -1
  226. package/presentation/textToLexicalTool/textToLexicalState.d.ts +2 -1
  227. package/presentation/textToLexicalTool/textToLexicalState.js +15 -3
  228. package/presentation/textToLexicalTool/textToLexicalState.js.map +1 -1
  229. package/routes.d.ts +1 -0
  230. package/routes.js +4 -0
  231. package/routes.js.map +1 -1
  232. package/base/Base/FieldRenderers/ObjectRenderer/ObjectListFlatRenderer.d.ts +0 -21
  233. package/base/Base/FieldRenderers/ObjectRenderer/ObjectListFlatRenderer.js +0 -28
  234. package/base/Base/FieldRenderers/ObjectRenderer/ObjectListFlatRenderer.js.map +0 -1
@@ -1,35 +1,42 @@
1
1
  import { makeAutoObservable, runInAction, toJS } from "mobx";
2
2
  import { Field } from "./Field.js";
3
- function createChildFields(childBuilders, form) {
3
+ import { createTemplateBuilder } from "./fieldTypes/ObjectFieldType.js";
4
+ /** Reserved key used as the template discriminator in templated object data. */
5
+ export const TEMPLATE_DISCRIMINATOR = "_templateId";
6
+ function createChildFields(childBuilders, form, parentPath) {
4
7
  const children = new Map();
5
8
  for (const [name, builder] of Object.entries(childBuilders)) {
6
9
  const config = builder.build(name);
7
- const field = createFieldFromConfig(config, form);
10
+ const field = createFieldFromConfig(config, form, parentPath);
8
11
  children.set(name, field);
9
12
  }
10
13
  return children;
11
14
  }
12
- function createFieldFromConfig(config, form) {
15
+ function createFieldFromConfig(config, form, parentPath) {
13
16
  if (config.childBuilders) {
14
17
  const objField = new ObjectField(config);
15
18
  if (form) {
16
- objField.setForm(form);
19
+ objField.setForm(form, parentPath);
17
20
  }
18
21
  return objField;
19
22
  }
20
23
  const field = new Field(config);
21
24
  if (form) {
22
- field.setForm(form);
25
+ field.setForm(form, parentPath);
23
26
  }
24
27
  return field;
25
28
  }
26
- function hydrateChildren(children, data) {
29
+ function hydrateChildren(children, data, options) {
27
30
  if (!data) {
28
31
  return;
29
32
  }
30
33
  for (const [name, field] of children) {
31
34
  if (name in data) {
32
- field.setValueSilent(data[name]);
35
+ if (options?.clone && field.config.cloneValue) {
36
+ field.setValueSilent(field.config.cloneValue(data[name]));
37
+ } else {
38
+ field.setValueSilent(data[name]);
39
+ }
33
40
  }
34
41
  }
35
42
  }
@@ -44,10 +51,10 @@ function getChildrenData(children) {
44
51
  }
45
52
  return data;
46
53
  }
47
- async function validateChildren(children) {
54
+ async function validateChildren(children, options) {
48
55
  let allValid = true;
49
56
  for (const [, field] of children) {
50
- const valid = await field.validate();
57
+ const valid = await field.validate(options);
51
58
  if (!valid) {
52
59
  allValid = false;
53
60
  }
@@ -66,18 +73,48 @@ let itemKeyCounter = 0;
66
73
  export class ObjectField {
67
74
  _form = null;
68
75
  _items = [];
76
+ _templates = [];
77
+ _activeTemplateId = null;
78
+ /** Inner layout for non-templated objects (single layout, applied per item too). */
79
+ _ownLayout = null;
80
+ /** Per-template inner layouts for templated objects. */
81
+ _templateLayouts = {};
82
+ _validating = false;
69
83
  constructor(config) {
70
84
  this.config = config;
71
85
  this._base = new Field({
72
86
  ...config,
73
87
  type: "object",
74
- renderer: config.renderer ?? "object"
88
+ renderer: config.renderer ?? (config.isList ? "objectAccordionMultiple" : "objectAccordionSingle")
75
89
  });
76
- this._children = createChildFields(config.childBuilders, null);
90
+ this._templates = config.templates ?? [];
91
+ this._isTemplated = this._templates.length > 0;
92
+ if (this._isTemplated) {
93
+ // Templated mode: children populated per-item (list) or when a template is picked (single).
94
+ this._children = new Map();
95
+ } else {
96
+ this._children = createChildFields(config.childBuilders, null);
97
+ }
77
98
  makeAutoObservable(this, {
78
99
  config: false
79
100
  });
80
101
  }
102
+ _findTemplate(id) {
103
+ return this._templates.find(t => t.id === id);
104
+ }
105
+ _rebuildChildrenForTemplate(templateId) {
106
+ const template = this._findTemplate(templateId);
107
+ if (!template) {
108
+ throw new Error(`Template "${templateId}" not found on field "${this.config.name}". ` + `Available: ${this._templates.map(t => t.id).join(", ") || "(none)"}.`);
109
+ }
110
+ const children = createChildFields(template.childBuilders, this._form, this.qualifiedName);
111
+ this._children = children;
112
+ this._activeTemplateId = templateId;
113
+ const inner = this._innerLayoutFor(templateId);
114
+ if (inner) {
115
+ this._applyNestedObjectLayouts(inner, children);
116
+ }
117
+ }
81
118
 
82
119
  // --- Forwarded from _base ---
83
120
 
@@ -90,21 +127,28 @@ export class ObjectField {
90
127
  get visible() {
91
128
  return this._base.visible;
92
129
  }
130
+ get disabled() {
131
+ return this._base.disabled;
132
+ }
93
133
  setDisabled(value) {
94
134
  this._base.setDisabled(value);
95
135
  }
96
136
  setVisible(value) {
97
137
  this._base.setVisible(value);
98
138
  }
99
- setForm(form) {
139
+ setAncestorRules(rules) {
140
+ this._base.setAncestorRules(rules);
141
+ }
142
+ setForm(form, parentPath) {
100
143
  this._form = form;
101
- this._base.setForm(form);
144
+ this._base.setForm(form, parentPath);
145
+ const myPath = this._base.qualifiedName;
102
146
  for (const [, field] of this._children) {
103
- field.setForm(form);
147
+ field.setForm(form, myPath);
104
148
  }
105
149
  for (const item of this._items) {
106
150
  for (const [, field] of item.children) {
107
- field.setForm(form);
151
+ field.setForm(form, myPath);
108
152
  }
109
153
  }
110
154
  }
@@ -123,18 +167,196 @@ export class ObjectField {
123
167
  addOnBlur(cb) {
124
168
  this._base.addOnBlur(cb);
125
169
  }
170
+ addRequiredWhen(fn, message) {
171
+ this._base.addRequiredWhen(fn, message);
172
+ }
173
+ setComputed(fn) {
174
+ this._base.setComputed(fn);
175
+ }
176
+ setComputedUntilDirty(fn) {
177
+ this._base.setComputedUntilDirty(fn);
178
+ }
126
179
  blur() {
127
180
  this._base.blur();
128
181
  }
129
182
  remove() {
130
183
  this._base.remove();
131
184
  }
185
+ get qualifiedName() {
186
+ return this._base.qualifiedName;
187
+ }
188
+ focus() {
189
+ this._base.focus();
190
+ }
191
+ requestFocus() {
192
+ this._base.requestFocus();
193
+ }
194
+ clearFocusRequest() {
195
+ this._base.clearFocusRequest();
196
+ }
197
+ getInnerLayout() {
198
+ if (this.isTemplated) {
199
+ return this._activeTemplateId ? this._templateLayouts[this._activeTemplateId] ?? null : null;
200
+ }
201
+ return this._ownLayout;
202
+ }
132
203
 
133
204
  // --- Object-specific ---
134
205
 
135
206
  get isList() {
136
207
  return this.config.isList;
137
208
  }
209
+ get isTemplated() {
210
+ return this._isTemplated;
211
+ }
212
+ get activeTemplateId() {
213
+ return this._activeTemplateId;
214
+ }
215
+ get availableTemplates() {
216
+ const result = [];
217
+ for (const template of this._templates) {
218
+ if (template.visible && this._form) {
219
+ if (!template.visible(this._form)) {
220
+ continue;
221
+ }
222
+ }
223
+ result.push({
224
+ id: template.id,
225
+ label: template.label,
226
+ icon: template.icon
227
+ });
228
+ }
229
+ return result;
230
+ }
231
+ setTemplate(templateId) {
232
+ if (this._activeTemplateId === templateId) {
233
+ return;
234
+ }
235
+ this._rebuildChildrenForTemplate(templateId);
236
+ }
237
+ get templates() {
238
+ return {
239
+ add: (id, configure) => this._addTemplate(id, configure),
240
+ remove: templateId => this._removeTemplate(templateId)
241
+ };
242
+ }
243
+ _addTemplate(id, configure) {
244
+ if (!this.isTemplated) {
245
+ throw new Error(`Object field "${this.config.name}" is not templated; templates.add() requires the field to be defined with .template().`);
246
+ }
247
+ if (id === TEMPLATE_DISCRIMINATOR) {
248
+ throw new Error(`Template id "${TEMPLATE_DISCRIMINATOR}" is reserved. Choose a different id.`);
249
+ }
250
+ if (this._findTemplate(id)) {
251
+ throw new Error(`Duplicate template id "${id}".`);
252
+ }
253
+ const tb = createTemplateBuilder();
254
+ configure(tb);
255
+ this._templates.push(tb._build(id, this._form.registry));
256
+ }
257
+ _removeTemplate(templateId) {
258
+ if (!this.isTemplated) {
259
+ throw new Error(`Object field "${this.config.name}" is not templated; templates.remove() requires the field to be defined with .template().`);
260
+ }
261
+ const index = this._templates.findIndex(t => t.id === templateId);
262
+ if (index === -1) {
263
+ return;
264
+ }
265
+ this._templates.splice(index, 1);
266
+ if (this.config.isList) {
267
+ this._items = this._items.filter(item => item.templateId !== templateId);
268
+ return;
269
+ }
270
+ if (this._activeTemplateId === templateId) {
271
+ this._activeTemplateId = null;
272
+ this._children = new Map();
273
+ }
274
+ }
275
+ setInnerLayout(layout) {
276
+ if (Array.isArray(layout)) {
277
+ if (this.isTemplated) {
278
+ throw new Error(`Object field "${this.config.name}" is templated; layout.object() must pass a per-template map (Record<templateId, LayoutNode[]>), not a single LayoutNode[].`);
279
+ }
280
+ this._ownLayout = layout;
281
+ this._templateLayouts = {};
282
+ // Apply nested object layouts to children that already exist
283
+ // (non-templated single + non-templated list items).
284
+ this._applyNestedObjectLayouts(layout, this._children);
285
+ for (const item of this._items) {
286
+ this._applyNestedObjectLayouts(layout, item.children);
287
+ }
288
+ return;
289
+ }
290
+ if (!this.isTemplated) {
291
+ throw new Error(`Object field "${this.config.name}" is not templated; layout.object() must pass a single LayoutNode[], not a per-template map.`);
292
+ }
293
+ this._templateLayouts = layout;
294
+ this._ownLayout = null;
295
+ // Apply nested object layouts to currently-active templated children
296
+ // (templated single with active template + templated list items).
297
+ if (this._activeTemplateId && layout[this._activeTemplateId]) {
298
+ this._applyNestedObjectLayouts(layout[this._activeTemplateId], this._children);
299
+ }
300
+ for (const item of this._items) {
301
+ if (item.templateId && layout[item.templateId]) {
302
+ this._applyNestedObjectLayouts(layout[item.templateId], item.children);
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Walk an inner layout for nested `object` nodes and forward each one to
309
+ * the matching child field. The recursion bottoms out naturally: each
310
+ * matched child is itself an ObjectField and will run `_applyNestedObjectLayouts`
311
+ * against its own children whenever they materialise.
312
+ */
313
+ _applyNestedObjectLayouts(layout, children) {
314
+ for (const node of layout) {
315
+ if (node.type === "object") {
316
+ const child = children.get(node.fieldName);
317
+ if (child && isObjectField(child)) {
318
+ child.setInnerLayout(node.inner);
319
+ }
320
+ } else if (node.type === "tabs") {
321
+ for (const tab of node.tabs) {
322
+ this._applyNestedObjectLayouts(tab.layout, children);
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Returns the inner layout that should apply to a freshly-built children
330
+ * Map for the given templateId (templated objects/lists) or for the
331
+ * non-templated case. Used to seed nested object layouts on newly-created
332
+ * children before they go live.
333
+ */
334
+ _innerLayoutFor(templateId) {
335
+ if (this.isTemplated) {
336
+ if (templateId === undefined) {
337
+ return undefined;
338
+ }
339
+ return this._templateLayouts[templateId];
340
+ }
341
+ return this._ownLayout ?? undefined;
342
+ }
343
+ _resolveLayoutForChildren(children, templateId) {
344
+ const layoutNodes = this.isTemplated ? templateId !== null ? this._templateLayouts[templateId] : undefined : this._ownLayout ?? undefined;
345
+ if (layoutNodes && this._form?.resolveChildLayout) {
346
+ return this._form.resolveChildLayout(layoutNodes, children);
347
+ }
348
+ // Default: one row per visible child, in insertion order.
349
+ const fallback = [];
350
+ for (const [, field] of children) {
351
+ if (field.visible) {
352
+ fallback.push({
353
+ type: "row",
354
+ fields: [field.vm]
355
+ });
356
+ }
357
+ }
358
+ return fallback;
359
+ }
138
360
  get children() {
139
361
  return this._children;
140
362
  }
@@ -142,19 +364,77 @@ export class ObjectField {
142
364
  return this._items.map(item => ({
143
365
  key: item.key,
144
366
  children: item.children,
145
- getData: () => getChildrenData(item.children)
367
+ templateId: item.templateId,
368
+ getData: () => this._getItemData(item)
146
369
  }));
147
370
  }
371
+ _getItemData(item) {
372
+ const data = getChildrenData(item.children);
373
+ if (item.templateId !== undefined) {
374
+ return {
375
+ [TEMPLATE_DISCRIMINATOR]: item.templateId,
376
+ ...data
377
+ };
378
+ }
379
+ return data;
380
+ }
148
381
  getChild(name) {
149
382
  return this._children.get(name);
150
383
  }
384
+ fields(factory) {
385
+ if (this.isTemplated) {
386
+ throw new Error(`Object field "${this.config.name}" is templated; use templates.add()/remove() to manage children. Each template owns its own fields.`);
387
+ }
388
+ const builders = factory(this._form.registry);
389
+ for (const [name, builder] of Object.entries(builders)) {
390
+ if (builder === undefined) {
391
+ this._children.delete(name);
392
+ this.config.childBuilders[name] = undefined;
393
+ delete this.config.childBuilders[name];
394
+ if (this.config.isList) {
395
+ for (const item of this._items) {
396
+ item.children.delete(name);
397
+ }
398
+ }
399
+ continue;
400
+ }
401
+ this.config.childBuilders[name] = builder;
402
+ const built = builder.build(name);
403
+ const newField = createFieldFromConfig(built, this._form, this.qualifiedName);
404
+ this._children.set(name, newField);
405
+ if (this.config.isList) {
406
+ for (const item of this._items) {
407
+ const itemField = createFieldFromConfig(built, this._form, this.qualifiedName);
408
+ item.children.set(name, itemField);
409
+ }
410
+ }
411
+ }
412
+
413
+ // Re-apply nested object layouts so newly-added children pick up any
414
+ // layout.object() entries already registered on this field.
415
+ if (this._ownLayout) {
416
+ this._applyNestedObjectLayouts(this._ownLayout, this._children);
417
+ for (const item of this._items) {
418
+ this._applyNestedObjectLayouts(this._ownLayout, item.children);
419
+ }
420
+ }
421
+ }
151
422
  getListItemChild(index, name) {
152
423
  const item = this._items[index];
153
424
  return item?.children.get(name);
154
425
  }
155
426
  getData() {
156
427
  if (this.config.isList) {
157
- return this._items.map(item => getChildrenData(item.children));
428
+ return this._items.map(item => this._getItemData(item));
429
+ }
430
+ if (this.isTemplated) {
431
+ if (this._activeTemplateId === null) {
432
+ return null;
433
+ }
434
+ return {
435
+ [TEMPLATE_DISCRIMINATOR]: this._activeTemplateId,
436
+ ...getChildrenData(this._children)
437
+ };
158
438
  }
159
439
  return getChildrenData(this._children);
160
440
  }
@@ -169,15 +449,69 @@ export class ObjectField {
169
449
  this._items = [];
170
450
  if (Array.isArray(value)) {
171
451
  for (const itemData of value) {
172
- this._addItemInternal(itemData);
452
+ if (this.isTemplated) {
453
+ if (!itemData || typeof itemData !== "object") {
454
+ continue;
455
+ }
456
+ const data = itemData;
457
+ const templateId = data[TEMPLATE_DISCRIMINATOR];
458
+ if (typeof templateId !== "string") {
459
+ continue;
460
+ }
461
+ if (!this._findTemplate(templateId)) {
462
+ continue;
463
+ }
464
+ const {
465
+ [TEMPLATE_DISCRIMINATOR]: _discarded,
466
+ ...rest
467
+ } = data;
468
+ this._addItemInternal(rest, templateId);
469
+ } else {
470
+ this._addItemInternal(itemData);
471
+ }
173
472
  }
174
473
  }
175
- } else {
176
- hydrateChildren(this._children, value);
474
+ return;
475
+ }
476
+ if (this.isTemplated) {
477
+ if (value === null || value === undefined) {
478
+ this._activeTemplateId = null;
479
+ this._children = new Map();
480
+ return;
481
+ }
482
+ const data = value;
483
+ const templateId = data[TEMPLATE_DISCRIMINATOR];
484
+ if (typeof templateId !== "string") {
485
+ return;
486
+ }
487
+ if (!this._findTemplate(templateId)) {
488
+ return;
489
+ }
490
+ this._rebuildChildrenForTemplate(templateId);
491
+ const {
492
+ [TEMPLATE_DISCRIMINATOR]: _discarded,
493
+ ...rest
494
+ } = data;
495
+ hydrateChildren(this._children, rest);
496
+ return;
177
497
  }
498
+ hydrateChildren(this._children, value);
178
499
  }
179
- addItem(data) {
180
- this._addItemInternal(data);
500
+ addItem(templateIdOrData, data) {
501
+ if (this.isTemplated) {
502
+ if (typeof templateIdOrData !== "string") {
503
+ throw new Error(`Object field "${this.config.name}": templated list items require a template id. ` + `Call addItem(templateId) with one of: ${this._templates.map(t => t.id).join(", ")}.`);
504
+ }
505
+ if (!this._findTemplate(templateIdOrData)) {
506
+ throw new Error(`Template "${templateIdOrData}" not found on field "${this.config.name}". ` + `Available: ${this._templates.map(t => t.id).join(", ") || "(none)"}.`);
507
+ }
508
+ this._addItemInternal(data, templateIdOrData);
509
+ return;
510
+ }
511
+ if (typeof templateIdOrData === "string") {
512
+ throw new Error(`Object field "${this.config.name}" is not templated; addItem() does not accept a template id.`);
513
+ }
514
+ this._addItemInternal(templateIdOrData);
181
515
  }
182
516
  removeItem(index) {
183
517
  this._items.splice(index, 1);
@@ -189,16 +523,52 @@ export class ObjectField {
189
523
  const [item] = this._items.splice(fromIndex, 1);
190
524
  this._items.splice(toIndex, 0, item);
191
525
  }
192
- _addItemInternal(data) {
193
- const children = createChildFields(this.config.childBuilders, this._form);
526
+ duplicateItem(index) {
527
+ const source = this._items[index];
528
+ if (!source) {
529
+ return;
530
+ }
531
+ const data = getChildrenData(source.children);
532
+ const children = createChildFields(this._templateChildBuilders(source.templateId), this._form, this.qualifiedName);
533
+ hydrateChildren(children, data, {
534
+ clone: true
535
+ });
536
+ const key = `item_${++itemKeyCounter}`;
537
+ this._items.splice(index + 1, 0, {
538
+ key,
539
+ children,
540
+ templateId: source.templateId
541
+ });
542
+ const inner = this._innerLayoutFor(source.templateId);
543
+ if (inner) {
544
+ this._applyNestedObjectLayouts(inner, children);
545
+ }
546
+ }
547
+ _templateChildBuilders(templateId) {
548
+ if (templateId === undefined) {
549
+ return this.config.childBuilders;
550
+ }
551
+ const template = this._findTemplate(templateId);
552
+ if (!template) {
553
+ throw new Error(`Template "${templateId}" not found on field "${this.config.name}".`);
554
+ }
555
+ return template.childBuilders;
556
+ }
557
+ _addItemInternal(data, templateId) {
558
+ const children = createChildFields(this._templateChildBuilders(templateId), this._form, this.qualifiedName);
194
559
  if (data) {
195
560
  hydrateChildren(children, data);
196
561
  }
197
562
  const key = `item_${++itemKeyCounter}`;
198
563
  this._items.push({
199
564
  key,
200
- children
565
+ children,
566
+ templateId
201
567
  });
568
+ const inner = this._innerLayoutFor(templateId);
569
+ if (inner) {
570
+ this._applyNestedObjectLayouts(inner, children);
571
+ }
202
572
  }
203
573
  resetValidation() {
204
574
  this._base.resetValidation();
@@ -229,24 +599,44 @@ export class ObjectField {
229
599
  placeholder: baseVm.placeholder,
230
600
  value: this.getValue(),
231
601
  validation: baseVm.validation,
602
+ validating: this._validating,
232
603
  required: baseVm.required,
604
+ visible: baseVm.visible,
233
605
  disabled: baseVm.disabled,
234
606
  renderer: baseVm.renderer,
235
607
  rendererSettings: baseVm.rendererSettings,
236
608
  onChange: value => this.setValue(value),
237
- onBlur: () => this.blur(),
609
+ onBlur: () => {
610
+ if (this._form?.submitted) {
611
+ void this.validate();
612
+ }
613
+ this.blur();
614
+ },
615
+ focusRequested: baseVm.focusRequested,
616
+ clearFocusRequest: baseVm.clearFocusRequest,
238
617
  isList: this.config.isList,
239
618
  fields: this.config.isList ? [] : Array.from(this._children.values()).filter(f => f.visible).map(f => f.vm),
619
+ layout: this.config.isList ? [] : this._resolveLayoutForChildren(this._children, this._activeTemplateId),
240
620
  items: this.config.isList ? this._items.map((item, index) => ({
241
621
  key: item.key,
242
622
  fields: Array.from(item.children.values()).filter(f => f.visible).map(f => f.vm),
623
+ layout: this._resolveLayoutForChildren(item.children, item.templateId ?? null),
624
+ templateId: item.templateId,
243
625
  remove: () => this.removeItem(index),
244
626
  moveUp: () => this.moveItem(index, index - 1),
245
- moveDown: () => this.moveItem(index, index + 1)
627
+ moveDown: () => this.moveItem(index, index + 1),
628
+ duplicate: () => this.duplicateItem(index)
246
629
  })) : [],
247
- addItem: () => this.addItem(),
630
+ addItem: value => {
631
+ return value !== undefined ? this.addItem(value) : this.addItem();
632
+ },
248
633
  removeItem: index => this.removeItem(index),
249
- moveItem: (from, to) => this.moveItem(from, to)
634
+ moveItem: (from, to) => this.moveItem(from, to),
635
+ duplicateItem: index => this.duplicateItem(index),
636
+ isTemplated: this.isTemplated,
637
+ availableTemplates: this.availableTemplates,
638
+ activeTemplateId: this._activeTemplateId,
639
+ setTemplate: templateId => this.setTemplate(templateId)
250
640
  };
251
641
  }
252
642
  get hasErrors() {
@@ -274,72 +664,99 @@ export class ObjectField {
274
664
  }
275
665
  return false;
276
666
  }
277
- async validate() {
278
- if (this.config.required) {
279
- if (this.config.isList && this._items.length === 0) {
280
- this.setValidation({
281
- isValid: false,
282
- message: this.config.requiredMessage || "This field is required."
283
- });
284
- return false;
285
- }
286
- if (!this.config.isList) {
287
- const data = this.getData();
288
- const hasAnyValue = Object.values(data).some(v => v !== null && v !== undefined && v !== "");
289
- if (!hasAnyValue) {
667
+ async validate(options) {
668
+ if (!this.visible) {
669
+ this.setValidation({
670
+ isValid: null
671
+ });
672
+ return true;
673
+ }
674
+ runInAction(() => {
675
+ this._validating = true;
676
+ });
677
+ try {
678
+ const requiredState = this._base.resolveRequired();
679
+ if (requiredState.required) {
680
+ const message = requiredState.message || "This field is required.";
681
+ if (this.config.isList && this._items.length === 0) {
290
682
  this.setValidation({
291
683
  isValid: false,
292
- message: this.config.requiredMessage || "This field is required."
684
+ message
293
685
  });
294
686
  return false;
295
687
  }
688
+ if (!this.config.isList) {
689
+ if (this.isTemplated) {
690
+ if (this._activeTemplateId === null) {
691
+ this.setValidation({
692
+ isValid: false,
693
+ message
694
+ });
695
+ return false;
696
+ }
697
+ } else {
698
+ const data = this.getData();
699
+ const hasAnyValue = Object.values(data).some(v => v !== null && v !== undefined && v !== "");
700
+ if (!hasAnyValue) {
701
+ this.setValidation({
702
+ isValid: false,
703
+ message
704
+ });
705
+ return false;
706
+ }
707
+ }
708
+ }
296
709
  }
297
- }
298
- if (this.config.isList && this.config.listSchema) {
299
- const listData = this.getData();
300
- const result = await this.config.listSchema.safeParseAsync(listData);
301
- if (!result.success) {
302
- const firstIssue = result.error.issues[0];
303
- runInAction(() => {
304
- this.setValidation({
305
- isValid: false,
306
- message: firstIssue?.message || "Invalid value."
710
+ if (this.config.isList && this.config.listSchema) {
711
+ const listData = this.getData();
712
+ const result = await this.config.listSchema.safeParseAsync(listData);
713
+ if (!result.success) {
714
+ const firstIssue = result.error.issues[0];
715
+ runInAction(() => {
716
+ this.setValidation({
717
+ isValid: false,
718
+ message: firstIssue?.message || "Invalid value."
719
+ });
307
720
  });
308
- });
309
- return false;
721
+ return false;
722
+ }
310
723
  }
311
- }
312
- if (this.config.schema) {
313
- const data = this.getData();
314
- const result = await this.config.schema.safeParseAsync(data);
315
- if (!result.success) {
316
- const firstIssue = result.error.issues[0];
317
- runInAction(() => {
318
- this.setValidation({
319
- isValid: false,
320
- message: firstIssue?.message || "Invalid value."
724
+ if (this.config.schema) {
725
+ const data = this.getData();
726
+ const result = await this.config.schema.safeParseAsync(data);
727
+ if (!result.success) {
728
+ const firstIssue = result.error.issues[0];
729
+ runInAction(() => {
730
+ this.setValidation({
731
+ isValid: false,
732
+ message: firstIssue?.message || "Invalid value."
733
+ });
321
734
  });
322
- });
323
- return false;
735
+ return false;
736
+ }
324
737
  }
325
- }
326
- let allValid = true;
327
- if (this.config.isList) {
328
- for (const item of this._items) {
329
- const valid = await validateChildren(item.children);
330
- if (!valid) {
331
- allValid = false;
738
+ let allValid = true;
739
+ if (this.config.isList) {
740
+ for (const item of this._items) {
741
+ const valid = await validateChildren(item.children, options);
742
+ if (!valid) {
743
+ allValid = false;
744
+ }
332
745
  }
746
+ } else {
747
+ allValid = await validateChildren(this._children, options);
333
748
  }
334
- } else {
335
- allValid = await validateChildren(this._children);
336
- }
337
- runInAction(() => {
338
- this.setValidation({
339
- isValid: allValid
749
+ runInAction(() => {
750
+ this.setValidation({
751
+ isValid: allValid
752
+ });
340
753
  });
341
- });
342
- return allValid;
754
+ return allValid;
755
+ } finally {
756
+ runInAction(() => {
757
+ this._validating = false;
758
+ });
759
+ }
343
760
  }
344
761
  }
345
762