platformcommons-web-lib 1.0.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.
Files changed (208) hide show
  1. package/commons-shared-web-ui-1.0.0.tgz +0 -0
  2. package/documentation/alert.md +123 -0
  3. package/documentation/button-dropdown.md +126 -0
  4. package/documentation/button.md +184 -0
  5. package/documentation/cards-usage-guidelines.md +131 -0
  6. package/documentation/configurable-form.md +605 -0
  7. package/documentation/confirmation-modal.md +250 -0
  8. package/documentation/filter-sidebar.md +178 -0
  9. package/documentation/filter-table-selector.md +228 -0
  10. package/documentation/form-builder.md +597 -0
  11. package/documentation/form-components.md +384 -0
  12. package/documentation/nav.md +427 -0
  13. package/documentation/pagination.md +181 -0
  14. package/documentation/side-nav-documentation.md +169 -0
  15. package/documentation/smart-form.md +2177 -0
  16. package/documentation/smart-table.md +1198 -0
  17. package/documentation/snackbar.md +118 -0
  18. package/documentation/style-externalization.md +88 -0
  19. package/documentation/summary-card.md +279 -0
  20. package/ng-package.json +28 -0
  21. package/package.json +54 -0
  22. package/src/lib/modules/alert/alert.models.ts +6 -0
  23. package/src/lib/modules/alert/alert.module.ts +16 -0
  24. package/src/lib/modules/alert/alert.theme.scss +85 -0
  25. package/src/lib/modules/alert/components/alert/alert.component.html +27 -0
  26. package/src/lib/modules/alert/components/alert/alert.component.scss +92 -0
  27. package/src/lib/modules/alert/components/alert/alert.component.ts +81 -0
  28. package/src/lib/modules/button/button.models.ts +13 -0
  29. package/src/lib/modules/button/button.module.ts +16 -0
  30. package/src/lib/modules/button/button.theme.scss +121 -0
  31. package/src/lib/modules/button/components/button/button.component.html +22 -0
  32. package/src/lib/modules/button/components/button/button.component.scss +88 -0
  33. package/src/lib/modules/button/components/button/button.component.ts +67 -0
  34. package/src/lib/modules/button-dropdown/button-dropdown.models.ts +26 -0
  35. package/src/lib/modules/button-dropdown/button-dropdown.module.ts +22 -0
  36. package/src/lib/modules/button-dropdown/button-dropdown.theme.scss +87 -0
  37. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.html +41 -0
  38. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.scss +135 -0
  39. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.ts +160 -0
  40. package/src/lib/modules/configurable-form/component/configurable-form.component.html +294 -0
  41. package/src/lib/modules/configurable-form/component/configurable-form.component.scss +503 -0
  42. package/src/lib/modules/configurable-form/component/configurable-form.component.ts +628 -0
  43. package/src/lib/modules/configurable-form/configurable-form.examples.ts +154 -0
  44. package/src/lib/modules/configurable-form/configurable-form.model.ts +131 -0
  45. package/src/lib/modules/configurable-form/configurable-form.module.ts +19 -0
  46. package/src/lib/modules/configurable-form/configurable-form.theme.scss +78 -0
  47. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.html +77 -0
  48. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.scss +395 -0
  49. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.ts +266 -0
  50. package/src/lib/modules/confirmation-modal/confirmation-modal.models.ts +71 -0
  51. package/src/lib/modules/confirmation-modal/confirmation-modal.module.ts +20 -0
  52. package/src/lib/modules/confirmation-modal/confirmation-modal.theme.scss +87 -0
  53. package/src/lib/modules/filter/components/filter/filter.component.html +131 -0
  54. package/src/lib/modules/filter/components/filter/filter.component.scss +245 -0
  55. package/src/lib/modules/filter/components/filter/filter.component.ts +216 -0
  56. package/src/lib/modules/filter/filter.models.ts +88 -0
  57. package/src/lib/modules/filter/filter.module.ts +24 -0
  58. package/src/lib/modules/filter/filter.theme.scss +92 -0
  59. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.html +112 -0
  60. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.scss +186 -0
  61. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.ts +163 -0
  62. package/src/lib/modules/filter-sidebar/filter-sidebar.models.ts +95 -0
  63. package/src/lib/modules/filter-sidebar/filter-sidebar.module.ts +24 -0
  64. package/src/lib/modules/filter-sidebar/filter-sidebar.theme.scss +38 -0
  65. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.html +73 -0
  66. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.scss +321 -0
  67. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.ts +361 -0
  68. package/src/lib/modules/filter-table-selector/filter-table-selector.models.ts +91 -0
  69. package/src/lib/modules/filter-table-selector/filter-table-selector.module.ts +22 -0
  70. package/src/lib/modules/filter-table-selector/filter-table-selector.theme.scss +36 -0
  71. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.html +63 -0
  72. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.scss +496 -0
  73. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.ts +445 -0
  74. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.html +75 -0
  75. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.scss +210 -0
  76. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.ts +55 -0
  77. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.html +25 -0
  78. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.scss +82 -0
  79. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.ts +95 -0
  80. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.html +20 -0
  81. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.scss +37 -0
  82. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.ts +94 -0
  83. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.html +46 -0
  84. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.scss +102 -0
  85. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.ts +50 -0
  86. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.html +35 -0
  87. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.scss +67 -0
  88. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.ts +34 -0
  89. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.html +68 -0
  90. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.scss +113 -0
  91. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.ts +74 -0
  92. package/src/lib/modules/form-builder/configs/field-type-schema.map.ts +533 -0
  93. package/src/lib/modules/form-builder/form-builder.module.ts +36 -0
  94. package/src/lib/modules/form-builder/form-builder.theme.scss +212 -0
  95. package/src/lib/modules/form-builder/index.ts +9 -0
  96. package/src/lib/modules/form-builder/models/builder.models.ts +7 -0
  97. package/src/lib/modules/form-builder/models/field-configurator.models.ts +38 -0
  98. package/src/lib/modules/form-builder/models/field-selection.models.ts +51 -0
  99. package/src/lib/modules/form-builder/services/field-configurator.service.ts +258 -0
  100. package/src/lib/modules/form-builder/services/field-selection.service.ts +300 -0
  101. package/src/lib/modules/form-builder/services/form-schema-tree.service.ts +652 -0
  102. package/src/lib/modules/form-builder/tokens/builder.tokens.ts +10 -0
  103. package/src/lib/modules/form-builder/utils/constants.ts +43 -0
  104. package/src/lib/modules/form-components/components/checkbox/_theme.scss +63 -0
  105. package/src/lib/modules/form-components/components/checkbox/checkbox.component.html +29 -0
  106. package/src/lib/modules/form-components/components/checkbox/checkbox.component.scss +111 -0
  107. package/src/lib/modules/form-components/components/checkbox/checkbox.component.ts +207 -0
  108. package/src/lib/modules/form-components/components/checkbox/checkbox.models.ts +35 -0
  109. package/src/lib/modules/form-components/components/datepicker/_theme.scss +82 -0
  110. package/src/lib/modules/form-components/components/datepicker/datepicker.component.html +42 -0
  111. package/src/lib/modules/form-components/components/datepicker/datepicker.component.scss +115 -0
  112. package/src/lib/modules/form-components/components/datepicker/datepicker.component.ts +267 -0
  113. package/src/lib/modules/form-components/components/datepicker/datepicker.models.ts +45 -0
  114. package/src/lib/modules/form-components/components/dropdown/_theme.scss +91 -0
  115. package/src/lib/modules/form-components/components/dropdown/dropdown.component.html +74 -0
  116. package/src/lib/modules/form-components/components/dropdown/dropdown.component.scss +252 -0
  117. package/src/lib/modules/form-components/components/dropdown/dropdown.component.ts +377 -0
  118. package/src/lib/modules/form-components/components/dropdown/dropdown.models.ts +53 -0
  119. package/src/lib/modules/form-components/components/input/_theme.scss +77 -0
  120. package/src/lib/modules/form-components/components/input/input.component.html +51 -0
  121. package/src/lib/modules/form-components/components/input/input.component.scss +128 -0
  122. package/src/lib/modules/form-components/components/input/input.component.ts +250 -0
  123. package/src/lib/modules/form-components/components/input/input.models.ts +55 -0
  124. package/src/lib/modules/form-components/components/radio/_theme.scss +61 -0
  125. package/src/lib/modules/form-components/components/radio/radio.component.html +22 -0
  126. package/src/lib/modules/form-components/components/radio/radio.component.scss +107 -0
  127. package/src/lib/modules/form-components/components/radio/radio.component.ts +181 -0
  128. package/src/lib/modules/form-components/components/radio/radio.models.ts +39 -0
  129. package/src/lib/modules/form-components/components/search/_theme.scss +73 -0
  130. package/src/lib/modules/form-components/components/search/search.component.html +15 -0
  131. package/src/lib/modules/form-components/components/search/search.component.scss +87 -0
  132. package/src/lib/modules/form-components/components/search/search.component.ts +213 -0
  133. package/src/lib/modules/form-components/components/search/search.models.ts +40 -0
  134. package/src/lib/modules/form-components/components/toggle/_theme.scss +45 -0
  135. package/src/lib/modules/form-components/components/toggle/toggle.component.html +15 -0
  136. package/src/lib/modules/form-components/components/toggle/toggle.component.scss +81 -0
  137. package/src/lib/modules/form-components/components/toggle/toggle.component.ts +166 -0
  138. package/src/lib/modules/form-components/components/toggle/toggle.models.ts +27 -0
  139. package/src/lib/modules/form-components/directives/click-outside.directive.ts +22 -0
  140. package/src/lib/modules/form-components/form-components.module.ts +41 -0
  141. package/src/lib/modules/form-components/form-components.theme.scss +25 -0
  142. package/src/lib/modules/material/material.module.ts +94 -0
  143. package/src/lib/modules/nav/components/nav/nav.component.html +34 -0
  144. package/src/lib/modules/nav/components/nav/nav.component.scss +171 -0
  145. package/src/lib/modules/nav/components/nav/nav.component.ts +82 -0
  146. package/src/lib/modules/nav/nav.models.ts +31 -0
  147. package/src/lib/modules/nav/nav.module.ts +17 -0
  148. package/src/lib/modules/nav/nav.theme.scss +86 -0
  149. package/src/lib/modules/pagination/components/pagination/pagination.component.html +52 -0
  150. package/src/lib/modules/pagination/components/pagination/pagination.component.scss +155 -0
  151. package/src/lib/modules/pagination/components/pagination/pagination.component.ts +109 -0
  152. package/src/lib/modules/pagination/pagination.module.ts +17 -0
  153. package/src/lib/modules/pagination/pagination.theme.scss +66 -0
  154. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.html +56 -0
  155. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.scss +342 -0
  156. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.ts +135 -0
  157. package/src/lib/modules/side-nav/side-nav.models.ts +38 -0
  158. package/src/lib/modules/side-nav/side-nav.module.ts +16 -0
  159. package/src/lib/modules/side-nav/side-nav.theme.scss +111 -0
  160. package/src/lib/modules/smart-form/components/form-field/form-field.component.html +1109 -0
  161. package/src/lib/modules/smart-form/components/form-field/form-field.component.scss +1860 -0
  162. package/src/lib/modules/smart-form/components/form-field/form-field.component.ts +2232 -0
  163. package/src/lib/modules/smart-form/components/form-section/form-section.component.html +64 -0
  164. package/src/lib/modules/smart-form/components/form-section/form-section.component.scss +209 -0
  165. package/src/lib/modules/smart-form/components/form-section/form-section.component.ts +119 -0
  166. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.html +253 -0
  167. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.scss +689 -0
  168. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.ts +1087 -0
  169. package/src/lib/modules/smart-form/index.ts +10 -0
  170. package/src/lib/modules/smart-form/models/form-schema.model.ts +700 -0
  171. package/src/lib/modules/smart-form/models/hierarchy-config.model.ts +21 -0
  172. package/src/lib/modules/smart-form/services/expression.service.ts +75 -0
  173. package/src/lib/modules/smart-form/services/smart-form-controller.service.ts +65 -0
  174. package/src/lib/modules/smart-form/smart-form.examples.ts +1324 -0
  175. package/src/lib/modules/smart-form/smart-form.module.ts +36 -0
  176. package/src/lib/modules/smart-form/smart-form.theme.scss +890 -0
  177. package/src/lib/modules/smart-form/utils/translation.utils.ts +82 -0
  178. package/src/lib/modules/smart-form/utils/trusted-url.pipe.ts +25 -0
  179. package/src/lib/modules/smart-form/utils/validation.utils.ts +98 -0
  180. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.html +283 -0
  181. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.scss +685 -0
  182. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.ts +1118 -0
  183. package/src/lib/modules/smart-table/models/table-config.model.ts +202 -0
  184. package/src/lib/modules/smart-table/smart-table.module.ts +30 -0
  185. package/src/lib/modules/smart-table/smart-table.theme.scss +335 -0
  186. package/src/lib/modules/smart-table/utils/safe-html.pipe.ts +22 -0
  187. package/src/lib/modules/smart-table/utils/smart-table.utils.ts +18 -0
  188. package/src/lib/modules/snackbar/components/snackbar.component.html +41 -0
  189. package/src/lib/modules/snackbar/components/snackbar.component.scss +99 -0
  190. package/src/lib/modules/snackbar/components/snackbar.component.ts +18 -0
  191. package/src/lib/modules/snackbar/models/snackbar.models.ts +10 -0
  192. package/src/lib/modules/snackbar/services/snackbar.service.ts +40 -0
  193. package/src/lib/modules/snackbar/snackbar.module.ts +11 -0
  194. package/src/lib/modules/snackbar/snackbar.theme.scss +93 -0
  195. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.html +47 -0
  196. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.scss +199 -0
  197. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.ts +126 -0
  198. package/src/lib/modules/summary-card/summary-card.module.ts +18 -0
  199. package/src/lib/modules/summary-card/summary-card.theme.scss +176 -0
  200. package/src/lib/shared-ui.module.ts +44 -0
  201. package/src/lib/styles/global.scss +152 -0
  202. package/src/lib/styles/utilities.scss +250 -0
  203. package/src/lib/utils/constants.ts +11 -0
  204. package/src/lib/utils/storage.utils.ts +37 -0
  205. package/src/lib/utils/string.utils.ts +23 -0
  206. package/src/lib/utils/translation.utils.ts +87 -0
  207. package/src/public-api.ts +104 -0
  208. package/tsconfig.lib.json +15 -0
