commons-shared-web-ui 0.0.62 → 0.0.63

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 (222) hide show
  1. package/fesm2022/commons-shared-web-ui.mjs +16153 -0
  2. package/fesm2022/commons-shared-web-ui.mjs.map +1 -0
  3. package/index.d.ts +5033 -0
  4. package/package.json +34 -53
  5. package/.claude/settings.local.json +0 -10
  6. package/documentation/alert.md +0 -123
  7. package/documentation/button-dropdown.md +0 -126
  8. package/documentation/button.md +0 -184
  9. package/documentation/cards-usage-guidelines.md +0 -131
  10. package/documentation/configurable-form.md +0 -605
  11. package/documentation/confirmation-modal.md +0 -250
  12. package/documentation/filter-sidebar.md +0 -178
  13. package/documentation/filter-table-selector.md +0 -228
  14. package/documentation/form-builder.md +0 -599
  15. package/documentation/form-components.md +0 -384
  16. package/documentation/form-field-configuration.md +0 -250
  17. package/documentation/nav.md +0 -427
  18. package/documentation/pagination.md +0 -181
  19. package/documentation/side-nav-documentation.md +0 -169
  20. package/documentation/smart-form.md +0 -2819
  21. package/documentation/smart-table.md +0 -1297
  22. package/documentation/snackbar.md +0 -118
  23. package/documentation/style-externalization.md +0 -88
  24. package/documentation/summary-card.md +0 -279
  25. package/documentation/table-column-configuration.md +0 -221
  26. package/documentation/time-picker.md +0 -274
  27. package/ng-package.json +0 -30
  28. package/src/lib/modules/alert/alert.models.ts +0 -6
  29. package/src/lib/modules/alert/alert.module.ts +0 -16
  30. package/src/lib/modules/alert/components/alert/alert.component.html +0 -27
  31. package/src/lib/modules/alert/components/alert/alert.component.scss +0 -92
  32. package/src/lib/modules/alert/components/alert/alert.component.ts +0 -81
  33. package/src/lib/modules/button/button.models.ts +0 -13
  34. package/src/lib/modules/button/button.module.ts +0 -16
  35. package/src/lib/modules/button/components/button/button.component.html +0 -22
  36. package/src/lib/modules/button/components/button/button.component.scss +0 -92
  37. package/src/lib/modules/button/components/button/button.component.ts +0 -67
  38. package/src/lib/modules/button-dropdown/button-dropdown.models.ts +0 -26
  39. package/src/lib/modules/button-dropdown/button-dropdown.module.ts +0 -22
  40. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.html +0 -41
  41. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.scss +0 -135
  42. package/src/lib/modules/button-dropdown/components/button-dropdown/button-dropdown.component.ts +0 -160
  43. package/src/lib/modules/configurable-form/component/configurable-form.component.html +0 -294
  44. package/src/lib/modules/configurable-form/component/configurable-form.component.scss +0 -503
  45. package/src/lib/modules/configurable-form/component/configurable-form.component.ts +0 -628
  46. package/src/lib/modules/configurable-form/configurable-form.examples.ts +0 -154
  47. package/src/lib/modules/configurable-form/configurable-form.model.ts +0 -131
  48. package/src/lib/modules/configurable-form/configurable-form.module.ts +0 -19
  49. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.html +0 -77
  50. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.scss +0 -395
  51. package/src/lib/modules/confirmation-modal/components/confirmation-modal/confirmation-modal.component.ts +0 -266
  52. package/src/lib/modules/confirmation-modal/confirmation-modal.models.ts +0 -71
  53. package/src/lib/modules/confirmation-modal/confirmation-modal.module.ts +0 -20
  54. package/src/lib/modules/filter/components/filter/filter.component.html +0 -131
  55. package/src/lib/modules/filter/components/filter/filter.component.scss +0 -245
  56. package/src/lib/modules/filter/components/filter/filter.component.ts +0 -216
  57. package/src/lib/modules/filter/filter.models.ts +0 -88
  58. package/src/lib/modules/filter/filter.module.ts +0 -24
  59. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.html +0 -112
  60. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.scss +0 -186
  61. package/src/lib/modules/filter-sidebar/components/filter-sidebar/filter-sidebar.component.ts +0 -163
  62. package/src/lib/modules/filter-sidebar/filter-sidebar.models.ts +0 -95
  63. package/src/lib/modules/filter-sidebar/filter-sidebar.module.ts +0 -24
  64. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.html +0 -73
  65. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.scss +0 -321
  66. package/src/lib/modules/filter-table-selector/components/filter-table-selector/filter-table-selector.component.ts +0 -361
  67. package/src/lib/modules/filter-table-selector/filter-table-selector.models.ts +0 -91
  68. package/src/lib/modules/filter-table-selector/filter-table-selector.module.ts +0 -22
  69. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.html +0 -63
  70. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.scss +0 -496
  71. package/src/lib/modules/form-builder/components/field-configurator/configurator-config-panel/configurator-config-panel.component.ts +0 -445
  72. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.html +0 -75
  73. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.scss +0 -210
  74. package/src/lib/modules/form-builder/components/field-configurator/configurator-tree/configurator-tree.component.ts +0 -55
  75. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.html +0 -25
  76. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.scss +0 -82
  77. package/src/lib/modules/form-builder/components/field-configurator/field-configurator.component.ts +0 -95
  78. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.html +0 -20
  79. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.scss +0 -37
  80. package/src/lib/modules/form-builder/components/field-selection/field-selection.component.ts +0 -94
  81. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.html +0 -46
  82. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.scss +0 -102
  83. package/src/lib/modules/form-builder/components/field-selection/group-node/group-node.component.ts +0 -50
  84. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.html +0 -35
  85. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.scss +0 -67
  86. package/src/lib/modules/form-builder/components/field-selection/selection-field-node/selection-field-node.component.ts +0 -34
  87. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.html +0 -68
  88. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.scss +0 -113
  89. package/src/lib/modules/form-builder/components/field-selection/selection-section-node/selection-section-node.component.ts +0 -74
  90. package/src/lib/modules/form-builder/configs/field-type-schema.map.ts +0 -533
  91. package/src/lib/modules/form-builder/form-builder.module.ts +0 -36
  92. package/src/lib/modules/form-builder/index.ts +0 -9
  93. package/src/lib/modules/form-builder/models/builder.models.ts +0 -7
  94. package/src/lib/modules/form-builder/models/field-configurator.models.ts +0 -38
  95. package/src/lib/modules/form-builder/models/field-selection.models.ts +0 -51
  96. package/src/lib/modules/form-builder/services/field-configurator.service.ts +0 -258
  97. package/src/lib/modules/form-builder/services/field-selection.service.ts +0 -299
  98. package/src/lib/modules/form-builder/services/form-schema-tree.service.ts +0 -670
  99. package/src/lib/modules/form-builder/tokens/builder.tokens.ts +0 -10
  100. package/src/lib/modules/form-builder/utils/constants.ts +0 -43
  101. package/src/lib/modules/form-components/components/checkbox/checkbox.component.html +0 -29
  102. package/src/lib/modules/form-components/components/checkbox/checkbox.component.scss +0 -111
  103. package/src/lib/modules/form-components/components/checkbox/checkbox.component.ts +0 -207
  104. package/src/lib/modules/form-components/components/checkbox/checkbox.models.ts +0 -35
  105. package/src/lib/modules/form-components/components/datepicker/datepicker.component.html +0 -42
  106. package/src/lib/modules/form-components/components/datepicker/datepicker.component.scss +0 -115
  107. package/src/lib/modules/form-components/components/datepicker/datepicker.component.ts +0 -267
  108. package/src/lib/modules/form-components/components/datepicker/datepicker.models.ts +0 -45
  109. package/src/lib/modules/form-components/components/dropdown/dropdown.component.html +0 -74
  110. package/src/lib/modules/form-components/components/dropdown/dropdown.component.scss +0 -252
  111. package/src/lib/modules/form-components/components/dropdown/dropdown.component.ts +0 -377
  112. package/src/lib/modules/form-components/components/dropdown/dropdown.models.ts +0 -53
  113. package/src/lib/modules/form-components/components/input/input.component.html +0 -51
  114. package/src/lib/modules/form-components/components/input/input.component.scss +0 -128
  115. package/src/lib/modules/form-components/components/input/input.component.ts +0 -250
  116. package/src/lib/modules/form-components/components/input/input.models.ts +0 -55
  117. package/src/lib/modules/form-components/components/radio/radio.component.html +0 -22
  118. package/src/lib/modules/form-components/components/radio/radio.component.scss +0 -107
  119. package/src/lib/modules/form-components/components/radio/radio.component.ts +0 -181
  120. package/src/lib/modules/form-components/components/radio/radio.models.ts +0 -39
  121. package/src/lib/modules/form-components/components/search/search.component.html +0 -15
  122. package/src/lib/modules/form-components/components/search/search.component.scss +0 -87
  123. package/src/lib/modules/form-components/components/search/search.component.ts +0 -213
  124. package/src/lib/modules/form-components/components/search/search.models.ts +0 -40
  125. package/src/lib/modules/form-components/components/toggle/toggle.component.html +0 -15
  126. package/src/lib/modules/form-components/components/toggle/toggle.component.scss +0 -81
  127. package/src/lib/modules/form-components/components/toggle/toggle.component.ts +0 -166
  128. package/src/lib/modules/form-components/components/toggle/toggle.models.ts +0 -27
  129. package/src/lib/modules/form-components/directives/click-outside.directive.ts +0 -22
  130. package/src/lib/modules/form-components/form-components.module.ts +0 -42
  131. package/src/lib/modules/form-field-configuration/components/_ffc-controls.scss +0 -89
  132. package/src/lib/modules/form-field-configuration/components/config-field-node/config-field-node.component.html +0 -68
  133. package/src/lib/modules/form-field-configuration/components/config-field-node/config-field-node.component.scss +0 -73
  134. package/src/lib/modules/form-field-configuration/components/config-field-node/config-field-node.component.ts +0 -34
  135. package/src/lib/modules/form-field-configuration/components/config-section-node/config-section-node.component.html +0 -78
  136. package/src/lib/modules/form-field-configuration/components/config-section-node/config-section-node.component.scss +0 -83
  137. package/src/lib/modules/form-field-configuration/components/config-section-node/config-section-node.component.ts +0 -94
  138. package/src/lib/modules/form-field-configuration/components/form-field-configuration/form-field-configuration.component.html +0 -38
  139. package/src/lib/modules/form-field-configuration/components/form-field-configuration/form-field-configuration.component.scss +0 -42
  140. package/src/lib/modules/form-field-configuration/components/form-field-configuration/form-field-configuration.component.ts +0 -92
  141. package/src/lib/modules/form-field-configuration/form-field-configuration.module.ts +0 -23
  142. package/src/lib/modules/form-field-configuration/index.ts +0 -7
  143. package/src/lib/modules/form-field-configuration/models/field-configuration.models.ts +0 -62
  144. package/src/lib/modules/form-field-configuration/services/config-schema-tree.service.ts +0 -159
  145. package/src/lib/modules/form-field-configuration/services/field-configuration.service.ts +0 -228
  146. package/src/lib/modules/material/material.module.ts +0 -94
  147. package/src/lib/modules/nav/components/nav/nav.component.html +0 -34
  148. package/src/lib/modules/nav/components/nav/nav.component.scss +0 -171
  149. package/src/lib/modules/nav/components/nav/nav.component.ts +0 -82
  150. package/src/lib/modules/nav/nav.models.ts +0 -31
  151. package/src/lib/modules/nav/nav.module.ts +0 -17
  152. package/src/lib/modules/pagination/components/pagination/pagination.component.html +0 -52
  153. package/src/lib/modules/pagination/components/pagination/pagination.component.scss +0 -155
  154. package/src/lib/modules/pagination/components/pagination/pagination.component.ts +0 -109
  155. package/src/lib/modules/pagination/pagination.module.ts +0 -17
  156. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.html +0 -56
  157. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.scss +0 -342
  158. package/src/lib/modules/side-nav/components/side-nav/side-nav.component.ts +0 -135
  159. package/src/lib/modules/side-nav/side-nav.models.ts +0 -38
  160. package/src/lib/modules/side-nav/side-nav.module.ts +0 -16
  161. package/src/lib/modules/smart-form/components/form-field/form-field.component.html +0 -1379
  162. package/src/lib/modules/smart-form/components/form-field/form-field.component.scss +0 -2255
  163. package/src/lib/modules/smart-form/components/form-field/form-field.component.ts +0 -3174
  164. package/src/lib/modules/smart-form/components/form-section/form-section.component.html +0 -67
  165. package/src/lib/modules/smart-form/components/form-section/form-section.component.scss +0 -209
  166. package/src/lib/modules/smart-form/components/form-section/form-section.component.ts +0 -142
  167. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.html +0 -253
  168. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.scss +0 -689
  169. package/src/lib/modules/smart-form/components/smart-form/smart-form.component.ts +0 -1134
  170. package/src/lib/modules/smart-form/index.ts +0 -10
  171. package/src/lib/modules/smart-form/models/form-schema.model.ts +0 -803
  172. package/src/lib/modules/smart-form/models/hierarchy-config.model.ts +0 -22
  173. package/src/lib/modules/smart-form/services/expression.service.ts +0 -75
  174. package/src/lib/modules/smart-form/services/smart-form-controller.service.ts +0 -67
  175. package/src/lib/modules/smart-form/smart-form.examples.ts +0 -1324
  176. package/src/lib/modules/smart-form/smart-form.module.ts +0 -40
  177. package/src/lib/modules/smart-form/utils/translation.utils.ts +0 -82
  178. package/src/lib/modules/smart-form/utils/trusted-url.pipe.ts +0 -25
  179. package/src/lib/modules/smart-form/utils/validation.utils.ts +0 -98
  180. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.html +0 -283
  181. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.scss +0 -685
  182. package/src/lib/modules/smart-table/components/smart-table/smart-table.component.ts +0 -1219
  183. package/src/lib/modules/smart-table/models/table-config.model.ts +0 -247
  184. package/src/lib/modules/smart-table/smart-table.module.ts +0 -30
  185. package/src/lib/modules/smart-table/utils/safe-html.pipe.ts +0 -28
  186. package/src/lib/modules/smart-table/utils/smart-table.utils.ts +0 -18
  187. package/src/lib/modules/snackbar/components/snackbar.component.html +0 -41
  188. package/src/lib/modules/snackbar/components/snackbar.component.scss +0 -99
  189. package/src/lib/modules/snackbar/components/snackbar.component.ts +0 -18
  190. package/src/lib/modules/snackbar/models/snackbar.models.ts +0 -10
  191. package/src/lib/modules/snackbar/services/snackbar.service.ts +0 -40
  192. package/src/lib/modules/snackbar/snackbar.module.ts +0 -11
  193. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.html +0 -47
  194. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.scss +0 -199
  195. package/src/lib/modules/summary-card/components/summary-card/summary-card.component.ts +0 -126
  196. package/src/lib/modules/summary-card/summary-card.module.ts +0 -18
  197. package/src/lib/modules/table-column-configuration/components/_tcc-controls.scss +0 -53
  198. package/src/lib/modules/table-column-configuration/components/config-column-node/config-column-node.component.html +0 -47
  199. package/src/lib/modules/table-column-configuration/components/config-column-node/config-column-node.component.scss +0 -50
  200. package/src/lib/modules/table-column-configuration/components/config-column-node/config-column-node.component.ts +0 -33
  201. package/src/lib/modules/table-column-configuration/components/table-column-configuration/table-column-configuration.component.html +0 -30
  202. package/src/lib/modules/table-column-configuration/components/table-column-configuration/table-column-configuration.component.scss +0 -36
  203. package/src/lib/modules/table-column-configuration/components/table-column-configuration/table-column-configuration.component.ts +0 -78
  204. package/src/lib/modules/table-column-configuration/index.ts +0 -5
  205. package/src/lib/modules/table-column-configuration/models/table-column-configuration.models.ts +0 -20
  206. package/src/lib/modules/table-column-configuration/services/table-column-configuration.service.ts +0 -77
  207. package/src/lib/modules/table-column-configuration/table-column-configuration.module.ts +0 -21
  208. package/src/lib/modules/time-picker/components/time-picker/time-picker.component.html +0 -37
  209. package/src/lib/modules/time-picker/components/time-picker/time-picker.component.scss +0 -102
  210. package/src/lib/modules/time-picker/components/time-picker/time-picker.component.ts +0 -178
  211. package/src/lib/modules/time-picker/components/time-wheel-panel/time-wheel-panel.component.html +0 -78
  212. package/src/lib/modules/time-picker/components/time-wheel-panel/time-wheel-panel.component.scss +0 -226
  213. package/src/lib/modules/time-picker/components/time-wheel-panel/time-wheel-panel.component.ts +0 -595
  214. package/src/lib/modules/time-picker/models/time-picker.models.ts +0 -49
  215. package/src/lib/modules/time-picker/time-picker.module.ts +0 -23
  216. package/src/lib/shared-ui.module.ts +0 -55
  217. package/src/lib/utils/constants.ts +0 -11
  218. package/src/lib/utils/storage.utils.ts +0 -37
  219. package/src/lib/utils/string.utils.ts +0 -23
  220. package/src/lib/utils/translation.utils.ts +0 -87
  221. package/src/public-api.ts +0 -127
  222. package/tsconfig.lib.json +0 -15
