@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,83 +1,97 @@
1
1
  import { makeAutoObservable, computed, toJS, runInAction, observable } from "mobx";
2
2
  import { Field } from "./Field.js";
3
3
  import { ObjectField, isObjectField } from "./ObjectField.js";
4
- import { createFieldBuilderRegistry } from "./FieldBuilder.js";
5
- const layoutAPI = {
6
- row(...fieldIds) {
7
- return {
8
- type: "row",
9
- fieldIds
10
- };
11
- },
12
- separator() {
13
- return {
14
- type: "separator"
15
- };
16
- },
17
- tabs(config) {
18
- return {
19
- type: "tabs",
20
- id: config.id,
21
- renderer: config.renderer,
22
- tabs: config.tabs
23
- };
24
- },
25
- element(renderer, props) {
26
- return {
27
- type: "element",
28
- renderer,
29
- props
30
- };
31
- }
32
- };
4
+ import { LayoutBuilderFactory } from "./LayoutBuilderFactory.js";
5
+ import { LayoutMutator } from "./LayoutMutator.js";
6
+ import { LayoutResolver } from "./LayoutResolver.js";
7
+ import { FocusManager } from "./FocusManager.js";
33
8
  export class FormModel {
34
9
  _fields = new Map();
10
+ _builders = new Map();
35
11
  _layout = [];
36
12
  _baseline = new Map();
37
13
  _submitted = false;
38
14
  _validateOnChange = false;
39
15
  _isValid = null;
40
- _errors = [];
16
+ _formRuleErrors = [];
41
17
  _activeTabs = observable.map();
42
- constructor(config) {
43
- const registry = createFieldBuilderRegistry();
18
+ _ruleEvaluators = [];
19
+ _warnedRuleTypes = new Set();
20
+ _formRules = [];
21
+ _lastFocusedField = null;
22
+ _layoutMutator = new LayoutMutator();
23
+ _layoutResolver = null;
24
+ _focusManager = null;
25
+ constructor(config, registry) {
26
+ this._registry = registry;
27
+ this._ruleEvaluators = config.ruleEvaluators ?? [];
44
28
  const builders = config.fields(registry);
45
-
46
- // Build fields from builders
47
29
  for (const [name, builder] of Object.entries(builders)) {
30
+ this._builders.set(name, builder);
48
31
  const fieldConfig = builder.build(name);
49
32
  const field = this._createField(fieldConfig);
50
33
  field.setForm(this);
51
34
  this._fields.set(name, field);
52
35
  }
53
-
54
- // Build layout
55
36
  if (config.layout) {
56
- this._layout = config.layout(layoutAPI);
37
+ this._layout = LayoutBuilderFactory.buildNodes(config.layout(LayoutBuilderFactory.create()));
57
38
  this._warnOrphanFields();
58
39
  } else {
59
40
  this._layout = this._generateDefaultLayout();
60
41
  }
61
-
62
- // Validation strategy
42
+ this._registerObjectNodeLayouts(this._layout);
43
+ this._propagateAncestorRules();
63
44
  this._validateOnChange = config.validateOnSubmit === false;
64
-
65
- // Snapshot baseline from defaults
66
45
  this._snapshotBaseline();
67
46
  makeAutoObservable(this, {
68
- vm: computed
47
+ vm: computed,
48
+ _layoutMutator: false,
49
+ _layoutResolver: false,
50
+ _focusManager: false
69
51
  }, {
70
52
  autoBind: true
71
53
  });
54
+ this._layoutResolver = new LayoutResolver(this._fields, this._activeTabs, this.evaluateRules.bind(this));
55
+ this._focusManager = new FocusManager(this._fields);
56
+ }
57
+ evaluateRules(rules) {
58
+ let visible = true;
59
+ let disabled = false;
60
+ if (!rules || rules.length === 0) {
61
+ return {
62
+ visible,
63
+ disabled
64
+ };
65
+ }
66
+ for (const rule of rules) {
67
+ const evaluator = this._ruleEvaluators.find(e => e.canEvaluate(rule));
68
+ if (!evaluator) {
69
+ if (process.env.NODE_ENV === "development" && !this._warnedRuleTypes.has(rule.type)) {
70
+ this._warnedRuleTypes.add(rule.type);
71
+ console.warn(`[FormModel] No evaluator registered for rule type "${rule.type}". Rule is ignored.`);
72
+ }
73
+ continue;
74
+ }
75
+ const matched = evaluator.evaluate(rule, this);
76
+ if (!matched) {
77
+ continue;
78
+ }
79
+ if (rule.action === "hide") {
80
+ visible = false;
81
+ } else if (rule.action === "disable") {
82
+ disabled = true;
83
+ }
84
+ }
85
+ return {
86
+ visible,
87
+ disabled
88
+ };
72
89
  }
73
90
  field(name) {
74
- // Try exact match first (supports dotted field names like "properties.language").
75
91
  const field = this._fields.get(name);
76
92
  if (field) {
77
93
  return field;
78
94
  }
79
-
80
- // Try dot-notation traversal through ObjectField children.
81
95
  const parts = name.split(".");
82
96
  if (parts.length > 1) {
83
97
  let current = this._fields.get(parts[0]);
@@ -94,63 +108,59 @@ export class FormModel {
94
108
  }
95
109
  throw new Error(`Field "${name}" not found.`);
96
110
  }
111
+ focusField(name) {
112
+ if (this._lastFocusedField) {
113
+ this._lastFocusedField.clearFocusRequest();
114
+ this._lastFocusedField = null;
115
+ }
116
+ const activations = this._focusManager.buildFocusPath(name, this._layout);
117
+ let field;
118
+ try {
119
+ field = this.field(name);
120
+ } catch {
121
+ // Field not found — no-op.
122
+ }
123
+ runInAction(() => {
124
+ if (activations) {
125
+ for (const act of activations) {
126
+ this._activeTabs.set(act.tabKey, act.tabId);
127
+ }
128
+ }
129
+ if (field) {
130
+ field.requestFocus();
131
+ this._lastFocusedField = field;
132
+ }
133
+ });
134
+ }
97
135
  fields(factory) {
98
- const registry = createFieldBuilderRegistry();
99
- const builders = factory(registry);
136
+ const builders = factory(this._registry);
100
137
  for (const [name, builder] of Object.entries(builders)) {
101
138
  if (builder === undefined) {
102
- // undefined = remove
139
+ this._builders.delete(name);
103
140
  this.removeField(name);
104
141
  continue;
105
142
  }
143
+ this._builders.set(name, builder);
106
144
  const fieldConfig = builder.build(name);
107
145
  const field = this._createField(fieldConfig);
108
146
  field.setForm(this);
109
-
110
- // Replace or add — same operation on the map
111
147
  this._fields.set(name, field);
112
148
  }
113
-
114
- // Re-snapshot baseline to include new fields
115
149
  this._snapshotBaseline();
150
+ this._propagateAncestorRules();
116
151
  }
117
152
  layout(factoryOrNodeId) {
118
153
  if (typeof factoryOrNodeId === "string") {
119
- return this._accessLayoutNode(factoryOrNodeId);
120
- }
121
- const factory = factoryOrNodeId;
122
- const removals = [];
123
- const modifierLayoutAPI = this._createModifierLayoutAPI(removals);
124
- const entries = factory(modifierLayoutAPI);
125
-
126
- // Process removals first
127
- for (const target of removals) {
128
- this._layout = this._removeFromLayout(this._layout, target);
129
- }
130
-
131
- // Process additions with positional modifiers
132
- for (const entry of entries) {
133
- if (this._isPositionedNode(entry)) {
134
- const {
135
- node,
136
- position
137
- } = entry;
138
- if (position) {
139
- this._layout = this._insertIntoLayout(this._layout, node, position);
140
- } else {
141
- this._layout.push(node);
142
- }
143
- } else {
144
- this._layout.push(entry);
145
- }
154
+ return this._layoutMutator.accessNode(this._layout, factoryOrNodeId);
146
155
  }
156
+ this._layout = this._layoutMutator.applyModifications(this._layout, factoryOrNodeId);
157
+ this._registerObjectNodeLayouts(this._layout);
158
+ this._propagateAncestorRules();
147
159
  }
148
160
  removeField(name) {
149
161
  this._fields.delete(name);
150
162
  this._baseline.delete(name);
151
-
152
- // Remove from layout
153
- this._layout = this._removeFromLayout(this._layout, name);
163
+ this._layout = this._layoutMutator.removeFromLayout(this._layout, name);
154
164
  }
155
165
  getData() {
156
166
  const data = {};
@@ -174,7 +184,7 @@ export class FormModel {
174
184
  this._resetAllValidation();
175
185
  this._submitted = false;
176
186
  this._isValid = null;
177
- this._errors = [];
187
+ this._formRuleErrors = [];
178
188
  }
179
189
  reset() {
180
190
  for (const [name, field] of this._fields) {
@@ -184,7 +194,7 @@ export class FormModel {
184
194
  this._resetAllValidation();
185
195
  this._submitted = false;
186
196
  this._isValid = null;
187
- this._errors = [];
197
+ this._formRuleErrors = [];
188
198
  }
189
199
  get isDirty() {
190
200
  for (const [name, field] of this._fields) {
@@ -199,14 +209,17 @@ export class FormModel {
199
209
  get isValid() {
200
210
  return this._isValid;
201
211
  }
202
- get errors() {
203
- return this._errors;
212
+ get submitted() {
213
+ return this._submitted;
204
214
  }
205
- async validate() {
215
+ get errors() {
216
+ if (!this._submitted) {
217
+ return [];
218
+ }
219
+ const ruleErrorPaths = new Set(this._formRuleErrors.filter(e => e.path).map(e => e.path));
206
220
  const errors = [];
207
221
  for (const [, field] of this._fields) {
208
- const valid = await field.validate();
209
- if (!valid) {
222
+ if (field.vm.validation.isValid === false && !ruleErrorPaths.has(field.name)) {
210
223
  errors.push({
211
224
  path: field.name,
212
225
  label: field.config.label,
@@ -214,9 +227,46 @@ export class FormModel {
214
227
  });
215
228
  }
216
229
  }
217
- const isValid = errors.length === 0;
230
+ return [...errors, ...this._formRuleErrors];
231
+ }
232
+ addRule(rule) {
233
+ this._formRules.push(rule);
234
+ }
235
+ setLayout(factory) {
236
+ this._layout = LayoutBuilderFactory.buildNodes(factory(LayoutBuilderFactory.create()));
237
+ this._warnOrphanFields();
238
+ this._registerObjectNodeLayouts(this._layout);
239
+ this._propagateAncestorRules();
240
+ }
241
+ async validate() {
242
+ let allFieldsValid = true;
243
+ for (const [, field] of this._fields) {
244
+ const valid = await field.validate({
245
+ force: true
246
+ });
247
+ if (!valid) {
248
+ allFieldsValid = false;
249
+ }
250
+ }
251
+ const ruleErrors = [];
252
+ for (const rule of this._formRules) {
253
+ const errors = await this._runFormRule(rule);
254
+ for (const err of errors) {
255
+ ruleErrors.push(err);
256
+ if (err.path) {
257
+ const target = this._tryGetField(err.path);
258
+ if (target) {
259
+ target.setValidation({
260
+ isValid: false,
261
+ message: err.message
262
+ });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ const isValid = allFieldsValid && ruleErrors.length === 0;
218
268
  runInAction(() => {
219
- this._errors = errors;
269
+ this._formRuleErrors = ruleErrors;
220
270
  this._isValid = isValid;
221
271
  this._submitted = true;
222
272
  });
@@ -231,121 +281,97 @@ export class FormModel {
231
281
  }
232
282
  get vm() {
233
283
  return {
234
- layout: this._resolveLayout(),
235
- errors: this._errors,
284
+ layout: this._layoutResolver.resolve(this._layout),
285
+ errors: this.errors,
236
286
  isDirty: this.isDirty,
237
287
  isValid: this._isValid
238
288
  };
239
289
  }
290
+ get registry() {
291
+ return this._registry;
292
+ }
293
+ resolveChildLayout(layout, children) {
294
+ return this._layoutResolver.resolveChildLayout(layout, children);
295
+ }
240
296
  getField(name) {
241
297
  return this._fields.get(name);
242
298
  }
243
299
  getFields() {
244
300
  return this._fields;
245
301
  }
246
- _resolveLayout() {
247
- return this._layout.map(node => this._resolveLayoutNode(node)).filter(Boolean);
248
- }
249
- _resolveLayoutNode(node) {
250
- switch (node.type) {
251
- case "row":
252
- return this._resolveRowNode(node);
253
- case "separator":
254
- return this._resolveSeparatorNode();
255
- case "tabs":
256
- return this._resolveTabsNode(node);
257
- case "element":
258
- return this._resolveElementNode(node);
259
- default:
260
- return null;
261
- }
262
- }
263
- _resolveRowNode(node) {
264
- const fields = node.fieldIds.map(id => this._fields.get(id)).filter(f => f !== undefined && f.visible).map(f => f.vm);
265
- if (fields.length === 0) {
266
- return null;
267
- }
268
- return {
269
- type: "row",
270
- fields
271
- };
272
- }
273
- _resolveSeparatorNode() {
274
- return {
275
- type: "separator"
276
- };
302
+ getFieldBuilders(predicate) {
303
+ const pred = predicate ?? (() => true);
304
+ const result = [];
305
+ LayoutBuilderFactory.collectBuilders(this._fields, this._builders, pred, result);
306
+ return result;
277
307
  }
278
- _resolveTabsNode(node) {
279
- if (node.tabs.length === 0) {
280
- return null;
308
+ async _runFormRule(rule) {
309
+ if (typeof rule === "function") {
310
+ const fn = rule;
311
+ const result = await fn(this);
312
+ return Array.isArray(result) ? result : [];
281
313
  }
282
- const tabKey = node.id || this._tabsNodeKey(node);
283
- const tabs = node.tabs.map(tab => ({
284
- id: tab.id,
285
- label: tab.label,
286
- description: tab.description,
287
- icon: tab.icon,
288
- hasErrors: this._tabHasErrors(tab.layout),
289
- layout: tab.layout.map(child => this._resolveLayoutNode(child)).filter(Boolean)
290
- }));
291
- if (tabs.length === 0) {
292
- return null;
314
+ const data = this.getData();
315
+ const result = await rule.safeParseAsync(data);
316
+ if (result.success) {
317
+ return [];
293
318
  }
294
-
295
- // Resolve active tab — fall back to first tab if stored value is invalid
296
- const storedActive = this._activeTabs.get(tabKey);
297
- const validActive = tabs.find(t => t.id === storedActive) ? storedActive : tabs[0].id;
298
- return {
299
- type: "tabs",
300
- id: node.id,
301
- renderer: node.renderer,
302
- tabs,
303
- activeTabId: validActive,
304
- setActiveTab: id => {
305
- runInAction(() => {
306
- this._activeTabs.set(tabKey, id);
307
- });
308
- }
309
- };
310
- }
311
- _resolveElementNode(node) {
312
- return {
313
- type: "element",
314
- renderer: node.renderer,
315
- props: node.props
316
- };
319
+ return result.error.issues.map(issue => {
320
+ const path = issue.path.map(String).join(".");
321
+ const field = path ? this._tryGetField(path) : undefined;
322
+ return {
323
+ path,
324
+ label: field?.config.label,
325
+ message: issue.message || "Invalid value."
326
+ };
327
+ });
317
328
  }
318
- _tabHasErrors(layout) {
319
- const fieldIds = this._collectFieldIdsFromLayout(layout);
320
- for (const id of fieldIds) {
321
- const field = this._fields.get(id);
322
- if (!field) {
323
- continue;
324
- }
325
- if (field.vm.validation.isValid === false) {
326
- return true;
327
- }
328
- if (isObjectField(field) && field.hasErrors) {
329
- return true;
330
- }
329
+ _tryGetField(path) {
330
+ try {
331
+ return this.field(path);
332
+ } catch {
333
+ return undefined;
331
334
  }
332
- return false;
333
335
  }
334
- _collectFieldIdsFromLayout(layout) {
335
- const ids = [];
336
+ _registerObjectNodeLayouts(layout) {
336
337
  for (const node of layout) {
337
- if (node.type === "row") {
338
- ids.push(...node.fieldIds);
338
+ if (node.type === "object") {
339
+ const field = this._fields.get(node.fieldName);
340
+ if (field && isObjectField(field)) {
341
+ field.setInnerLayout(node.inner);
342
+ }
339
343
  } else if (node.type === "tabs") {
340
344
  for (const tab of node.tabs) {
341
- ids.push(...this._collectFieldIdsFromLayout(tab.layout));
345
+ this._registerObjectNodeLayouts(tab.layout);
342
346
  }
343
347
  }
344
348
  }
345
- return ids;
346
349
  }
347
- _tabsNodeKey(node) {
348
- return `__tabs_${node.tabs.map(t => t.id).join("_")}`;
350
+ _propagateAncestorRules() {
351
+ const ancestry = new Map();
352
+ const walk = (nodes, rules) => {
353
+ for (const node of nodes) {
354
+ if (node.type === "row") {
355
+ for (const id of node.fieldIds) {
356
+ const existing = ancestry.get(id) ?? [];
357
+ ancestry.set(id, [...existing, ...rules]);
358
+ }
359
+ } else if (node.type === "tabs") {
360
+ const containerRules = [...rules, ...(node.rules ?? [])];
361
+ for (const tab of node.tabs) {
362
+ const tabRules = [...containerRules, ...(tab.rules ?? [])];
363
+ walk(tab.layout, tabRules);
364
+ }
365
+ } else if (node.type === "object") {
366
+ const existing = ancestry.get(node.fieldName) ?? [];
367
+ ancestry.set(node.fieldName, [...existing, ...rules]);
368
+ }
369
+ }
370
+ };
371
+ walk(this._layout, []);
372
+ for (const [, field] of this._fields) {
373
+ field.setAncestorRules(ancestry.get(field.name) ?? []);
374
+ }
349
375
  }
350
376
  _generateDefaultLayout() {
351
377
  const layout = [];
@@ -360,7 +386,7 @@ export class FormModel {
360
386
  return layout;
361
387
  }
362
388
  _warnOrphanFields() {
363
- const layoutFieldIds = new Set(this._collectFieldIdsFromLayout(this._layout));
389
+ const layoutFieldIds = new Set(LayoutBuilderFactory.collectFieldIds(this._layout));
364
390
  for (const [name, field] of this._fields) {
365
391
  if (field.visible && !layoutFieldIds.has(name)) {
366
392
  console.warn(`[FormModel] Field "${name}" is not in the layout and not marked as .hidden(). ` + `Add it to the layout or mark it as .hidden() to suppress this warning.`);
@@ -378,231 +404,12 @@ export class FormModel {
378
404
  field.resetValidation();
379
405
  }
380
406
  }
381
-
382
- /**
383
- * Find the index of a layout node that matches the given target.
384
- * Matches: row containing fieldId, tabs by id, element by id/renderer.
385
- * Returns -1 if not found.
386
- */
387
- _findLayoutIndex(layout, target) {
388
- return layout.findIndex(node => this._nodeMatchesTarget(node, target));
389
- }
390
- _nodeMatchesTarget(node, target) {
391
- switch (node.type) {
392
- case "row":
393
- return node.fieldIds.includes(target);
394
- case "tabs":
395
- return node.id === target;
396
- case "element":
397
- return node.id === target || node.renderer === target;
398
- default:
399
- return false;
400
- }
401
- }
402
-
403
- /**
404
- * Remove a target from the layout tree. Handles field IDs in rows,
405
- * and node IDs for tabs/elements. Drops rows that become empty.
406
- */
407
- _removeFromLayout(layout, target) {
408
- return layout.map(node => {
409
- if (node.type === "row") {
410
- const filtered = node.fieldIds.filter(id => id !== target);
411
- if (filtered.length === 0) {
412
- return null;
413
- }
414
- return {
415
- ...node,
416
- fieldIds: filtered
417
- };
418
- }
419
- // Remove tabs/elements by their ID
420
- if (node.type === "tabs" && node.id === target || node.type === "element" && (node.id === target || node.renderer === target)) {
421
- return null;
422
- }
423
- return node;
424
- }).filter(Boolean);
425
- }
426
-
427
- /**
428
- * Insert a layout node relative to a target field ID.
429
- */
430
- _insertIntoLayout(layout, node, position) {
431
- const targetIndex = this._findLayoutIndex(layout, position.target);
432
- if (targetIndex === -1) {
433
- // Target not found — append
434
- return [...layout, node];
435
- }
436
- const result = [...layout];
437
- switch (position.type) {
438
- case "before":
439
- result.splice(targetIndex, 0, node);
440
- break;
441
- case "after":
442
- result.splice(targetIndex + 1, 0, node);
443
- break;
444
- case "replace":
445
- result.splice(targetIndex, 1, node);
446
- break;
447
- }
448
- return result;
449
- }
450
- _createModifierLayoutAPI(removals) {
451
- return {
452
- row(...fieldIds) {
453
- const node = {
454
- type: "row",
455
- fieldIds
456
- };
457
- return createLayoutNodeHandle(node);
458
- },
459
- separator() {
460
- const node = {
461
- type: "separator"
462
- };
463
- return createLayoutNodeHandle(node);
464
- },
465
- tabs(config) {
466
- const node = {
467
- type: "tabs",
468
- id: config.id,
469
- renderer: config.renderer,
470
- tabs: config.tabs
471
- };
472
- return createLayoutNodeHandle(node);
473
- },
474
- element(renderer, props) {
475
- const node = {
476
- type: "element",
477
- renderer,
478
- props
479
- };
480
- return createLayoutNodeHandle(node);
481
- },
482
- remove(target) {
483
- removals.push(target);
484
- }
485
- };
486
- }
487
- _accessLayoutNode(nodeId) {
488
- const findTabsNode = layout => {
489
- for (const node of layout) {
490
- if (node.type === "tabs" && node.id === nodeId) {
491
- return node;
492
- }
493
- // Search inside nested tabs
494
- if (node.type === "tabs") {
495
- for (const tab of node.tabs) {
496
- const found = findTabsNode(tab.layout);
497
- if (found) {
498
- return found;
499
- }
500
- }
501
- }
502
- }
503
- return undefined;
504
- };
505
- return {
506
- as: type => {
507
- const tabsNode = findTabsNode(this._layout);
508
- if (!tabsNode) {
509
- throw new Error(`Layout node "${nodeId}" not found.`);
510
- }
511
- if (tabsNode.type !== type) {
512
- throw new Error(`Layout node "${nodeId}" is type "${tabsNode.type}", not "${type}".`);
513
- }
514
- return {
515
- tab: definitionOrId => {
516
- if (typeof definitionOrId === "string") {
517
- // Access existing tab
518
- const tab = tabsNode.tabs.find(t => t.id === definitionOrId);
519
- if (!tab) {
520
- throw new Error(`Tab "${definitionOrId}" not found in tabs node "${nodeId}".`);
521
- }
522
- return {
523
- layout: factory => {
524
- const nodes = factory(layoutAPI);
525
- tab.layout.push(...nodes);
526
- },
527
- before: () => {
528
- /* no-op for existing tabs */
529
- },
530
- after: () => {
531
- /* no-op for existing tabs */
532
- }
533
- };
534
- } else {
535
- // Add new tab
536
- const newTab = {
537
- ...definitionOrId
538
- };
539
- // Resolve layout if it's a factory
540
- if (typeof definitionOrId.layout === "function") {
541
- newTab.layout = definitionOrId.layout(layoutAPI);
542
- }
543
- return {
544
- layout: factory => {
545
- newTab.layout = factory(layoutAPI);
546
- },
547
- before: targetTabId => {
548
- const idx = tabsNode.tabs.findIndex(t => t.id === targetTabId);
549
- if (idx !== -1) {
550
- tabsNode.tabs.splice(idx, 0, newTab);
551
- } else {
552
- tabsNode.tabs.push(newTab);
553
- }
554
- },
555
- after: targetTabId => {
556
- const idx = tabsNode.tabs.findIndex(t => t.id === targetTabId);
557
- if (idx !== -1) {
558
- tabsNode.tabs.splice(idx + 1, 0, newTab);
559
- } else {
560
- tabsNode.tabs.push(newTab);
561
- }
562
- }
563
- };
564
- }
565
- }
566
- };
567
- }
568
- };
569
- }
570
407
  _createField(config) {
571
408
  if (config.childBuilders) {
572
409
  return new ObjectField(config);
573
410
  }
574
411
  return new Field(config);
575
412
  }
576
- _isPositionedNode(entry) {
577
- return "node" in entry;
578
- }
579
- }
580
- function createLayoutNodeHandle(node) {
581
- const handle = {
582
- node,
583
- before(target) {
584
- handle.position = {
585
- type: "before",
586
- target
587
- };
588
- return handle;
589
- },
590
- after(target) {
591
- handle.position = {
592
- type: "after",
593
- target
594
- };
595
- return handle;
596
- },
597
- replace(target) {
598
- handle.position = {
599
- type: "replace",
600
- target
601
- };
602
- return handle;
603
- }
604
- };
605
- return handle;
606
413
  }
607
414
 
608
415
  //# sourceMappingURL=FormModel.js.map