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,445 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Input,
6
+ Output,
7
+ EventEmitter,
8
+ OnChanges,
9
+ SimpleChanges,
10
+ } from '@angular/core';
11
+ import { FieldConfig, FormSchema } from '../../../../smart-form/models/form-schema.model';
12
+ import { ConfiguratorFieldInfo } from '../../../models/field-configurator.models';
13
+ import { SWITCHABLE_FIELD_TYPES } from '../../../utils/constants';
14
+ import { SnackbarService } from '../../../../snackbar/services/snackbar.service';
15
+
16
+ @Component({
17
+ selector: 'lib-configurator-config-panel',
18
+ standalone: false,
19
+ templateUrl: './configurator-config-panel.component.html',
20
+ styleUrls: ['./configurator-config-panel.component.scss'],
21
+ changeDetection: ChangeDetectionStrategy.OnPush,
22
+ })
23
+ export class ConfiguratorConfigPanelComponent implements OnChanges {
24
+ constructor(
25
+ private cdr: ChangeDetectorRef,
26
+ private snackbarService: SnackbarService
27
+ ) {}
28
+
29
+ @Input() selectedField: FieldConfig | null = null;
30
+ @Input() selectedFieldInfo: ConfiguratorFieldInfo | null = null;
31
+ @Input() builderFieldType: string | null = null;
32
+ @Input() fieldTypeSchemaMap: Record<string, FormSchema> = {};
33
+
34
+ @Output() configChange = new EventEmitter<Record<string, unknown>>();
35
+ @Output() typeChange = new EventEmitter<string>();
36
+
37
+ /**
38
+ * When false, all `optionConfig.*` fields (apiUrl, dataPath, labelPath, etc.)
39
+ * are hidden from the config panel. Business users don't need to configure
40
+ * option URLs — these are pre-filled by the developer in the master JSON.
41
+ */
42
+ @Input() showOptionConfig: boolean = true;
43
+
44
+ /**
45
+ * Field names that are mandatory and cannot be disabled.
46
+ * When the currently selected field is in this list, IS ENABLED / READ ONLY /
47
+ * DISABLED controls are hidden from the Field State step.
48
+ */
49
+ @Input() lockedFieldNames: string[] = [];
50
+
51
+ /** Exposed to the template for the native type-switcher select */
52
+ readonly switchableFieldTypes = SWITCHABLE_FIELD_TYPES;
53
+
54
+ configFormJson: string = '';
55
+ configInitialValues: Record<string, unknown> = {};
56
+ showConfigForm: boolean = false;
57
+
58
+ /**
59
+ * The type currently being PREVIEWED in the config form.
60
+ * Starts equal to builderFieldType; changes as user picks a new type.
61
+ * Emitted to the parent on Apply.
62
+ */
63
+ currentBuilderType: string | null = null;
64
+
65
+ /** Identity key of the currently rendered field — used to detect real field switches. */
66
+ private _currentFieldKey: string = '';
67
+
68
+ /** True when the current field type is DROPDOWN — type switching is disabled for these fields. */
69
+ get isDropdownType(): boolean {
70
+ const t = this.selectedField?.type?.toUpperCase();
71
+ return t === 'DROPDOWN' || t === 'MULTI_SELECT';
72
+ }
73
+
74
+ ngOnChanges(changes: SimpleChanges): void {
75
+ if (
76
+ (changes['selectedField'] || changes['builderFieldType'] || changes['fieldTypeSchemaMap'])
77
+ && this.selectedField
78
+ && this.builderFieldType
79
+ ) {
80
+ const key = `${this.selectedField.name}__${this.builderFieldType}`;
81
+ if (key !== this._currentFieldKey) {
82
+ this._currentFieldKey = key;
83
+ // Reset the previewed type whenever we switch to a different field
84
+ this.currentBuilderType = this.builderFieldType;
85
+ this._buildConfigFormForType(this.builderFieldType);
86
+ }
87
+ }
88
+ }
89
+
90
+ // ─── Type Switcher ────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Called when the user picks a new type from the native select in the header.
94
+ * Immediately rebuilds the config form to show fields for the new type.
95
+ * The actual field-level type change is only committed when Apply is clicked.
96
+ */
97
+ onTypeSelectChange(event: Event): void {
98
+ const newType = (event.target as HTMLSelectElement).value;
99
+ if (!newType || newType === this.currentBuilderType) return;
100
+ if (!this.fieldTypeSchemaMap[newType]) {
101
+ console.warn(`[ConfigPanel] No schema found for field type: ${newType}`);
102
+ return;
103
+ }
104
+ this.currentBuilderType = newType;
105
+ this._buildConfigFormForType(newType);
106
+ }
107
+
108
+ // ─── Form Actions ─────────────────────────────────────────────────────────
109
+
110
+ onActionClick(event: { id: string; formData: Record<string, unknown> }): void {
111
+ if (event.id === 'apply') {
112
+ const patch = this._buildPatchFromFormData(event.formData);
113
+
114
+ // Emit type change only if the user picked a different type
115
+ if (this.currentBuilderType && this.currentBuilderType !== this.builderFieldType) {
116
+ this.typeChange.emit(this.currentBuilderType);
117
+ }
118
+
119
+ this.configChange.emit(patch);
120
+
121
+ // Show success notification to user
122
+ const fieldLabel = this.selectedField?.label || this.selectedField?.name || 'Field';
123
+ this.snackbarService.success(`${fieldLabel} configuration updated successfully`);
124
+ }
125
+ }
126
+
127
+ // ─── Private: Build the config form JSON ─────────────────────────────────
128
+
129
+ /**
130
+ * Rebuild the SmartForm for the given builder type key.
131
+ * Uses a destroy/recreate cycle (showConfigForm toggle + setTimeout) to
132
+ * guarantee a completely fresh SmartForm instance is mounted.
133
+ */
134
+ private _buildConfigFormForType(builderType: string): void {
135
+ if (!this.fieldTypeSchemaMap || !this.selectedField) return;
136
+
137
+ let schema = this.fieldTypeSchemaMap[builderType];
138
+ if (!schema) {
139
+ this.showConfigForm = false;
140
+ this.configFormJson = '';
141
+ this.cdr.markForCheck();
142
+ return;
143
+ }
144
+
145
+ // When optionConfig should be hidden, filter it out of the schema
146
+ if (!this.showOptionConfig) {
147
+ schema = this._filterSchemaForOptionConfig(schema);
148
+ }
149
+
150
+ // When a mandatory/locked field is selected, hide IS ENABLED / READ ONLY / DISABLED
151
+ const isLockedField = this.selectedField?.name
152
+ ? this.lockedFieldNames.includes(this.selectedField.name)
153
+ : false;
154
+ if (isLockedField) {
155
+ schema = this._filterSchemaForLockedField(schema);
156
+ }
157
+
158
+ // 1. Hide current SmartForm instance (renders false in the current CD cycle
159
+ // since ngOnChanges fires before the view is rendered)
160
+ this.showConfigForm = false;
161
+
162
+ // 2. Prepare new schema + initial values
163
+ this.configFormJson = JSON.stringify(schema);
164
+ this.configInitialValues = this._extractInitialValuesFromField(this.selectedField);
165
+
166
+ // 3. Re-mount on next tick — markForCheck schedules a proper CD pass
167
+ // rather than forcing a synchronous check mid-cycle
168
+ setTimeout(() => {
169
+ this.showConfigForm = true;
170
+ this.cdr.markForCheck();
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Deep-clones the schema and removes any field whose `name` matches developer-only fields.
176
+ * This prevents end users from modifying system-level configurations like:
177
+ * - optionConfig.* (API URLs, data paths, etc.)
178
+ * - payloadPath (payload mapping)
179
+ * - className (CSS styling)
180
+ * - name (system identifier)
181
+ *
182
+ * Also removes entire sections that become empty after filtering.
183
+ */
184
+ private _filterSchemaForOptionConfig(schema: FormSchema): FormSchema {
185
+ // List of developer-only field names that should be hidden from end users
186
+ const DEVELOPER_ONLY_FIELDS = [
187
+ 'payloadPath', // Payload mapping configuration
188
+ 'className', // CSS class for styling
189
+ 'name', // System field identifier
190
+ ];
191
+
192
+ const isDeveloperOnlyField = (f: any): boolean => {
193
+ if (typeof f?.name !== 'string') return false;
194
+
195
+ // Hide optionConfig.* fields (API URL, data path, label path, etc.)
196
+ if (f.name.startsWith('optionConfig.')) return true;
197
+
198
+ // Hide other developer-only fields
199
+ if (DEVELOPER_ONLY_FIELDS.includes(f.name)) return true;
200
+
201
+ return false;
202
+ };
203
+
204
+ const filterChildren = (children: any[]): any[] =>
205
+ children
206
+ .map((child: any) => {
207
+ // ROW — filter its inline children
208
+ if (child.type === 'ROW' && Array.isArray(child.children)) {
209
+ const filtered = child.children.filter((c: any) => !isDeveloperOnlyField(c));
210
+ return filtered.length ? { ...child, children: filtered } : null;
211
+ }
212
+ // GROUP / section wrapper — recurse into sectionConfig.children
213
+ if (child.sectionConfig?.children) {
214
+ const filteredChildren = filterChildren(child.sectionConfig.children);
215
+ // Drop the entire section if it becomes empty
216
+ if (!filteredChildren.length) return null;
217
+ return {
218
+ ...child,
219
+ sectionConfig: { ...child.sectionConfig, children: filteredChildren },
220
+ };
221
+ }
222
+ // Leaf field
223
+ if (isDeveloperOnlyField(child)) return null;
224
+ return child;
225
+ })
226
+ .filter(Boolean);
227
+
228
+ const topChildren = schema.sectionConfig?.children
229
+ ? filterChildren(schema.sectionConfig.children)
230
+ : [];
231
+
232
+ return {
233
+ ...schema,
234
+ sectionConfig: schema.sectionConfig
235
+ ? { ...schema.sectionConfig, children: topChildren }
236
+ : schema.sectionConfig,
237
+ } as FormSchema;
238
+ }
239
+
240
+ /**
241
+ * Remove IS ENABLED, READ ONLY, and DISABLED controls from the config schema
242
+ * when the selected field is a mandatory/locked field.
243
+ */
244
+ private _filterSchemaForLockedField(schema: FormSchema): FormSchema {
245
+ const LOCKED_HIDDEN_FIELDS = new Set(['isEnabled', 'readonly', 'disabled']);
246
+
247
+ const filterChildren = (children: any[]): any[] =>
248
+ children
249
+ .map((child: any) => {
250
+ if (child.type === 'ROW' && Array.isArray(child.children)) {
251
+ const filtered = child.children.filter((c: any) => !LOCKED_HIDDEN_FIELDS.has(c.name));
252
+ return filtered.length ? { ...child, children: filtered } : null;
253
+ }
254
+ if (child.sectionConfig?.children) {
255
+ const filteredChildren = filterChildren(child.sectionConfig.children);
256
+ if (!filteredChildren.length) return null;
257
+ return { ...child, sectionConfig: { ...child.sectionConfig, children: filteredChildren } };
258
+ }
259
+ if (LOCKED_HIDDEN_FIELDS.has(child.name)) return null;
260
+ return child;
261
+ })
262
+ .filter(Boolean);
263
+
264
+ const topChildren = schema.sectionConfig?.children
265
+ ? filterChildren(schema.sectionConfig.children)
266
+ : [];
267
+
268
+ return {
269
+ ...schema,
270
+ sectionConfig: schema.sectionConfig
271
+ ? { ...schema.sectionConfig, children: topChildren }
272
+ : schema.sectionConfig,
273
+ } as FormSchema;
274
+ }
275
+
276
+ // ─── Private: Extract initial values from FieldConfig ─────────────────────
277
+
278
+ private _extractInitialValuesFromField(field: FieldConfig): Record<string, unknown> {
279
+ const v: Record<string, unknown> = {};
280
+
281
+ const set = (key: string, val: unknown) => {
282
+ if (val !== undefined && val !== null) v[key] = val;
283
+ };
284
+
285
+ // ── Common FieldConfig properties ─────────────────────────────────────────
286
+ set('label', field.label);
287
+ set('name', field.name);
288
+ set('colSpan', field.colSpan);
289
+ set('placeholder', field.placeholder);
290
+ set('hint', field.hint);
291
+ set('defaultValue', field.defaultValue);
292
+ set('payloadPath', field.payloadPath);
293
+ set('className', field.className);
294
+ set('prefix', field.prefix);
295
+ set('suffix', field.suffix);
296
+ set('required', field.required);
297
+ set('readonly', field.readonly);
298
+ set('disabled', field.disabled);
299
+ set('isEnabled', field.isEnabled ?? field.visible);
300
+ set('errorMessage', field.errorMessage);
301
+ set('visibilityExpression', field.visibilityExpression);
302
+
303
+ if (field.textConfig) {
304
+ const tc = field.textConfig;
305
+ set('textConfig.length.min', tc.length?.min);
306
+ set('textConfig.length.max', tc.length?.max);
307
+ set('textConfig.pattern', tc.pattern);
308
+ set('textConfig.patternMessage', tc.patternMessage);
309
+ set('textConfig.showCharCount', tc.showCharCount);
310
+ set('textConfig.matchField', tc.matchField);
311
+ }
312
+ if (field.emailConfig) {
313
+ const ec = field.emailConfig;
314
+ set('emailConfig.defaultDomain', ec.defaultDomain);
315
+ set('emailConfig.allowedDomains', ec.allowedDomains?.join(', '));
316
+ set('emailConfig.blockedDomains', ec.blockedDomains?.join(', '));
317
+ }
318
+ if (field.phoneConfig) {
319
+ const pc = field.phoneConfig;
320
+ set('phoneConfig.defaultCountryCode', pc.defaultCountryCode);
321
+ set('phoneConfig.prefixCountryCode', pc.prefixCountryCode);
322
+ set('phoneConfig.supportedCountryCodeLocale', pc.supportedCountryCodeLocale?.join(', '));
323
+ }
324
+ if (field.numberConfig) {
325
+ const nc = field.numberConfig;
326
+ set('numberConfig.min', nc.min);
327
+ set('numberConfig.max', nc.max);
328
+ set('numberConfig.step', nc.step);
329
+ set('numberConfig.precision', nc.precision);
330
+ }
331
+ if (field.dateConfig) {
332
+ const dc = field.dateConfig;
333
+ set('dateConfig.minDate', dc.minDate);
334
+ set('dateConfig.maxDate', dc.maxDate);
335
+ set('dateConfig.allowFuture', dc.allowFuture);
336
+ }
337
+ if (field.optionConfig) {
338
+ const oc = field.optionConfig;
339
+ if (oc.optionList?.length) set('optionConfig.optionList', JSON.stringify(oc.optionList, null, 2));
340
+ set('optionConfig.apiUrl', oc.apiUrl || oc.optionUrl);
341
+ set('optionConfig.dataPath', oc.dataPath);
342
+ set('optionConfig.labelPath', oc.labelPath);
343
+ set('optionConfig.valuePath', oc.valuePath);
344
+ set('optionConfig.sortBy', oc.sortBy);
345
+ set('optionConfig.layout', oc.layout);
346
+ }
347
+ if (field.attachmentConfig) {
348
+ const ac = field.attachmentConfig;
349
+ set('attachmentConfig.multiple', ac.multiple);
350
+ set('attachmentConfig.maxFiles', ac.maxFiles);
351
+ set('attachmentConfig.maxSizeMB', ac.maxSizeMB);
352
+ set('attachmentConfig.accept', ac.accept);
353
+ set('attachmentConfig.acceptLabel', ac.acceptLabel);
354
+ set('attachmentConfig.uploadUrl', ac.uploadUrl);
355
+ set('attachmentConfig.deleteUrl', ac.deleteUrl);
356
+ }
357
+ if (field.richTextConfig) {
358
+ const rc = field.richTextConfig;
359
+ set('richTextConfig.height', rc.height);
360
+ set('richTextConfig.maxLength', rc.maxLength);
361
+ set('richTextConfig.placeholder', rc.placeholder);
362
+ set('richTextConfig.showCharCount', rc.showCharCount);
363
+ }
364
+ if (field.ratingConfig) {
365
+ const rat = field.ratingConfig;
366
+ set('ratingConfig.maxRating', rat.maxRating);
367
+ set('ratingConfig.allowHalf', rat.allowHalf);
368
+ }
369
+ if (field.locationConfig) {
370
+ const lc = field.locationConfig;
371
+ set('locationConfig.defaultTab', lc.defaultTab);
372
+ set('locationConfig.maxLocations', lc.maxLocations);
373
+ set('locationConfig.allowMulti', lc.allowMulti);
374
+ set('locationConfig.showMap', lc.showMap);
375
+ set('locationConfig.googleMapsApiKey', lc.googleMapsApiKey);
376
+ set('locationConfig.venuePlaceholder', lc.venuePlaceholder);
377
+ set('locationConfig.onlinePlaceholder', lc.onlinePlaceholder);
378
+ }
379
+
380
+ return v;
381
+ }
382
+
383
+ // ─── Private: Convert flat dot-notation form data → nested FieldConfig patch
384
+
385
+ private _buildPatchFromFormData(formData: Record<string, unknown>): Record<string, unknown> {
386
+ const patch: Record<string, unknown> = {};
387
+
388
+ const setNested = (obj: any, dotPath: string, val: unknown): void => {
389
+ const parts = dotPath.split('.');
390
+ let cur = obj;
391
+ for (let i = 0; i < parts.length - 1; i++) {
392
+ const part = parts[i];
393
+ if (typeof cur[part] !== 'object' || cur[part] === null) {
394
+ cur[part] = {};
395
+ }
396
+ cur = cur[part];
397
+ }
398
+ cur[parts[parts.length - 1]] = val;
399
+ };
400
+
401
+ const flatData: Record<string, unknown> = {};
402
+ for (const [key, val] of Object.entries(formData)) {
403
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
404
+ for (const [innerKey, innerVal] of Object.entries(val as Record<string, unknown>)) {
405
+ flatData[innerKey] = innerVal;
406
+ }
407
+ } else {
408
+ flatData[key] = val;
409
+ }
410
+ }
411
+
412
+ for (const [key, val] of Object.entries(flatData)) {
413
+ if (val === null || val === undefined) continue;
414
+ if (key.startsWith('row-')) continue;
415
+ setNested(patch, key, val);
416
+ }
417
+
418
+ // Post-process: parse JSON strings for optionConfig.optionList
419
+ if (patch['optionConfig'] && (patch['optionConfig'] as any)['optionList']) {
420
+ try {
421
+ const raw = (patch['optionConfig'] as any)['optionList'];
422
+ if (typeof raw === 'string') {
423
+ (patch['optionConfig'] as any)['optionList'] = JSON.parse(raw);
424
+ }
425
+ } catch {
426
+ // Ignore invalid JSON — leave as-is
427
+ }
428
+ }
429
+
430
+ // Post-process: split comma-separated strings for email/phone arrays
431
+ if (patch['emailConfig']) {
432
+ const ec = patch['emailConfig'] as any;
433
+ if (typeof ec['allowedDomains'] === 'string') ec['allowedDomains'] = ec['allowedDomains'].split(',').map((s: string) => s.trim()).filter(Boolean);
434
+ if (typeof ec['blockedDomains'] === 'string') ec['blockedDomains'] = ec['blockedDomains'].split(',').map((s: string) => s.trim()).filter(Boolean);
435
+ }
436
+ if (patch['phoneConfig']) {
437
+ const pc = patch['phoneConfig'] as any;
438
+ if (typeof pc['supportedCountryCodeLocale'] === 'string') {
439
+ pc['supportedCountryCodeLocale'] = pc['supportedCountryCodeLocale'].split(',').map((s: string) => s.trim()).filter(Boolean);
440
+ }
441
+ }
442
+
443
+ return patch;
444
+ }
445
+ }
@@ -0,0 +1,75 @@
1
+ <div class="fb-cfg-tree">
2
+ <div class="fb-cfg-tree__header">
3
+ <h4 class="fb-cfg-tree__title">Field Tree</h4>
4
+ <p class="fb-cfg-tree__subtitle">Select a field to configure</p>
5
+ </div>
6
+
7
+ <div class="fb-cfg-tree__scrollable">
8
+ @for (node of tree; track trackByNodeId($index, node)) {
9
+ <ng-container *ngTemplateOutlet="treeNodeTpl; context: { $implicit: node, depth: 0 }"></ng-container>
10
+ }
11
+
12
+ @if (tree.length === 0) {
13
+ <div class="fb-cfg-tree__empty">
14
+ No fields to configure.
15
+ </div>
16
+ }
17
+ </div>
18
+ </div>
19
+
20
+ <!-- Recursive tree node template -->
21
+ <ng-template #treeNodeTpl let-node let-depth="depth">
22
+ @if (node.type === 'field') {
23
+ <!-- Field node (clickable leaf) -->
24
+ <div
25
+ class="fb-cfg-tree-item"
26
+ [class.fb-cfg-tree-item--active]="isSelected(node)"
27
+ (click)="onFieldSelect(node)"
28
+ [style.padding-left.px]="16 + depth * 16"
29
+ >
30
+ <span class="fb-cfg-tree-item__drag" title="Drag to reorder">
31
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
32
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
33
+ <circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/>
34
+ <circle cx="9" cy="19" r="1"/><circle cx="15" cy="5" r="1"/>
35
+ <circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/>
36
+ </svg>
37
+ </span>
38
+ <div class="fb-cfg-tree-item__content">
39
+ <span class="fb-cfg-tree-item__label">{{ node.label }}</span>
40
+ @if (node.fieldConfig?.type) {
41
+ <span class="fb-cfg-tree-item__type">{{ node.fieldConfig.type }}</span>
42
+ }
43
+ </div>
44
+ </div>
45
+ } @else {
46
+ <!-- Group or Section node (expandable) -->
47
+ <div
48
+ class="fb-cfg-tree-section"
49
+ [style.padding-left.px]="8 + depth * 12"
50
+ >
51
+ <div class="fb-cfg-tree-section__header" (click)="onNodeClick(node)">
52
+ <div class="fb-cfg-tree-section__title-group">
53
+ <span
54
+ class="fb-cfg-tree-section__chevron"
55
+ [class.fb-cfg-tree-section__chevron--expanded]="node.expanded"
56
+ >
57
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
58
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
59
+ <polyline points="9 18 15 12 9 6"></polyline>
60
+ </svg>
61
+ </span>
62
+ <span class="fb-cfg-tree-section__label">{{ node.label }}</span>
63
+ </div>
64
+ </div>
65
+
66
+ @if (node.expanded && node.children.length > 0) {
67
+ <div class="fb-cfg-tree-section__children">
68
+ @for (child of node.children; track trackByNodeId($index, child)) {
69
+ <ng-container *ngTemplateOutlet="treeNodeTpl; context: { $implicit: child, depth: depth + 1 }"></ng-container>
70
+ }
71
+ </div>
72
+ }
73
+ </div>
74
+ }
75
+ </ng-template>