@@ -1,1134 +0,0 @@
1
- import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, OnChanges, SimpleChanges, Output, QueryList, ViewChildren } from '@angular/core';
2
- import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
3
- import { HttpClient, HttpHeaders } from '@angular/common/http';
4
- import { Router } from '@angular/router';
5
- import { ActionBarConfig, ActionButtonConfig, ActionConfig, FieldConfig, FormSchema } from '../../models/form-schema.model';
6
- import { HierarchyCfg } from '../../models/hierarchy-config.model';
7
- import { SmartFormController } from '../../services/smart-form-controller.service';
8
- import { ExpressionService } from '../../services/expression.service';
9
- import { SmartFormTranslationUtils } from '../../utils/translation.utils';
10
- import { SnackbarService } from '../../../snackbar/services/snackbar.service';
11
- import { FormFieldComponent } from '../form-field/form-field.component';
12
- import { Subject, takeUntil, forkJoin, Observable, of } from 'rxjs';
13
- import { catchError, map } from 'rxjs/operators';
14
-
15
- @Component({
16
- selector: 'lib-smart-form',
17
- templateUrl: './smart-form.component.html',
18
- styleUrls: ['./smart-form.component.scss'],
19
- providers: [SmartFormController],
20
- standalone: false
21
- })
22
- export class SmartFormComponent implements OnInit, OnChanges, OnDestroy {
23
- @ViewChildren(FormFieldComponent) formFieldComponents!: QueryList<FormFieldComponent>;
24
-
25
- private destroy$ = new Subject<void>();
26
- @Input() formJson!: string;
27
- @Input() initialValues?: { [key: string]: any };
28
- @Input() enableDraftAutoSave: boolean = false;
29
- /** Flat i18n labels map passed by the consuming app.
30
- * After JSON parse the schema is walked and every string value that
31
- * matches a key in this map is replaced with the translated value.
32
- * Mirrors the pattern used by ConfigurableFormComponent + translateConfig.
33
- */
34
- @Input() labels: any = {};
35
- @Input() mode: 'CREATE' | 'EDIT' = 'CREATE';
36
- /** When true, all form fields are disabled and the action bar is hidden (preview/read-only mode). */
37
- @Input() readOnly: boolean = false;
38
- /** Selected options details passed dynamically from parent component to resolve edit-mode dropdown labels */
39
- @Input() selectedOptionsData?: { [key: string]: any };
40
-
41
- @Output() submit = new EventEmitter<{ [key: string]: any }>();
42
- @Output() draftSave = new EventEmitter<string>();
43
- /**
44
- * Emitted when a button with a custom `type` (not 'cancel', 'draft', or
45
- * 'submit') is clicked. Payload contains the button `id` and the current
46
- * form data snapshot.
47
- */
48
- @Output() actionClick = new EventEmitter<{ id: string; formData: { [key: string]: any } }>();
49
- @Output() valueChange = new EventEmitter<{ [key: string]: any }>();
50
- @Output() fileAdded = new EventEmitter<any>();
51
- @Output() fileUploadFinished = new EventEmitter<any>();
52
- @Output() fileRemoved = new EventEmitter<any>();
53
- /** Emitted when a suffixActionIcon is clicked. Payload: { fieldName, actionId } */
54
- @Output() suffixActionClick = new EventEmitter<{ fieldName: string; actionId: string }>();
55
- /** Emitted whenever the active section step changes. Carries current state so the
56
- * host can show/hide Previous/Next/Submit buttons in its own footer. */
57
- @Output() stepChange = new EventEmitter<{
58
- currentStep: number;
59
- totalSteps: number;
60
- isFirst: boolean;
61
- isLast: boolean;
62
- stepLabel: string;
63
- }>();
64
-
65
- formSchema!: FormSchema;
66
- formGroup!: FormGroup;
67
- fieldList: FieldConfig[] = [];
68
- isStepper: boolean = false;
69
- currentStep: number = 0;
70
- isLoading: boolean = false;
71
- isDraftLoading: boolean = false;
72
-
73
- /** True when sectionStepper mode is active (SECTION form with top-level GROUPs as steps). */
74
- isSectionStepper: boolean = false;
75
- /** Index of the currently visible section step. */
76
- currentSectionStep: number = 0;
77
- /** Flat list of top-level GROUP FieldConfigs that become the stepper steps. */
78
- sectionSteps: FieldConfig[] = [];
79
- /** Validation state per section step — drives badge colour/icon. */
80
- stepValidationStates: ('untouched' | 'valid' | 'warning')[] = [];
81
- /** Flat field-name lists per step used for targeted validation. */
82
- private stepFieldNames: string[][] = [];
83
-
84
- /** Controls skeleton visibility. Stays false until schema is parsed AND
85
- * any EDIT-mode data fetch completes, but always shows for at least
86
- * SKELETON_MIN_MS so the animation is visible even on fast loads. */
87
- isFormReady = false;
88
- private readonly SKELETON_MIN_MS = 350;
89
- private _skeletonStart = 0;
90
-
91
- constructor(
92
- private fb: FormBuilder,
93
- public controller: SmartFormController,
94
- private expressionService: ExpressionService,
95
- private http: HttpClient,
96
- private snackbarService: SnackbarService,
97
- private router: Router,
98
- private cdr: ChangeDetectorRef
99
- ) { }
100
-
101
- ngOnInit(): void {
102
- this._skeletonStart = Date.now();
103
- this._startForm();
104
- this.controller.fileAdded$.pipe(takeUntil(this.destroy$)).subscribe(file => this.fileAdded.emit(file));
105
- this.controller.fileUploadFinished$.pipe(takeUntil(this.destroy$)).subscribe(file => this.fileUploadFinished.emit(file));
106
- this.controller.fileRemoved$.pipe(takeUntil(this.destroy$)).subscribe(file => this.fileRemoved.emit(file));
107
- this.controller.suffixActionClick$.pipe(takeUntil(this.destroy$)).subscribe(data => this.suffixActionClick.emit(data));
108
- }
109
-
110
- loadEditData(): void {
111
- const config = this.formSchema.editConfig!;
112
- this.isLoading = true;
113
- const headers = this.getHeaders();
114
- this.http.get(config.loadApiUrl, { headers }).subscribe({
115
- next: (response: any) => {
116
- this.initialValues = response;
117
- this.controller.initialize(this.initialValues || {});
118
- this.isLoading = false;
119
- this._markReady();
120
- },
121
- error: (err) => {
122
- this.showAlert('error', config.errorMessage || 'Failed to load form data', config.snackbarConfig);
123
- console.error('Load data error:', err);
124
- this.isLoading = false;
125
- this._markReady();
126
- }
127
- });
128
- }
129
-
130
- /** Flips isFormReady=true after the skeleton has been visible for at least SKELETON_MIN_MS. */
131
- private _markReady(): void {
132
- const elapsed = Date.now() - this._skeletonStart;
133
- const remaining = Math.max(0, this.SKELETON_MIN_MS - elapsed);
134
- setTimeout(() => {
135
- this.isFormReady = true;
136
- // markForCheck propagates dirty up through any OnPush ancestor components
137
- // (e.g. configurator-config-panel) so the skeleton-to-form transition
138
- // renders in the same zone tick rather than waiting for a future user event.
139
- this.cdr.markForCheck();
140
- }, remaining);
141
- }
142
-
143
- ngOnChanges(changes: SimpleChanges): void {
144
- if (changes['formJson'] && !changes['formJson'].isFirstChange()) {
145
- this._startForm();
146
- }
147
- if (changes['labels'] && !changes['labels'].isFirstChange() && this.formSchema) {
148
- this._startForm();
149
- }
150
- if (changes['selectedOptionsData'] && this.formSchema) {
151
- this.controller.selectedOptionsData = this.selectedOptionsData || {};
152
- this.formFieldComponents?.forEach(field => field.mergeSelectedOptionsIntoList());
153
- }
154
- }
155
-
156
- ngOnDestroy(): void {
157
- this.controller.destroy();
158
- this.destroy$.next();
159
- this.destroy$.complete();
160
- }
161
-
162
- /** Public backward-compatible entry point — delegates to _startForm. */
163
- parseFormJson(): void {
164
- this._startForm();
165
- }
166
-
167
- private _startForm(): void {
168
- if (!this.formJson) return;
169
- let schema: any;
170
- try {
171
- schema = JSON.parse(this.formJson);
172
- } catch (e) {
173
- console.error('SmartForm: invalid formJson', e);
174
- return;
175
- }
176
- if (this._hasHierarchyDynamic(schema)) {
177
- this._expandHierarchyNodes(schema)
178
- .pipe(takeUntil(this.destroy$))
179
- .subscribe({
180
- next: expanded => this._applySchema(expanded),
181
- error: () => this._applySchema(schema),
182
- });
183
- } else {
184
- this._applySchema(schema);
185
- }
186
- }
187
-
188
- private _applySchema(schema: any): void {
189
- this.formSchema = schema as FormSchema;
190
- this.controller.selectedOptionsData = this.selectedOptionsData || {};
191
- this.controller.token = this.formSchema.token;
192
- this.controller.tokenHeader = this.formSchema.tokenHeader;
193
- if (this.labels && Object.keys(this.labels).length) {
194
- SmartFormTranslationUtils.translateSchema(this.formSchema, this.labels);
195
- }
196
- this.controller.labels = this.labels;
197
- this.controller.actionLabels = this.formSchema.labels;
198
- this.isStepper = this.formSchema.formType === 'STEPPER';
199
- this.fieldList = this.isStepper
200
- ? (this.formSchema.stepperConfig?.children || []).filter(step => step.isEnabled !== false && step.sectionConfig?.isEnabled !== false)
201
- : (this.formSchema.sectionConfig?.children || []).filter(field => field.isEnabled !== false);
202
- const topLevelGroups = this.fieldList.filter(f => f.type === 'GROUP' && f.sectionConfig);
203
- // >= 1, not > 1: when every top-level field is a GROUP, that's the section-stepper
204
- // layout regardless of how many groups survive the isEnabled filter above. The
205
- // non-stepper "Regular SECTION form" fallback (see the template) hands the WHOLE
206
- // sectionConfig — including these GROUP children — to a single lib-form-section,
207
- // which isn't built to expand GROUP-type children; requiring >1 group meant a schema
208
- // reduced to exactly one enabled section (e.g. everything but "Identity &
209
- // Demographics" toggled off) silently rendered nothing. The nav bar itself still
210
- // hides for a single step (see template) so a lone section looks like a plain form.
211
- this.isSectionStepper = !this.isStepper &&
212
- (this.formSchema.sectionStepper === true ||
213
- (this.formSchema.sectionStepper !== false && topLevelGroups.length === this.fieldList.length && topLevelGroups.length >= 1));
214
- if (this.isSectionStepper) {
215
- this.sectionSteps = topLevelGroups;
216
- this.currentSectionStep = 0;
217
- this.stepValidationStates = this.sectionSteps.map(() => 'untouched' as const);
218
- this.stepFieldNames = this.sectionSteps.map(s => this._collectFieldNames(s.sectionConfig?.children || []));
219
- setTimeout(() => this._emitStepChange());
220
- }
221
- // Pre-seed array values (allowMulti GROUP initial data) into the controller
222
- // BEFORE initializeForm() runs. form-field.initGroupField() calls
223
- // controller.getFieldValue(groupKey) during ngOnInit to determine how many
224
- // rows to create — it must find the array already there or it falls back to
225
- // a single empty row and loses all initial values.
226
- if (this.initialValues) {
227
- Object.entries(this.initialValues).forEach(([key, value]) => {
228
- if (Array.isArray(value)) {
229
- this.controller.updateField(key, value);
230
- }
231
- });
232
- }
233
- this.initializeForm();
234
- if (this.mode === 'EDIT' && this.formSchema?.editConfig?.loadApiUrl) {
235
- this.loadEditData();
236
- } else if (this.initialValues) {
237
- this.controller.initialize(this.initialValues);
238
- this._markReady();
239
- } else {
240
- this._markReady();
241
- }
242
- }
243
-
244
-
245
- initializeForm(): void {
246
- this.formGroup = this.fb.group({});
247
- this.collectFields(this.fieldList);
248
-
249
- // In read-only mode, disable all controls so the form acts as a preview
250
- if (this.readOnly) {
251
- this.formGroup.disable();
252
- }
253
-
254
- // Emit value changes to parent (skipped in read-only mode)
255
- if (!this.readOnly) {
256
- this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
257
- this.valueChange.emit(this.collectFormData());
258
- });
259
- }
260
- }
261
-
262
- collectFields(fields: FieldConfig[]): void {
263
- fields.forEach(field => {
264
- if (field.isEnabled === false || field.sectionConfig?.isEnabled === false) return;
265
-
266
- // Flat leaf fields: seed controller with default values
267
- if (field.name && field.type !== 'GROUP' && field.type !== 'ROW') {
268
- // Check if initialValues already has a value for this field
269
- const existingValue = this.initialValues?.[field.name];
270
- const value = existingValue !== undefined
271
- ? existingValue
272
- : (field.defaultValue !== undefined ? field.defaultValue : null);
273
- this.controller.updateField(field.name, value);
274
- }
275
- // Recurse into ROW children
276
- if (field.type === 'ROW' && field.children?.length) {
277
- this.collectFields(field.children);
278
- }
279
- // Recurse into GROUP children
280
- if (field.type === 'GROUP' && field.sectionConfig?.children?.length) {
281
- this.collectFields(field.sectionConfig.children);
282
- }
283
- });
284
- }
285
-
286
- // ───────────────────────────────────────────────────────────────────────────
287
- // Submit
288
- // ───────────────────────────────────────────────────────────────────────────
289
-
290
- handleSubmit(): void {
291
- if (this.isStepper && this.currentStep < this.fieldList.length - 1) {
292
- if (this.validate()) this.nextStep();
293
- return;
294
- }
295
- if (!this.validate()) return;
296
-
297
- this.submitToApi(this.collectFormData(), 'submit');
298
- }
299
-
300
- // ───────────────────────────────────────────────────────────────────────────
301
- // Cancel
302
- // ───────────────────────────────────────────────────────────────────────────
303
-
304
- /**
305
- * Universal action handler for any button click.
306
- * One handler decides how to process based on action.kind.
307
- */
308
- handleButtonClick(btn: ActionButtonConfig): void {
309
- const action = btn.action;
310
- if (!action) return;
311
-
312
- switch (action.kind) {
313
- case 'submit':
314
- this.handleSubmit();
315
- break;
316
- case 'draft':
317
- this.submitToApi(this.collectFormData(), 'draft', btn);
318
- break;
319
- case 'navigate':
320
- this.navigateTo(action.redirectUrl);
321
- break;
322
- case 'api':
323
- this.fireActionApiCall(action);
324
- break;
325
- case 'emit':
326
- this.actionClick.emit({ id: btn.id, formData: this.collectFormData() });
327
- break;
328
- case 'next':
329
- if (this.validate()) this.nextStep();
330
- break;
331
- case 'prev':
332
- this.previousStep();
333
- break;
334
- }
335
- }
336
-
337
- // ── Action-Redirects & API ──────────────────────────────────────────────────
338
-
339
- private fireActionApiCall(action: ActionConfig): void {
340
- if (!action.apiUrl) return;
341
- const headers = this.getHeaders();
342
- const method = action.method || 'POST';
343
- const body = action.extraPayload || {};
344
-
345
- this.isLoading = true;
346
- this.http.request(method, action.apiUrl, { body, headers }).subscribe({
347
- next: (response) => {
348
- this.isLoading = false;
349
- const msg = action.successMessage || 'Action successful';
350
- this.showAlert('success', msg, action.snackbarConfig);
351
- if (action.redirectUrl) this.navigateTo(action.redirectUrl);
352
- },
353
- error: (err) => {
354
- this.isLoading = false;
355
- const msg = action.errorMessage || 'Action failed';
356
- this.showAlert('error', msg, action.snackbarConfig);
357
- console.error('API Action error:', err);
358
- }
359
- });
360
- }
361
-
362
- /**
363
- * Constructs nested payload by checking field properties on form controls.
364
- */
365
- collectFormData(): { [key: string]: any } {
366
- const rawValue = this.extractGroupValue(this.formGroup);
367
- let payload = this.buildNestedPayload(rawValue, this.fieldList);
368
-
369
- // Merge extra fields based on the mode
370
- const config = this.mode === 'EDIT' ? this.formSchema.editConfig : this.formSchema.submitConfig;
371
- if (config?.extraPayload) {
372
- payload = this.deepMerge(payload, config.extraPayload);
373
- }
374
-
375
- return payload;
376
- }
377
-
378
- /**
379
- * Deep merges the source object (e.g. extraPayload) into the target object (e.g. form payload).
380
- */
381
- private deepMerge(target: any, source: any): any {
382
- if (typeof target !== 'object' || target === null) {
383
- return source;
384
- }
385
- if (typeof source !== 'object' || source === null) {
386
- return source;
387
- }
388
-
389
- if (Array.isArray(target) && Array.isArray(source)) {
390
- // For arrays, we merge objects at the same index
391
- const maxLength = Math.max(target.length, source.length);
392
- const mergedArray = [];
393
- for (let i = 0; i < maxLength; i++) {
394
- if (i < target.length && i < source.length) {
395
- mergedArray.push(this.deepMerge(target[i], source[i]));
396
- } else if (i < source.length) {
397
- mergedArray.push(source[i]);
398
- } else {
399
- mergedArray.push(target[i]);
400
- }
401
- }
402
- return mergedArray;
403
- }
404
-
405
- const merged = { ...target };
406
- Object.keys(source).forEach(key => {
407
- if (source[key] instanceof Object && key in target) {
408
- merged[key] = this.deepMerge(target[key], source[key]);
409
- } else {
410
- merged[key] = source[key];
411
- }
412
- });
413
- return merged;
414
- }
415
-
416
- private buildNestedPayload(rawValue: any, fields: FieldConfig[]): { [key: string]: any } {
417
- if (rawValue === null || typeof rawValue !== 'object') return rawValue;
418
-
419
- const payload: { [key: string]: any } = {};
420
- const processedKeys = new Set<string>();
421
-
422
- const processFields = (fieldList: FieldConfig[]) => {
423
- fieldList.forEach(field => {
424
- if (field.isEnabled === false || field.sectionConfig?.isEnabled === false) return;
425
-
426
- if (field.type === 'ROW' && field.children) {
427
- processFields(field.children);
428
- } else if (field.type === 'GROUP' && field.sectionConfig?.children) {
429
- // FormFieldComponent generates unnamed groups using their label inside the root FormGroup.
430
- const generatedKey = field.sectionConfig.label
431
- ? field.sectionConfig.label.replace(/(?:^\w|[A-Z]|\b\w)/g, (w, i) => i === 0 ? w.toLowerCase() : w.toUpperCase()).replace(/\s+/g, '')
432
- : '';
433
- const groupKey = field.sectionConfig.name || field.name || generatedKey || '__group__';
434
-
435
- processedKeys.add(groupKey);
436
-
437
- const groupRawValue = rawValue[groupKey];
438
- if (groupRawValue !== undefined) {
439
- // Identify if it's purely a visual section vs an explicit structural data block
440
- const isStructural = field.sectionConfig.name || field.name || field.sectionConfig.allowMulti;
441
-
442
- if (!isStructural) {
443
- // Visual section: Flatten its contents directly onto the target payload layer
444
- Object.assign(payload, this.buildNestedPayload(groupRawValue, field.sectionConfig.children));
445
- } else {
446
- // Structural block: process nested mappings / array instances
447
- const nestedData = (field.sectionConfig.allowMulti && Array.isArray(groupRawValue))
448
- ? groupRawValue.map(item => this.buildNestedPayload(item, field.sectionConfig!.children!))
449
- : this.buildNestedPayload(groupRawValue, field.sectionConfig.children);
450
-
451
- if (field.payloadPath) {
452
- this.setNestedValue(payload, field.payloadPath, nestedData);
453
- } else {
454
- payload[groupKey] = nestedData;
455
- }
456
- }
457
- }
458
- } else if (field.name) {
459
- processedKeys.add(field.name);
460
- if (rawValue[field.name] !== undefined) {
461
- const val = rawValue[field.name];
462
- if (field.payloadPath) {
463
- this.setNestedValue(payload, field.payloadPath, val);
464
- } else {
465
- payload[field.name] = val;
466
- }
467
- }
468
- }
469
- });
470
- };
471
-
472
- processFields(fields);
473
-
474
- // Preserve any hidden tracking keys (e.g. id, uuid, isEdit) that were present in the initial data
475
- Object.keys(rawValue).forEach(key => {
476
- if (!processedKeys.has(key)) {
477
- payload[key] = rawValue[key];
478
- }
479
- });
480
-
481
- return payload;
482
- }
483
-
484
- private setNestedValue(obj: any, path: string, value: any): void {
485
- // Regex matches the property name and optionally an array index e.g. "name[0]" -> regex yields ["name[0]", "name", "0"]
486
- const arrayRegex = /^([a-zA-Z0-9_]+)\[(\d+)\]$/;
487
-
488
- const parts = path.split('.');
489
- let current = obj;
490
-
491
- for (let i = 0; i < parts.length - 1; i++) {
492
- const part = parts[i];
493
- const match = part.match(arrayRegex);
494
-
495
- if (match) {
496
- const prop = match[1];
497
- const index = parseInt(match[2], 10);
498
- if (!current[prop]) current[prop] = [];
499
- if (!current[prop][index]) current[prop][index] = {};
500
- current = current[prop][index];
501
- } else {
502
- if (!current[part]) current[part] = {};
503
- current = current[part];
504
- }
505
- }
506
-
507
- const lastPart = parts[parts.length - 1];
508
- const matchLast = lastPart.match(arrayRegex);
509
- if (matchLast) {
510
- const prop = matchLast[1];
511
- const index = parseInt(matchLast[2], 10);
512
- if (!current[prop]) current[prop] = [];
513
- current[prop][index] = value;
514
- } else {
515
- current[lastPart] = value;
516
- }
517
- }
518
-
519
- private extractGroupValue(group: FormGroup): { [key: string]: any } {
520
- const result: { [key: string]: any } = {};
521
- Object.keys(group.controls).forEach(key => {
522
- const ctrl = group.controls[key];
523
- if (ctrl instanceof FormArray) {
524
- // Repeater → array of objects
525
- result[key] = (ctrl.controls as FormGroup[]).map(fg => this.extractGroupValue(fg));
526
- } else if (ctrl instanceof FormGroup) {
527
- // Nested single group → nested object
528
- result[key] = this.extractGroupValue(ctrl);
529
- } else {
530
- result[key] = ctrl?.value ?? null;
531
- }
532
- });
533
- return result;
534
- }
535
-
536
- validate(): boolean {
537
- if (this.formGroup.invalid) {
538
- this.formGroup.markAllAsTouched();
539
-
540
- // Re-open any repeater card/accordion entry whose nested fields are invalid,
541
- // so its error messages (otherwise hidden behind a collapsed panel) become visible.
542
- let hadHiddenInvalidEntry = false;
543
- this.formFieldComponents?.forEach(field => {
544
- if (field.revealInvalidInstances()) hadHiddenInvalidEntry = true;
545
- });
546
-
547
- const message = hadHiddenInvalidEntry
548
- ? (this.controller.labels?.['ERR_MSG_HIDDEN_REQUIRED_FIELDS'] || 'Please fill in all required fields — some are inside a collapsed section.')
549
- : (this.controller.labels?.['ERR_MSG_REQUIRED_FIELDS'] || 'Please fill in all required fields before submitting.');
550
- this.showAlert('warning', message);
551
-
552
- this.scrollToFirstInvalidControl();
553
- return false;
554
- }
555
- return true;
556
- }
557
-
558
- scrollToFirstInvalidControl(): void {
559
- setTimeout(() => {
560
- // .is-invalid is set by form-field on actual control elements (not on <form>).
561
- // The fallback targets native inputs/selects directly so we never accidentally
562
- // scroll to the <form> element, which also receives .ng-invalid.ng-touched.
563
- const firstInvalidControl =
564
- document.querySelector('.is-invalid') ||
565
- document.querySelector(
566
- 'input.ng-invalid.ng-touched, select.ng-invalid.ng-touched, textarea.ng-invalid.ng-touched'
567
- );
568
- if (firstInvalidControl) {
569
- firstInvalidControl.scrollIntoView({ behavior: 'smooth', block: 'center' });
570
- }
571
- }, 100);
572
- }
573
-
574
- submitToApi(formData: any, actionType: 'submit' | 'draft' = 'submit', btn?: ActionButtonConfig): void {
575
- const isEdit = this.mode === 'EDIT';
576
- const configLine: any = isEdit ? this.formSchema.editConfig : this.formSchema.submitConfig;
577
- const action = btn?.action;
578
-
579
- // Build the final payload, merging draft extraPayload if applicable
580
- let finalPayload = formData;
581
- if (actionType === 'draft' && action?.extraPayload) {
582
- finalPayload = this.deepMerge(finalPayload, action.extraPayload);
583
- }
584
-
585
- // Fallback for emitted outputs if no API URL is defined centrally
586
- if (!configLine || (!configLine.apiUrl && !configLine.submitApiUrl)) {
587
- if (actionType === 'draft') {
588
- this.draftSave.emit(JSON.stringify(finalPayload));
589
- if (action?.redirectUrl) this.navigateTo(action.redirectUrl);
590
- } else {
591
- this.submit.emit(finalPayload);
592
- }
593
- return;
594
- }
595
-
596
- const apiUrl = isEdit ? configLine.submitApiUrl : configLine.apiUrl;
597
- const method = isEdit ? (configLine.submitMethod || 'PATCH') : (configLine.method || 'POST');
598
- const headers = this.getHeaders();
599
-
600
- // Messages and redirect URLs (Prefer button action overrides if present)
601
- const successMsg = actionType === 'draft'
602
- ? (action?.successMessage || 'Draft saved successfully')
603
- : (action?.successMessage || configLine.successMessage || 'Form submitted successfully');
604
-
605
- const errorMsg = actionType === 'draft'
606
- ? (action?.errorMessage || 'Failed to save draft')
607
- : (action?.errorMessage || configLine.errorMessage || 'Failed to submit form');
608
-
609
- const redirectUrl = actionType === 'draft'
610
- ? (action?.redirectUrl || configLine.redirectUrl)
611
- : (action?.redirectUrl || configLine.redirectUrl);
612
-
613
- if (actionType === 'draft') {
614
- this.isDraftLoading = true;
615
- } else {
616
- this.isLoading = true;
617
- }
618
-
619
- this.http.request(method, apiUrl, { body: finalPayload, headers }).subscribe({
620
- next: (response) => {
621
- this.showAlert('success', successMsg, action?.snackbarConfig || configLine.snackbarConfig);
622
-
623
- if (actionType === 'draft') {
624
- this.draftSave.emit(JSON.stringify(response));
625
- this.isDraftLoading = false;
626
- } else {
627
- this.submit.emit(response);
628
- this.isLoading = false;
629
- }
630
-
631
- if (redirectUrl) {
632
- setTimeout(() => this.navigateTo(redirectUrl), 1500);
633
- }
634
- },
635
- error: (err) => {
636
- this.showAlert('error', errorMsg, action?.snackbarConfig || configLine.snackbarConfig);
637
- console.error(actionType === 'draft' ? 'Draft save error:' : 'Submit error:', err);
638
- this.isLoading = false;
639
- this.isDraftLoading = false;
640
- }
641
- });
642
- }
643
-
644
- showAlert(type: 'success' | 'error' | 'warning' | 'info', message: string, customConfig?: any): void {
645
- this.snackbarService.show({
646
- variant: type,
647
- message: message,
648
- showCloseButton: true,
649
- ...customConfig
650
- });
651
- }
652
-
653
- /** Builds HttpHeaders from the token stored in the controller (sourced from configJSON). */
654
- getHeaders(): HttpHeaders {
655
- let headers = new HttpHeaders();
656
- if (this.controller.token) {
657
- const headerName = this.controller.tokenHeader || 'Authorization';
658
- headers = headers.set(headerName, this.controller.token);
659
- }
660
- return headers;
661
- }
662
-
663
- // ───────────────────────────────────────────────────────────────────────────
664
- // Stepper
665
- // ───────────────────────────────────────────────────────────────────────────
666
-
667
- nextStep(): void {
668
- if (this.currentStep < this.fieldList.length - 1) this.currentStep++;
669
- }
670
-
671
- previousStep(): void {
672
- if (this.currentStep > 0) this.currentStep--;
673
- }
674
-
675
- get canGoNext(): boolean { return this.currentStep < this.fieldList.length - 1; }
676
- get canGoPrevious(): boolean { return this.currentStep > 0; }
677
- get currentStepConfig(): FieldConfig | undefined {
678
- return this.isStepper ? this.fieldList[this.currentStep] : undefined;
679
- }
680
-
681
- // ── Section Stepper (public API called by host via ViewChild) ────────────────
682
-
683
- /** Advance to the next section step. Called by the host footer "Next" button.
684
- * Validates the current step first — marks it valid (green) or warning (orange). */
685
- navigateToNext(): void {
686
- if (!this.isSectionStepper) return;
687
- if (this.currentSectionStep < this.sectionSteps.length - 1) {
688
- this._validateStep(this.currentSectionStep);
689
- this.currentSectionStep++;
690
- this._emitStepChange();
691
- }
692
- }
693
-
694
- /** Go back to the previous section step. Called by the host footer "Previous" button. */
695
- navigateToPrevious(): void {
696
- if (!this.isSectionStepper) return;
697
- if (this.currentSectionStep > 0) {
698
- this.currentSectionStep--;
699
- this._emitStepChange();
700
- }
701
- }
702
-
703
- /** Jump directly to a specific section step by index.
704
- * Validates the step being left so the badge state updates correctly. */
705
- goToSectionStep(index: number): void {
706
- if (!this.isSectionStepper) return;
707
- if (index < 0 || index >= this.sectionSteps.length || index === this.currentSectionStep) return;
708
- this._validateStep(this.currentSectionStep);
709
- this.currentSectionStep = index;
710
- this._emitStepChange();
711
- }
712
-
713
- get isSectionStepFirst(): boolean { return this.currentSectionStep === 0; }
714
- get isSectionStepLast(): boolean { return this.currentSectionStep === this.sectionSteps.length - 1; }
715
-
716
- /** Returns the SectionConfig for a given step — passed to lib-form-section.
717
- * The outer label is intentionally omitted because the stepper nav already
718
- * displays it; showing it again inside the content would be redundant. */
719
- getSectionStepConfig(step: FieldConfig): any {
720
- const { label, ...rest } = step.sectionConfig! as any;
721
- return rest;
722
- }
723
-
724
- private _emitStepChange(): void {
725
- const step = this.sectionSteps[this.currentSectionStep];
726
- this.stepChange.emit({
727
- currentStep: this.currentSectionStep,
728
- totalSteps: this.sectionSteps.length,
729
- isFirst: this.isSectionStepFirst,
730
- isLast: this.isSectionStepLast,
731
- stepLabel: step?.sectionConfig?.label || `Step ${this.currentSectionStep + 1}`
732
- });
733
- }
734
-
735
- /** Marks all controls in the given step as touched, records valid/warning state,
736
- * and returns whether the step had any invalid control. */
737
- private _validateStep(stepIndex: number): boolean {
738
- const names = this.stepFieldNames[stepIndex] || [];
739
- let hasInvalid = false;
740
- names.forEach(name => {
741
- const ctrl = this.formGroup.get(name);
742
- if (!ctrl) return;
743
- if (ctrl instanceof FormArray) {
744
- ctrl.controls.forEach(c => c.markAllAsTouched());
745
- if (ctrl.invalid) hasInvalid = true;
746
- } else if (ctrl instanceof FormGroup) {
747
- ctrl.markAllAsTouched();
748
- if (ctrl.invalid) hasInvalid = true;
749
- } else {
750
- ctrl.markAsTouched();
751
- if (ctrl.invalid) hasInvalid = true;
752
- }
753
- });
754
- this.stepValidationStates[stepIndex] = hasInvalid ? 'warning' : 'valid';
755
- return hasInvalid;
756
- }
757
-
758
- /** Public hard-validation check for the host app: does the CURRENT step have any
759
- * invalid required field? navigateToNext()/goToSectionStep() only mark a step's badge
760
- * as a soft "warning" and still advance regardless — hosts that need to actually BLOCK
761
- * navigation until required fields are filled (e.g. the beneficiary add/edit wizard)
762
- * should call this before invoking navigateToNext(). Also marks the step's controls as
763
- * touched so the field-level error states render immediately. */
764
- isCurrentStepValid(): boolean {
765
- if (!this.isSectionStepper) return true;
766
- return !this._validateStep(this.currentSectionStep);
767
- }
768
-
769
- /** Recursively collects all leaf field names from a set of FieldConfigs. */
770
- private _collectFieldNames(fields: FieldConfig[]): string[] {
771
- const names: string[] = [];
772
- fields.forEach(f => {
773
- if (f.isEnabled === false) return;
774
- if (f.name && f.type !== 'GROUP' && f.type !== 'ROW') {
775
- names.push(f.name);
776
- }
777
- if (f.type === 'ROW' && f.children?.length) {
778
- names.push(...this._collectFieldNames(f.children));
779
- }
780
- if (f.type === 'GROUP' && f.sectionConfig?.children?.length) {
781
- if (f.name || f.sectionConfig.name) {
782
- names.push((f.name || f.sectionConfig.name)!);
783
- }
784
- names.push(...this._collectFieldNames(f.sectionConfig.children));
785
- }
786
- });
787
- return names;
788
- }
789
-
790
- // ── Action Labels ──────────────────────────────────────────────────────────
791
-
792
- get nextLabel(): string {
793
- const key = this.formSchema?.labels?.nextLabel || 'Next';
794
- return this.labels[key] || key;
795
- }
796
-
797
- get submitLabel(): string {
798
- const btn = this.getButtonByActionKind('submit');
799
- if (btn?.label) return this.labels[btn.label] || btn.label;
800
- const key = this.formSchema?.labels?.submitLabel || 'Submit';
801
- return this.labels[key] || key;
802
- }
803
-
804
- get previousLabel(): string {
805
- const key = this.formSchema?.labels?.previousLabel || 'Previous';
806
- return this.labels[key] || key;
807
- }
808
-
809
- // ── Action Bar helpers ─────────────────────────────────────────────────────
810
-
811
- get actionBarConfig(): ActionBarConfig | undefined {
812
- return this.formSchema?.actionBarConfig;
813
- }
814
-
815
- /**
816
- * Returns buttons for a given alignment, sorted by `order` (stable).
817
- */
818
- getButtonsForAlignment(alignment: 'left' | 'right'): ActionButtonConfig[] {
819
- return (this.actionBarConfig?.buttons || [])
820
- .filter(b => !b.hidden && (b.alignment ?? 'right') === alignment)
821
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
822
- }
823
-
824
- getButtonLabel(btn: ActionButtonConfig): string {
825
- if (btn.action?.kind === 'submit' && this.isStepper && this.canGoNext) {
826
- return this.nextLabel;
827
- }
828
- const key = btn.label || btn.id;
829
- return this.labels[key] || key;
830
- }
831
-
832
- isButtonDisabled(btn: ActionButtonConfig): boolean {
833
- if (btn.disabled) return true;
834
- const kind = btn.action?.kind;
835
- if (kind === 'submit' || kind === 'draft' || kind === 'api') {
836
- return this.isLoading || this.isDraftLoading;
837
- }
838
- return false;
839
- }
840
-
841
- private getButtonByActionKind(kind: string): ActionButtonConfig | undefined {
842
- return (this.actionBarConfig?.buttons || []).find(b => b.action?.kind === kind);
843
- }
844
-
845
- // ── Navigation helper ──────────────────────────────────────────────────────
846
-
847
- private navigateTo(url?: string): void {
848
- if (!url) return;
849
- if (url.startsWith('http://') || url.startsWith('https://')) {
850
- window.location.href = url;
851
- } else {
852
- this.router.navigateByUrl(url);
853
- }
854
- }
855
-
856
- // ── GEOGRAPHY_DYNAMIC / Hierarchy expansion ───────────────────────────────
857
- // Supports any API-driven hierarchy template. All paths, URLs, field names
858
- // and dependency params are driven entirely by the geographyConfig JSON node —
859
- // nothing is hardcoded here. New deployments only need to update the JSON.
860
-
861
- private _hasHierarchyDynamic(obj: any): boolean {
862
- if (!obj || typeof obj !== 'object') return false;
863
- if ((obj as any)['type'] === 'GEOGRAPHY_DYNAMIC') return true;
864
- return Object.values(obj as object).some((v: any) => this._hasHierarchyDynamic(v));
865
- }
866
-
867
- private _expandHierarchyNodes(schema: any): Observable<any> {
868
- return this._expandNodeRecursive(schema);
869
- }
870
-
871
- private _expandNodeRecursive(obj: any): Observable<any> {
872
- if (!this._hasHierarchyDynamic(obj)) return of(obj);
873
-
874
- if (Array.isArray(obj)) {
875
- const tasks$: Observable<any[]>[] = (obj as any[]).map((item: any) => {
876
- if (item && (item as any)['type'] === 'GEOGRAPHY_DYNAMIC') {
877
- return this._expandSingleNode(item);
878
- }
879
- return this._expandNodeRecursive(item).pipe(map((v: any) => [v]));
880
- });
881
- return (tasks$.length > 0 ? forkJoin(tasks$) : of([] as any[][])).pipe(
882
- map((arrays: any[][]) => ([] as any[]).concat(...arrays))
883
- );
884
- }
885
-
886
- if (typeof obj === 'object' && obj !== null) {
887
- const keysWithDynamic = Object.keys(obj as object).filter(k =>
888
- this._hasHierarchyDynamic((obj as any)[k])
889
- );
890
- if (keysWithDynamic.length === 0) return of(obj);
891
- const updates$ = keysWithDynamic.map(key =>
892
- this._expandNodeRecursive((obj as any)[key]).pipe(map((v: any) => ({ key, value: v })))
893
- );
894
- return forkJoin(updates$).pipe(
895
- map((updates: { key: string; value: any }[]) => {
896
- const result: any = { ...(obj as object) };
897
- updates.forEach(u => { result[u.key] = u.value; });
898
- return result;
899
- })
900
- );
901
- }
902
-
903
- return of(obj);
904
- }
905
-
906
- private _expandSingleNode(node: any): Observable<any[]> {
907
- const rawCfg = (node as any)['geographyConfig'] || (node as any)['hierarchyConfig'];
908
- if (!rawCfg) return of([]);
909
- const cfg = this._normalizeHierarchyCfg(rawCfg);
910
- if (!cfg.templateApiUrl) return of([]);
911
- return this._fetchHierarchyTemplate(cfg).pipe(
912
- map((template: any) => template ? this._buildHierarchyFields(cfg, template) : []),
913
- catchError(() => of([]))
914
- );
915
- }
916
-
917
- private _normalizeHierarchyCfg(raw: any): HierarchyCfg {
918
- // New nested format: templateApi / dataApi / structure / field sub-objects
919
- if (raw['templateApi'] && typeof raw['templateApi'] === 'object') {
920
- const ta = raw['templateApi'];
921
- const da = raw['dataApi'] || {};
922
- const st = raw['structure'] || {};
923
- const fi = raw['field'] || {};
924
- return {
925
- templateApiUrl: ta['apiUrl'] || ta['url'] || '',
926
- templateListPath: ta['listPath'] ?? null,
927
- templateSelect: ta['select'] || 'default',
928
- templateSelectField: ta['selectField'] || 'isDefault',
929
- templateSelectCode: ta['selectCode'],
930
- rootCodePath: st['rootCodePath'] || 'rootClassCode',
931
- templateCodePath: st['templateCodePath'] || 'templateCode',
932
- hierarchyListPath: st['hierarchyListPath'] || 'mdmclasshierarchytemplatestructList',
933
- parentField: st['parentField'] || 'parentCode',
934
- childField: st['childField'] || 'childCode',
935
- dataApiUrl: da['apiUrl'] || da['url'] || '',
936
- dataPath: da['dataPath'] || 'elements',
937
- labelPath: da['labelPath'] || 'name[0].text',
938
- valuePath: da['valuePath'] || 'code',
939
- colSpan: fi['colSpan'] ?? 12,
940
- payloadPrefix: fi['payloadPathPrefix'] || '',
941
- dependencyParam: fi['dependency']?.['paramName'] || 'parentDataCode',
942
- nodes: fi['nodes'] || {},
943
- branchSubType: fi['branchSubType'] || 'SINGLE',
944
- showLoader: raw['showLoader'] ?? raw['field']?.['showLoader'] ?? false,
945
- };
946
- }
947
- // Legacy flat format (backward compatibility with existing geographyConfig JSON)
948
- const reqCodes: string[] = raw['requiredClassCodes'] || [];
949
- const payloadMap: Record<string, string> = raw['classCodePayloadFieldMap'] || {};
950
- const nodes: Record<string, any> = {};
951
- [...new Set([...reqCodes, ...Object.keys(payloadMap)])].forEach(code => {
952
- nodes[code] = { payloadField: payloadMap[code], required: reqCodes.includes(code) };
953
- });
954
- return {
955
- templateApiUrl: raw['templateApiUrl'] || '',
956
- templateListPath: null,
957
- templateSelect: raw['useDefault'] !== false ? 'default' : 'first',
958
- templateSelectField: 'isDefault',
959
- rootCodePath: 'rootClassCode',
960
- templateCodePath: 'templateCode',
961
- hierarchyListPath: 'mdmclasshierarchytemplatestructList',
962
- parentField: 'parentCode',
963
- childField: 'childCode',
964
- dataApiUrl: raw['dataApiUrl'] || '',
965
- dataPath: raw['dataPath'] || 'elements',
966
- labelPath: raw['labelPath'] || 'name[0].text',
967
- valuePath: raw['valuePath'] || 'code',
968
- colSpan: raw['colSpan'] ?? 12,
969
- payloadPrefix: raw['payloadPathPrefix'] || '',
970
- dependencyParam: 'parentDataCode',
971
- nodes,
972
- branchSubType: 'SINGLE',
973
- showLoader: raw['showLoader'] ?? false,
974
- };
975
- }
976
-
977
- private _fetchHierarchyTemplate(cfg: HierarchyCfg): Observable<any> {
978
- const headers = this.getHeaders();
979
- return this.http.get<any>(cfg.templateApiUrl, { headers }).pipe(
980
- map((response: any) => {
981
- let list: any[];
982
- if (cfg.templateListPath) {
983
- list = this._getPathValue(response, cfg.templateListPath) || [];
984
- } else if (Array.isArray(response)) {
985
- list = response;
986
- } else {
987
- list = [response];
988
- }
989
- if (!Array.isArray(list) || list.length === 0) return null;
990
- if (cfg.templateSelect === 'first') return list[0];
991
- if (cfg.templateSelect === 'byCode') {
992
- return list.find((t: any) => this._getPathValue(t, cfg.templateCodePath) === cfg.templateSelectCode) || list[0];
993
- }
994
- return list.find((t: any) => !!t[cfg.templateSelectField]) || list[0];
995
- }),
996
- catchError(() => of(null))
997
- );
998
- }
999
-
1000
- private _buildHierarchyFields(cfg: HierarchyCfg, template: any): any[] {
1001
- const rootCode: string = this._getPathValue(template, cfg.rootCodePath) || '';
1002
- const templateCode: string = this._getPathValue(template, cfg.templateCodePath) || '';
1003
- const hierList: any[] = this._getPathValue(template, cfg.hierarchyListPath) || [];
1004
-
1005
- const childrenMap = new Map<string, string[]>();
1006
- hierList.forEach((s: any) => {
1007
- const parent = s[cfg.parentField];
1008
- const child = s[cfg.childField];
1009
- if (parent != null && child != null) {
1010
- if (!childrenMap.has(parent)) childrenMap.set(parent, []);
1011
- childrenMap.get(parent)!.push(child);
1012
- }
1013
- });
1014
-
1015
- const fields: any[] = [];
1016
-
1017
- const buildField = (code: string, parentFieldName: string | null, visibilityExpression?: string): void => {
1018
- const nodeOvr: any = cfg.nodes[code] || {};
1019
-
1020
- if (nodeOvr['hidden']) {
1021
- (childrenMap.get(code) || []).forEach(c => buildField(c, parentFieldName, visibilityExpression));
1022
- return;
1023
- }
1024
-
1025
- const fieldName: string = nodeOvr['name'] || this._codeToFieldName(code);
1026
- const label: string = nodeOvr['label'] || this._codeToLabel(code);
1027
- const payloadField: string = nodeOvr['payloadField'] || fieldName;
1028
- const colSpan: number = nodeOvr['colSpan'] ?? cfg.colSpan;
1029
- const payloadPath: string = cfg.payloadPrefix ? `${cfg.payloadPrefix}.${payloadField}` : payloadField;
1030
-
1031
- const apiUrl: string = (cfg.dataApiUrl || '')
1032
- .replace(/\{classCode\}/g, encodeURIComponent(code))
1033
- .replace(/\{templateCode\}/g, encodeURIComponent(templateCode));
1034
-
1035
- const optionConfig: any = {
1036
- apiUrl,
1037
- dataPath: cfg.dataPath,
1038
- labelPath: cfg.labelPath,
1039
- valuePath: cfg.valuePath,
1040
- showLoader: cfg.showLoader,
1041
- };
1042
- if (parentFieldName && !nodeOvr['noDependency']) {
1043
- optionConfig['dependencies'] = { [cfg.dependencyParam]: parentFieldName };
1044
- }
1045
-
1046
- const fieldDef: any = {
1047
- name: fieldName,
1048
- label,
1049
- payloadPath,
1050
- type: 'DROPDOWN',
1051
- subType: 'SINGLE',
1052
- colSpan,
1053
- isEnabled: true,
1054
- optionConfig,
1055
- };
1056
- if (visibilityExpression) fieldDef['visibilityExpression'] = visibilityExpression;
1057
- if (nodeOvr['required']) {
1058
- fieldDef['required'] = true;
1059
- fieldDef['errorMessage'] = nodeOvr['errorMessage'] || `Please select ${label.toLowerCase()}`;
1060
- }
1061
- if (nodeOvr['placeholder']) fieldDef['placeholder'] = nodeOvr['placeholder'];
1062
-
1063
- fields.push(fieldDef);
1064
-
1065
- const children = childrenMap.get(code) || [];
1066
- if (children.length > 1) {
1067
- // Branch point: dynamically build Area Type selector from children
1068
- const branchOptions = children.map(c => {
1069
- const childOvr: any = cfg.nodes[c] || {};
1070
- const childFieldName = childOvr['name'] || this._codeToFieldName(c);
1071
- const childLabel = childOvr['label'] || this._codeToLabel(c);
1072
- return { label: childLabel, code: childFieldName };
1073
- });
1074
-
1075
- const areaTypeField: any = {
1076
- name: 'areaType',
1077
- label: 'Area Type',
1078
- type: 'DROPDOWN',
1079
- subType: cfg.branchSubType,
1080
- colSpan,
1081
- isEnabled: true,
1082
- optionConfig: { optionList: branchOptions },
1083
- };
1084
- if (visibilityExpression) areaTypeField['visibilityExpression'] = visibilityExpression;
1085
- fields.push(areaTypeField);
1086
-
1087
- // Skip structural branch-type nodes (e.g. RURAL, URABN) — they carry no MDM data.
1088
- // Walk their descendants with `fieldName` (the linear parent before the fork, e.g. State)
1089
- // as the dependency, so the first real data level loads correctly.
1090
- children.forEach(c => {
1091
- const childOvr: any = cfg.nodes[c] || {};
1092
- const childFieldName = childOvr['name'] || this._codeToFieldName(c);
1093
- const branchVisExpr = `areaType === '${childFieldName}'`;
1094
- const branchDescendants = childrenMap.get(c) || [];
1095
- if (branchDescendants.length === 0) {
1096
- // Unusual: branch-type node has no descendants — fall back to treating it as a data field
1097
- buildField(c, fieldName, branchVisExpr);
1098
- } else {
1099
- // Skip the branch-type node; walk its children with the pre-fork parent as dependency
1100
- branchDescendants.forEach(desc => buildField(desc, fieldName, branchVisExpr));
1101
- }
1102
- });
1103
- } else {
1104
- children.forEach(c => buildField(c, fieldName, visibilityExpression));
1105
- }
1106
- };
1107
-
1108
- buildField(rootCode, null);
1109
- return fields;
1110
- }
1111
-
1112
- private _getPathValue(obj: any, path: string): any {
1113
- if (!path || obj == null) return obj;
1114
- return path.split('.').reduce((curr: any, key: string) => {
1115
- if (curr == null) return undefined;
1116
- const m = key.match(/^(\w+)\[(\d+)\]$/);
1117
- return m ? curr[m[1]]?.[Number(m[2])] : curr[key];
1118
- }, obj);
1119
- }
1120
-
1121
- private _codeToFieldName(classCode: string): string {
1122
- const suffix = (classCode.split('.').pop() || classCode).replace(/^REGION_/, '');
1123
- const words = suffix.split('_');
1124
- if (words.length === 0) return classCode.toLowerCase();
1125
- return words[0].toLowerCase() +
1126
- words.slice(1).map((w: string) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
1127
- }
1128
-
1129
- private _codeToLabel(classCode: string): string {
1130
- const suffix = (classCode.split('.').pop() || classCode).replace(/^REGION_/, '');
1131
- return suffix.split('_').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
1132
- }
1133
-
1134
- }