@@ -0,0 +1,652 @@
1
+ import { Injectable } from '@angular/core';
2
+ import {
3
+ FormSchema,
4
+ FieldConfig,
5
+ SectionConfig,
6
+ } from '../../smart-form/models/form-schema.model';
7
+ import {
8
+ SelectionGroupNode,
9
+ SelectionSectionNode,
10
+ SelectionFieldNode,
11
+ } from '../models/field-selection.models';
12
+ import {
13
+ ConfiguratorTreeNode,
14
+ } from '../models/field-configurator.models';
15
+
16
+ /**
17
+ * Service to parse a FormSchema into selection/configurator tree structures,
18
+ * and to rebuild a modified FormSchema from those trees.
19
+ */
20
+ @Injectable({ providedIn: 'root' })
21
+ export class FormSchemaTreeService {
22
+
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+ // SELECTION TREE
25
+ // ────────────────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Parse a FormSchema into SelectionGroupNodes for the field-selection UI.
29
+ * Stepper forms produce one group per step; section forms produce one group.
30
+ */
31
+ parseSchemaToSelectionGroups(schema: FormSchema): SelectionGroupNode[] {
32
+ if (schema.formType === 'STEPPER' && schema.stepperConfig?.children?.length) {
33
+ return schema.stepperConfig.children
34
+ .filter(child => child.type === 'GROUP')
35
+ .map(groupField => this._buildGroupNode(groupField));
36
+ }
37
+
38
+ // SECTION form → single virtual group
39
+ if (schema.sectionConfig?.children?.length) {
40
+ return [{
41
+ label: schema.label || 'Form',
42
+ groupFieldConfig: null,
43
+ enabled: true,
44
+ expanded: true, // Collapse by default for better UX
45
+ sections: this._buildSectionsFromChildren(schema.sectionConfig.children),
46
+ }];
47
+ }
48
+
49
+ return [];
50
+ }
51
+
52
+ /**
53
+ * Rebuild a FormSchema from the selection tree, updating visible/disabled/required
54
+ * flags on each FieldConfig. Does NOT mutate the original schema.
55
+ */
56
+ applySelectionToSchema(
57
+ groups: SelectionGroupNode[],
58
+ originalSchema: FormSchema,
59
+ ): FormSchema {
60
+ const schema: FormSchema = JSON.parse(JSON.stringify(originalSchema));
61
+
62
+ if (schema.formType === 'STEPPER' && schema.stepperConfig?.children?.length) {
63
+ const stepChildren = schema.stepperConfig.children.filter(c => c.type === 'GROUP');
64
+ groups.forEach((group, gi) => {
65
+ if (gi < stepChildren.length) {
66
+ const groupField = stepChildren[gi];
67
+ groupField.isEnabled = group.enabled;
68
+ groupField.visible = group.enabled; // Backward compatibility
69
+ groupField.disabled = !group.enabled;
70
+ if (groupField.sectionConfig?.children) {
71
+ this._applySectionsToChildren(group.sections, groupField.sectionConfig.children);
72
+ }
73
+ }
74
+ });
75
+ } else if (schema.sectionConfig?.children?.length && groups.length > 0) {
76
+ this._applySectionsToChildren(groups[0].sections, schema.sectionConfig.children);
77
+ }
78
+
79
+ return schema;
80
+ }
81
+
82
+ // ────────────────────────────────────────────────────────────────────────────
83
+ // CONFIGURATOR TREE
84
+ // ────────────────────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Parse a FormSchema into a flat ConfiguratorTreeNode[] for the left-panel tree.
88
+ */
89
+ parseSchemaToConfiguratorTree(schema: FormSchema): ConfiguratorTreeNode[] {
90
+ if (schema.formType === 'STEPPER' && schema.stepperConfig?.children?.length) {
91
+ return schema.stepperConfig.children
92
+ .filter(c => c.type === 'GROUP')
93
+ .map((groupField, gi) => this._buildConfiguratorGroupNode(groupField, [gi]));
94
+ }
95
+
96
+ if (schema.sectionConfig?.children?.length) {
97
+ return this._buildConfiguratorChildNodes(schema.sectionConfig.children, []);
98
+ }
99
+
100
+ return [];
101
+ }
102
+
103
+ /**
104
+ * Find a FieldConfig inside a FormSchema by its index path.
105
+ */
106
+ findFieldByPath(schema: FormSchema, path: number[]): FieldConfig | null {
107
+ if (path.length === 0) return null;
108
+
109
+ let children: FieldConfig[] | undefined;
110
+ if (schema.formType === 'STEPPER') {
111
+ children = schema.stepperConfig?.children;
112
+ } else {
113
+ children = schema.sectionConfig?.children;
114
+ }
115
+ if (!children) return null;
116
+
117
+ let current: FieldConfig | undefined;
118
+ for (let i = 0; i < path.length; i++) {
119
+ const idx = path[i];
120
+ if (!children || idx >= children.length) return null;
121
+ current = children[idx];
122
+
123
+ if (i < path.length - 1) {
124
+ // Navigate deeper
125
+ if (current.sectionConfig?.children) {
126
+ children = current.sectionConfig.children;
127
+ } else if (current.children) {
128
+ children = current.children;
129
+ } else {
130
+ return null;
131
+ }
132
+ }
133
+ }
134
+
135
+ return current ?? null;
136
+ }
137
+
138
+ /**
139
+ * Apply a partial patch to a FieldConfig located at the given path.
140
+ * Returns a new FormSchema (does not mutate the original).
141
+ */
142
+ /**
143
+ * Known top-level keys of FieldConfig.
144
+ * Used to strip stale group-name keys (e.g. textConstraintsTextConfig) that
145
+ * may have been introduced by previous incorrect patch operations.
146
+ */
147
+ private readonly _VALID_FIELD_KEYS = new Set([
148
+ 'name', 'type', 'subType', 'label', 'placeholder', 'hint',
149
+ 'required', 'disabled', 'readonly', 'isEnabled', 'visible',
150
+ 'colSpan', 'defaultValue', 'prefix', 'suffix', 'className',
151
+ 'payloadPath', 'errorMessage', 'visibilityExpression', 'onValidate',
152
+ 'children', 'sectionConfig', 'rows',
153
+ 'textConfig', 'emailConfig', 'phoneConfig', 'numberConfig',
154
+ 'dateConfig', 'optionConfig', 'attachmentConfig', 'richTextConfig',
155
+ 'ratingConfig', 'locationConfig', 'generatedConfig', 'rangeConfig',
156
+ ]);
157
+
158
+ applyFieldPatchToSchema(
159
+ path: number[],
160
+ patch: Partial<FieldConfig>,
161
+ originalSchema: FormSchema,
162
+ ): FormSchema {
163
+ const schema: FormSchema = JSON.parse(JSON.stringify(originalSchema));
164
+ const field = this.findFieldByPath(schema, path) as Record<string, any> | null;
165
+ if (field) {
166
+ // 1. Strip stale keys that don't belong to FieldConfig (e.g. old group names)
167
+ for (const key of Object.keys(field)) {
168
+ if (!this._VALID_FIELD_KEYS.has(key)) {
169
+ delete field[key];
170
+ }
171
+ }
172
+ // 2. Deep-merge the new patch on top of the cleaned field
173
+ FormSchemaTreeService.deepMerge(field, patch as any);
174
+ // 3. Clean up empty-string / null leaves from nested config objects.
175
+ // e.g. user clears minDate → dateConfig.minDate becomes "" → remove it.
176
+ // If the entire config object ends up empty → remove the key.
177
+ FormSchemaTreeService.cleanEmptyConfigValues(field);
178
+ }
179
+ return schema;
180
+ }
181
+
182
+ /**
183
+ * Remove empty-string and null values from all typed config sub-objects on a field.
184
+ * If all values in a config sub-object are empty, the key is deleted entirely.
185
+ */
186
+ public static cleanEmptyConfigValues(field: Record<string, any>): void {
187
+ const CONFIG_KEYS = [
188
+ 'textConfig', 'emailConfig', 'phoneConfig', 'numberConfig',
189
+ 'dateConfig', 'optionConfig', 'attachmentConfig', 'richTextConfig',
190
+ 'ratingConfig', 'locationConfig', 'generatedConfig', 'rangeConfig',
191
+ ];
192
+ for (const key of CONFIG_KEYS) {
193
+ if (field[key] && typeof field[key] === 'object' && !Array.isArray(field[key])) {
194
+ field[key] = FormSchemaTreeService._removeEmptyLeaves(field[key]);
195
+ if (Object.keys(field[key]).length === 0) {
196
+ delete field[key];
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Recursively strip empty-string, null, and undefined values from a plain object.
204
+ * Nested objects that become empty after stripping are also removed.
205
+ */
206
+ private static _removeEmptyLeaves(obj: Record<string, any>): Record<string, any> {
207
+ const result: Record<string, any> = {};
208
+ for (const [key, val] of Object.entries(obj)) {
209
+ if (val === '' || val === null || val === undefined) continue;
210
+ if (typeof val === 'object' && !Array.isArray(val)) {
211
+ const nested = FormSchemaTreeService._removeEmptyLeaves(val);
212
+ if (Object.keys(nested).length > 0) result[key] = nested;
213
+ } else {
214
+ result[key] = val;
215
+ }
216
+ }
217
+ return result;
218
+ }
219
+
220
+ /**
221
+ * Recursively merge `source` into `target` in-place.
222
+ * Objects are merged key-by-key; primitives/arrays replace the existing value.
223
+ */
224
+ public static deepMerge(target: Record<string, any>, source: Record<string, any>): void {
225
+ for (const key of Object.keys(source)) {
226
+ const srcVal = source[key];
227
+ const tgtVal = target[key];
228
+ if (
229
+ srcVal !== null &&
230
+ typeof srcVal === 'object' &&
231
+ !Array.isArray(srcVal) &&
232
+ tgtVal !== null &&
233
+ typeof tgtVal === 'object' &&
234
+ !Array.isArray(tgtVal)
235
+ ) {
236
+ // Both sides are plain objects — merge recursively
237
+ this.deepMerge(tgtVal, srcVal);
238
+ } else {
239
+ target[key] = srcVal;
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Convert a flat object with dot-notation keys into a nested object.
246
+ * E.g. { 'a.b.c': 1 } -> { a: { b: { c: 1 } } }
247
+ */
248
+ public static inflatePatch(flatPatch: Record<string, any>): Record<string, any> {
249
+ const nested: Record<string, any> = {};
250
+
251
+ for (const [key, value] of Object.entries(flatPatch)) {
252
+ const parts = key.split('.');
253
+ let current = nested;
254
+
255
+ for (let i = 0; i < parts.length - 1; i++) {
256
+ const part = parts[i];
257
+ if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
258
+ current[part] = {};
259
+ }
260
+ current = current[part];
261
+ }
262
+
263
+ current[parts[parts.length - 1]] = value;
264
+ }
265
+
266
+ return nested;
267
+ }
268
+
269
+ /**
270
+ * Convert a nested object into a flat object with dot-notation keys.
271
+ * E.g. { a: { b: { c: 1 } } } -> { 'a.b.c': 1 }
272
+ */
273
+ public static flattenObject(obj: Record<string, any>, prefix = ''): Record<string, any> {
274
+ const flat: Record<string, any> = {};
275
+
276
+ for (const [key, value] of Object.entries(obj)) {
277
+ const newKey = prefix ? `${prefix}.${key}` : key;
278
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
279
+ Object.assign(flat, this.flattenObject(value, newKey));
280
+ } else {
281
+ flat[newKey] = value;
282
+ }
283
+ }
284
+
285
+ return flat;
286
+ }
287
+
288
+ // ────────────────────────────────────────────────────────────────────────────
289
+ // FIELD TYPE MAPPING (FormSchema type → builder field type)
290
+ // ────────────────────────────────────────────────────────────────────────────
291
+
292
+ /**
293
+ * Map a FormSchema FieldConfig.type to the builder fieldType key
294
+ * used in field-type-schema.map.ts for config UI generation.
295
+ */
296
+ mapFieldTypeToBuilderType(type: string, subType?: string): string {
297
+ const typeMap: Record<string, string> = {
298
+ 'TEXT_INPUT': 'shortText',
299
+ 'TEXT': 'shortText',
300
+ 'TEXTAREA': 'longText',
301
+ 'RICH_TEXT': 'richText',
302
+ 'NUMBER_INPUT': 'number',
303
+ 'NUMBER': 'number',
304
+ 'DATE': 'date',
305
+ 'TIME': 'time',
306
+ 'DROPDOWN': 'dropdown',
307
+ 'SELECT': 'dropdown',
308
+ 'MULTI_SELECT': 'multiSelect',
309
+ 'AUTOCOMPLETE': 'autocomplete',
310
+ 'RADIO': 'radio',
311
+ 'CHECKBOX': 'checkbox',
312
+ 'SWITCH': 'toggle',
313
+ 'FILE_UPLOAD': 'file',
314
+ 'RATING': 'rating',
315
+ 'GENERATED': 'generated',
316
+ 'CHIP': 'chip',
317
+ 'LOCATION': 'location',
318
+ 'GROUP': 'group',
319
+ 'SUBFIELDS': 'section',
320
+ };
321
+
322
+ // Refinement by subType
323
+ if (type === 'TEXT_INPUT' && subType) {
324
+ switch (subType) {
325
+ case 'LONG': return 'longText';
326
+ case 'EMAIL': return 'email';
327
+ case 'PHONE': return 'phone';
328
+ case 'PASSWORD': return 'password';
329
+ default: return 'shortText';
330
+ }
331
+ }
332
+
333
+ if (type === 'NUMBER_INPUT') {
334
+ if (subType === 'DECIMAL') return 'decimal';
335
+ return 'number';
336
+ }
337
+
338
+ if (type === 'DATE') {
339
+ if (subType === 'DATETIME') return 'dateTime';
340
+ return 'date';
341
+ }
342
+
343
+ if (type === 'CHECKBOX' && subType === 'LIST') {
344
+ return 'multiCheckbox';
345
+ }
346
+
347
+ return typeMap[type] ?? 'shortText';
348
+ }
349
+
350
+ // ────────────────────────────────────────────────────────────────────────────
351
+ // PRIVATE — Selection tree builders
352
+ // ────────────────────────────────────────────────────────────────────────────
353
+
354
+ private _buildGroupNode(groupField: FieldConfig): SelectionGroupNode {
355
+ const sections: SelectionSectionNode[] = [];
356
+
357
+ if (groupField.sectionConfig?.children) {
358
+ const built = this._buildSectionsFromChildren(groupField.sectionConfig.children);
359
+ sections.push(...built);
360
+ }
361
+
362
+ return {
363
+ label: groupField.sectionConfig?.label || groupField.label || groupField.name || 'Group',
364
+ groupFieldConfig: groupField,
365
+ enabled: groupField.isEnabled ?? groupField.visible ?? true,
366
+ expanded: true, // Collapse by default for better UX - user can expand as needed
367
+ sections,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Build sections from a children array. Fields that are GROUP/SUBFIELDS
373
+ * become sections; other leaf fields are collected into a "default" section.
374
+ */
375
+ private _buildSectionsFromChildren(children: FieldConfig[]): SelectionSectionNode[] {
376
+ const sections: SelectionSectionNode[] = [];
377
+ let leafFields: SelectionFieldNode[] = [];
378
+
379
+ for (const child of children) {
380
+ if (child.type === 'GROUP' || (child.type === 'SUBFIELDS') || child.sectionConfig) {
381
+ // Flush any accumulated leaf fields into a default section
382
+ if (leafFields.length > 0) {
383
+ sections.push(this._createDefaultSection(leafFields));
384
+ leafFields = [];
385
+ }
386
+ sections.push(this._buildSectionNode(child));
387
+ } else if (child.type === 'ROW' && child.children?.length) {
388
+ // ROW children are leaf fields
389
+ for (const rowChild of child.children) {
390
+ leafFields.push(this._buildFieldNode(rowChild));
391
+ }
392
+ } else {
393
+ leafFields.push(this._buildFieldNode(child));
394
+ }
395
+ }
396
+
397
+ // Flush remaining leaf fields
398
+ if (leafFields.length > 0) {
399
+ sections.push(this._createDefaultSection(leafFields));
400
+ }
401
+
402
+ return sections;
403
+ }
404
+
405
+ private _buildSectionNode(field: FieldConfig): SelectionSectionNode {
406
+ const subsections: SelectionSectionNode[] = [];
407
+ const fields: SelectionFieldNode[] = [];
408
+
409
+ const children = field.sectionConfig?.children ?? field.children ?? [];
410
+ for (const child of children) {
411
+ if (child.type === 'GROUP' || child.sectionConfig) {
412
+ subsections.push(this._buildSectionNode(child));
413
+ } else if (child.type === 'ROW' && child.children?.length) {
414
+ for (const rowChild of child.children) {
415
+ fields.push(this._buildFieldNode(rowChild));
416
+ }
417
+ } else {
418
+ fields.push(this._buildFieldNode(child));
419
+ }
420
+ }
421
+
422
+ const isVisible = field.isEnabled ?? field.visible ?? true;
423
+ return {
424
+ label: field.sectionConfig?.label || field.label || field.name || 'Section',
425
+ sectionConfig: field.sectionConfig ?? null,
426
+ parentFieldConfig: field,
427
+ enabled: isVisible && field.disabled !== true,
428
+ expanded: false, // Collapse by default for better UX - user can expand as needed
429
+ fields,
430
+ subsections,
431
+ };
432
+ }
433
+
434
+ private _createDefaultSection(fields: SelectionFieldNode[]): SelectionSectionNode {
435
+ return {
436
+ label: 'Fields',
437
+ sectionConfig: null,
438
+ parentFieldConfig: null,
439
+ enabled: true,
440
+ expanded: false, // Collapse by default for better UX - user can expand as needed
441
+ fields,
442
+ subsections: [],
443
+ };
444
+ }
445
+
446
+ private _buildFieldNode(field: FieldConfig): SelectionFieldNode {
447
+ return {
448
+ fieldConfig: field,
449
+ selected: field.isEnabled ?? field.visible ?? true,
450
+ isLocked: field.readonly === true,
451
+ };
452
+ }
453
+
454
+ // ────────────────────────────────────────────────────────────────────────────
455
+ // PRIVATE — Apply selection back to FormSchema children
456
+ // ────────────────────────────────────────────────────────────────────────────
457
+
458
+ private _applySectionsToChildren(
459
+ sections: SelectionSectionNode[],
460
+ children: FieldConfig[],
461
+ ): void {
462
+ let sectionIdx = 0;
463
+ let fieldIdx = 0;
464
+
465
+ for (const child of children) {
466
+ if (child.type === 'GROUP' || child.sectionConfig) {
467
+ if (sectionIdx < sections.length) {
468
+ const section = sections[sectionIdx];
469
+ child.isEnabled = section.enabled;
470
+ child.visible = section.enabled; // Backward compatibility
471
+ child.disabled = !section.enabled;
472
+ const innerChildren = child.sectionConfig?.children ?? child.children ?? [];
473
+ if (innerChildren.length > 0) {
474
+ this._applySectionsToChildren(section.subsections, innerChildren);
475
+ // Also apply field selections
476
+ this._applyFieldsToChildren(section.fields, innerChildren);
477
+ }
478
+ sectionIdx++;
479
+ }
480
+ } else if (child.type === 'ROW' && child.children?.length) {
481
+ for (const rowChild of child.children) {
482
+ const matchedField = this._findSelectionField(sections, rowChild, fieldIdx);
483
+ if (matchedField) {
484
+ rowChild.isEnabled = matchedField.selected;
485
+ rowChild.visible = matchedField.selected; // Backward compatibility
486
+ fieldIdx++;
487
+ }
488
+ }
489
+ } else {
490
+ const matchedField = this._findSelectionField(sections, child, fieldIdx);
491
+ if (matchedField) {
492
+ child.isEnabled = matchedField.selected;
493
+ child.visible = matchedField.selected; // Backward compatibility
494
+ fieldIdx++;
495
+ }
496
+ }
497
+ }
498
+ }
499
+
500
+ private _applyFieldsToChildren(
501
+ selectionFields: SelectionFieldNode[],
502
+ children: FieldConfig[],
503
+ ): void {
504
+ let fi = 0;
505
+ for (const child of children) {
506
+ if (child.type === 'GROUP' || child.sectionConfig) continue;
507
+ if (child.type === 'ROW' && child.children?.length) {
508
+ for (const rowChild of child.children) {
509
+ if (fi < selectionFields.length) {
510
+ rowChild.isEnabled = selectionFields[fi].selected;
511
+ rowChild.visible = selectionFields[fi].selected; // Backward compatibility
512
+ fi++;
513
+ }
514
+ }
515
+ } else {
516
+ if (fi < selectionFields.length) {
517
+ child.isEnabled = selectionFields[fi].selected;
518
+ child.visible = selectionFields[fi].selected; // Backward compatibility
519
+ fi++;
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ private _findSelectionField(
526
+ sections: SelectionSectionNode[],
527
+ _field: FieldConfig,
528
+ _index: number,
529
+ ): SelectionFieldNode | null {
530
+ // Flatten all fields from all sections
531
+ for (const section of sections) {
532
+ for (const f of section.fields) {
533
+ if (f.fieldConfig === _field || f.fieldConfig.name === _field.name) {
534
+ return f;
535
+ }
536
+ }
537
+ }
538
+ return null;
539
+ }
540
+
541
+ // ────────────────────────────────────────────────────────────────────────────
542
+ // PRIVATE — Configurator tree builders
543
+ // ────────────────────────────────────────────────────────────────────────────
544
+
545
+ private _buildConfiguratorGroupNode(
546
+ groupField: FieldConfig,
547
+ basePath: number[],
548
+ ): ConfiguratorTreeNode {
549
+ const children: ConfiguratorTreeNode[] = [];
550
+ const sectionChildren = groupField.sectionConfig?.children ?? [];
551
+
552
+ sectionChildren.forEach((child, ci) => {
553
+ if (child.isEnabled === false || child.visible === false) return;
554
+ const childPath = [...basePath, ci];
555
+ if (child.type === 'GROUP' || child.type === 'SUBFIELDS' || child.subType === 'SECTION' || child.sectionConfig) {
556
+ children.push(this._buildConfiguratorSectionNode(child, childPath));
557
+ } else if (child.type === 'ROW' && child.children?.length) {
558
+ child.children.forEach((rowChild, ri) => {
559
+ if (rowChild.isEnabled !== false && rowChild.visible !== false) {
560
+ children.push(this._buildConfiguratorFieldNode(rowChild, [...childPath, ri]));
561
+ }
562
+ });
563
+ } else {
564
+ children.push(this._buildConfiguratorFieldNode(child, childPath));
565
+ }
566
+ });
567
+
568
+ return {
569
+ id: groupField.name || `group-${basePath.join('-')}`,
570
+ label: groupField.sectionConfig?.label || groupField.label || 'Group',
571
+ type: 'group',
572
+ expanded: false,
573
+ fieldConfig: groupField,
574
+ path: basePath,
575
+ children,
576
+ };
577
+ }
578
+
579
+ private _buildConfiguratorSectionNode(
580
+ field: FieldConfig,
581
+ basePath: number[],
582
+ ): ConfiguratorTreeNode {
583
+ const children: ConfiguratorTreeNode[] = [];
584
+ const sectionChildren = field.sectionConfig?.children ?? field.children ?? [];
585
+
586
+ sectionChildren.forEach((child, ci) => {
587
+ if (child.isEnabled === false || child.visible === false) return;
588
+ const childPath = [...basePath, ci];
589
+ if (child.type === 'GROUP' || child.type === 'SUBFIELDS' || child.subType === 'SECTION' || child.sectionConfig) {
590
+ children.push(this._buildConfiguratorSectionNode(child, childPath));
591
+ } else if (child.type === 'ROW' && child.children?.length) {
592
+ child.children.forEach((rowChild, ri) => {
593
+ if (rowChild.isEnabled !== false && rowChild.visible !== false) {
594
+ children.push(this._buildConfiguratorFieldNode(rowChild, [...childPath, ri]));
595
+ }
596
+ });
597
+ } else {
598
+ children.push(this._buildConfiguratorFieldNode(child, childPath));
599
+ }
600
+ });
601
+
602
+ return {
603
+ id: field.name || `section-${basePath.join('-')}`,
604
+ label: field.sectionConfig?.label || field.label || 'Section',
605
+ type: 'section',
606
+ expanded: false,
607
+ fieldConfig: field,
608
+ path: basePath,
609
+ children,
610
+ };
611
+ }
612
+
613
+ private _buildConfiguratorFieldNode(
614
+ field: FieldConfig,
615
+ path: number[],
616
+ ): ConfiguratorTreeNode {
617
+ return {
618
+ id: field.name || `field-${path.join('-')}`,
619
+ label: field.label || field.name || 'Field',
620
+ type: 'field',
621
+ expanded: false,
622
+ fieldConfig: field,
623
+ path,
624
+ children: [],
625
+ };
626
+ }
627
+
628
+ private _buildConfiguratorChildNodes(
629
+ children: FieldConfig[],
630
+ basePath: number[],
631
+ ): ConfiguratorTreeNode[] {
632
+ const nodes: ConfiguratorTreeNode[] = [];
633
+
634
+ children.forEach((child, ci) => {
635
+ if (child.isEnabled === false || child.visible === false) return;
636
+ const childPath = [...basePath, ci];
637
+ if (child.type === 'GROUP' || child.sectionConfig) {
638
+ nodes.push(this._buildConfiguratorSectionNode(child, childPath));
639
+ } else if (child.type === 'ROW' && child.children?.length) {
640
+ child.children.forEach((rowChild, ri) => {
641
+ if (rowChild.isEnabled !== false && rowChild.visible !== false) {
642
+ nodes.push(this._buildConfiguratorFieldNode(rowChild, [...childPath, ri]));
643
+ }
644
+ });
645
+ } else {
646
+ nodes.push(this._buildConfiguratorFieldNode(child, childPath));
647
+ }
648
+ });
649
+
650
+ return nodes;
651
+ }
652
+ }
@@ -0,0 +1,10 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { FieldTypeSchemaMap } from '../models/builder.models';
3
+
4
+ /**
5
+ * Optional token to override the field-type → config-schema mapping
6
+ * used by the standalone FieldConfiguratorComponent.
7
+ */
8
+ export const BUILDER_FIELD_TYPE_SCHEMA_MAP = new InjectionToken<FieldTypeSchemaMap>(
9
+ 'BUILDER_FIELD_TYPE_SCHEMA_MAP'
10
+ );