commons-shared-web-ui 0.0.53 → 0.0.54

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 (214) hide show
  1. package/fesm2022/commons-shared-web-ui.mjs +14823 -0
  2. package/fesm2022/commons-shared-web-ui.mjs.map +1 -0
  3. package/index.d.ts +4679 -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 -2532
  21. package/documentation/smart-table.md +0 -1221
  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/ng-package.json +0 -28
  27. package/src/lib/modules/alert/alert.models.ts +0 -6
  28. package/src/lib/modules/alert/alert.module.ts +0 -16
  29. package/src/lib/modules/alert/components/alert/alert.component.html +0 -27
  30. package/src/lib/modules/alert/components/alert/alert.component.scss +0 -92
  31. package/src/lib/modules/alert/components/alert/alert.component.ts +0 -81
  32. package/src/lib/modules/button/button.models.ts +0 -13
  33. package/src/lib/modules/button/button.module.ts +0 -16
  34. package/src/lib/modules/button/components/button/button.component.html +0 -22
  35. package/src/lib/modules/button/components/button/button.component.scss +0 -88
  36. package/src/lib/modules/button/components/button/button.component.ts +0 -67
  37. package/src/lib/modules/button-dropdown/button-dropdown.models.ts +0 -26
  38. package/src/lib/modules/button-dropdown/button-dropdown.module.ts +0 -22
  39. package/src/lib/modules/button-dropdown/button-dropdown.theme.scss +0 -87
  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 -41
  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 -148
  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 -1196
  162. package/src/lib/modules/smart-form/components/form-field/form-field.component.scss +0 -2014
  163. package/src/lib/modules/smart-form/components/form-field/form-field.component.ts +0 -2782
  164. package/src/lib/modules/smart-form/components/form-section/form-section.component.html +0 -65
  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 -131
  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 -1131
  170. package/src/lib/modules/smart-form/index.ts +0 -10
  171. package/src/lib/modules/smart-form/models/form-schema.model.ts +0 -749
  172. package/src/lib/modules/smart-form/models/hierarchy-config.model.ts +0 -21
  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 -36
  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 -1128
  183. package/src/lib/modules/smart-table/models/table-config.model.ts +0 -213
  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/shared-ui.module.ts +0 -50
  209. package/src/lib/utils/constants.ts +0 -11
  210. package/src/lib/utils/storage.utils.ts +0 -37
  211. package/src/lib/utils/string.utils.ts +0 -23
  212. package/src/lib/utils/translation.utils.ts +0 -87
  213. package/src/public-api.ts +0 -121
  214. package/tsconfig.lib.json +0 -15
@@ -1,1128 +0,0 @@
1
- import { Component, EventEmitter, Input, OnInit, Output, OnChanges, SimpleChanges, AfterViewInit, OnDestroy, ViewChildren, QueryList, ElementRef, ChangeDetectorRef, NgZone, inject, LOCALE_ID, HostListener } from '@angular/core';
2
- import { TableConfig, TableColumn, TableAction, TableFilter, TableDataChangeEvent, TableRowSaveEvent } from '../../models/table-config.model';
3
- import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
4
- import { formatDate } from '@angular/common';
5
- import { Router } from '@angular/router';
6
- import { catchError, finalize, map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
7
- import { of, forkJoin, Subject } from 'rxjs';
8
-
9
- @Component({
10
- selector: 'lib-smart-table',
11
- templateUrl: './smart-table.component.html',
12
- styleUrls: ['./smart-table.component.scss'],
13
- standalone: false
14
- })
15
- export class SmartTableComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
16
- @Input() config!: TableConfig;
17
-
18
- /**
19
- * External data mode: pass table rows directly from the parent.
20
- * When this input is provided, the component will NOT make any internal API calls.
21
- * Instead, it emits sortChange / pageChange / searchChange / filterChange events
22
- * so the parent can fetch and supply updated data.
23
- */
24
- @Input() tableData?: any[];
25
-
26
- /**
27
- * Total number of items — used by the pagination component when operating in
28
- * external-data mode. Must be kept in sync by the parent.
29
- */
30
- @Input() totalItemsCount?: number;
31
-
32
- @Output() action = new EventEmitter<{ action: TableAction, row: any }>();
33
- @Output() topAction = new EventEmitter<TableAction>(); // For top bar buttons
34
- @Output() filterChange = new EventEmitter<{ key: string, value: any }>();
35
- @Output() rowSelect = new EventEmitter<any[]>();
36
- @Output() columnClick = new EventEmitter<{ row: any, column: string }>();
37
-
38
- /**
39
- * Pre-selected row objects. These are tracked across pages.
40
- * Matching incoming data rows (via rowIdField) will be checked automatically.
41
- */
42
- @Input() selectedRows: any[] = [];
43
-
44
- /** Emitted in external-data mode when the user changes the sort column/direction. */
45
- @Output() sortChange = new EventEmitter<TableDataChangeEvent>();
46
- /** Emitted in external-data mode when the user changes the page or page size. */
47
- @Output() pageChange = new EventEmitter<TableDataChangeEvent>();
48
- /** Emitted in external-data mode when the user types in the search box. */
49
- @Output() searchChange = new EventEmitter<TableDataChangeEvent>();
50
- /** Emitted when an inline-edited or inline-added row is saved. */
51
- @Output() rowSave = new EventEmitter<TableRowSaveEvent>();
52
-
53
- data: any[] = [];
54
- totalItems: number = 0;
55
- currentPage: number = 1;
56
- loading: boolean = false;
57
- originalRowsCache = new Map<any, any>();
58
-
59
- // State
60
- activeSort: { key: string, direction: 'ASC' | 'DESC' } = { key: '', direction: 'ASC' };
61
- private isSortActive: boolean = false; // true only when orderBy is explicitly configured or user has sorted
62
- activeFilters: { [key: string]: any } = {};
63
- searchTerm: string = '';
64
- stickyColumnStyles: { [key: string]: any } = {};
65
- hasStickyColumns: boolean = false;
66
- openDropdownId: string | null = null;
67
- /** Viewport-relative position used to render the dropdown as position:fixed. */
68
- dropdownPosition: { top: number; right: number } = { top: 0, right: 0 };
69
- /** Items and row for the currently open dropdown — rendered in a portal outside the table. */
70
- activeDropdownItems: any[] | null = null;
71
- activeDropdownRow: any = null;
72
-
73
- // --- Filter dropdown state ---
74
- openFilterKey: string | null = null;
75
- activeFilterLabels: { [key: string]: string } = {};
76
- /** Viewport-relative position used to render the filter panel as position:fixed. */
77
- filterPosition: { top: number; left: number } = { top: 0, left: 0 };
78
- /** The filter config for the currently open filter panel. */
79
- activeFilterData: any | null = null;
80
-
81
- // --- Delete confirmation modal state ---
82
- deleteModalOpen = false;
83
- deleteModalConfig: any = {};
84
- private pendingDeleteAction: { item: any; row: any } | null = null;
85
-
86
- searchSubject = new Subject<string>();
87
-
88
- @ViewChildren('stickyHeader') stickyHeaders!: QueryList<ElementRef>;
89
- private resizeObserver: ResizeObserver | null = null;
90
- private locale = inject(LOCALE_ID);
91
-
92
- constructor(
93
- private http: HttpClient,
94
- private router: Router,
95
- private cdr: ChangeDetectorRef,
96
- private ngZone: NgZone
97
- ) {
98
- // Debounce search input
99
- this.searchSubject.pipe(
100
- debounceTime(this.config?.searchConfig?.debounceTime || 300),
101
- distinctUntilChanged()
102
- ).subscribe(term => {
103
- const minChars = this.config?.searchConfig?.minimumCharacter ?? 3;
104
- // Stay idle for partial input below the threshold; clearing the box always re-triggers.
105
- if (term.length > 0 && term.length < minChars) {
106
- return;
107
- }
108
- this.searchTerm = term;
109
- this.currentPage = 1;
110
- if (this.tableData !== undefined) {
111
- // External-data mode: delegate to parent via event
112
- this.searchChange.emit(this.buildChangeEvent());
113
- } else {
114
- this.loadData();
115
- }
116
- });
117
- }
118
-
119
- ngOnInit(): void {
120
- if (this.config) {
121
- this.applyPaginationDefaults();
122
- if (this.config.sortBy) {
123
- this.activeSort.key = this.config.sortBy;
124
- }
125
- if (this.config.orderBy) {
126
- this.activeSort.direction = this.config.orderBy;
127
- this.isSortActive = true;
128
- }
129
- this.loadFilterOptions();
130
-
131
- if (this.tableData !== undefined) {
132
- // External-data mode: sync incoming data, skip internal API call
133
- this.data = this.tableData;
134
- if (this.totalItemsCount !== undefined) {
135
- this.totalItems = this.totalItemsCount;
136
- }
137
- } else {
138
- this.loadData();
139
- }
140
- }
141
- }
142
-
143
- ngOnChanges(changes: SimpleChanges): void {
144
- // External data changed — sync directly without an API call
145
- if (changes['tableData']) {
146
- this.data = this.tableData || [];
147
- }
148
- // Total count updated from parent
149
- if (changes['totalItemsCount'] && this.totalItemsCount !== undefined) {
150
- this.totalItems = this.totalItemsCount;
151
- }
152
- // Config changed (non-first) — reload filter opts; reload data only if NOT in external mode
153
- if (changes['config'] && !changes['config'].firstChange) {
154
- this.applyPaginationDefaults();
155
- this.loadFilterOptions();
156
- if (this.tableData === undefined) {
157
- this.loadData();
158
- }
159
- setTimeout(() => this.calculateStickyPositions());
160
- }
161
- if (changes['selectedRows']) {
162
- this.syncSelection();
163
- }
164
- }
165
-
166
- ngAfterViewInit(): void {
167
- this.setupResizeObserver();
168
- }
169
-
170
- ngOnDestroy(): void {
171
- if (this.resizeObserver) {
172
- this.resizeObserver.disconnect();
173
- }
174
- }
175
-
176
- private setupResizeObserver(): void {
177
- if (this.resizeObserver) {
178
- this.resizeObserver.disconnect();
179
- }
180
-
181
- this.resizeObserver = new ResizeObserver(() => {
182
- this.ngZone.run(() => {
183
- this.calculateStickyPositions();
184
- });
185
- });
186
-
187
- if (this.stickyHeaders) {
188
- this.stickyHeaders.changes.subscribe(() => {
189
- this.observeHeaders();
190
- // Also recalculate immediately
191
- setTimeout(() => this.calculateStickyPositions());
192
- });
193
- this.observeHeaders();
194
- }
195
- }
196
-
197
- private observeHeaders(): void {
198
- if (!this.resizeObserver || !this.stickyHeaders) return;
199
- this.stickyHeaders.forEach(header => {
200
- this.resizeObserver!.observe(header.nativeElement);
201
- });
202
- }
203
-
204
- loadData(): void {
205
- if (!this.config?.apiUrl) return;
206
-
207
- this.loading = true;
208
- let params: HttpParams;
209
-
210
- // --- Query Params Construction ---
211
- if (this.config.queryParamsConfig) {
212
- const qpConfig = this.config.queryParamsConfig;
213
- const pageKey = qpConfig.pageKey || 'page';
214
- const sizeKey = qpConfig.sizeKey || 'pageSize';
215
- const pageIndex = this.currentPage + (qpConfig.pageIndexOffset || 0);
216
-
217
- let paramsObj: any = {
218
- [pageKey]: pageIndex.toString(),
219
- [sizeKey]: (this.config.pagination?.pageSize || 10).toString()
220
- };
221
-
222
- if (this.activeSort.key) paramsObj['sortBy'] = this.activeSort.key;
223
- // Only include orderBy if explicitly configured or user has sorted a column
224
- if (this.isSortActive && this.activeSort.direction) paramsObj['orderBy'] = this.activeSort.direction;
225
-
226
- // Search Handling
227
- if (this.searchTerm) {
228
- const searchConfig = this.config.searchConfig;
229
- const searchKey = searchConfig?.searchKey || 'search';
230
-
231
- if (searchConfig?.handling === 'nested_string' && qpConfig.nestedStringConfig) {
232
- // Will be handled in nested string construction below
233
- } else {
234
- paramsObj[searchKey] = this.searchTerm;
235
- }
236
- }
237
-
238
- // Filter Handling (and Search if nested)
239
- if (qpConfig.filterHandling === 'nested_string' && qpConfig.nestedStringConfig) {
240
- const { paramName, baseValue, separator, assignment } = qpConfig.nestedStringConfig;
241
- let nestedString = baseValue || '';
242
- const assign = assignment || '=';
243
-
244
- // Add Filters
245
- Object.keys(this.activeFilters).forEach(key => {
246
- if (this.activeFilters[key]) {
247
- const prefix = nestedString ? separator : '';
248
- nestedString += `${prefix}${key}${assign}${this.activeFilters[key]}`;
249
- }
250
- });
251
-
252
- // Add Search if nested
253
- if (this.searchTerm && this.config.searchConfig?.handling === 'nested_string') {
254
- const searchKey = this.config.searchConfig.searchKey || 'SEARCH_TERM';
255
- const prefix = nestedString ? separator : '';
256
- nestedString += `${prefix}${searchKey}${assign}${this.searchTerm}`;
257
- }
258
-
259
- paramsObj[paramName] = nestedString;
260
- } else {
261
- // Standard handling
262
- Object.keys(this.activeFilters).forEach(key => {
263
- if (this.activeFilters[key]) paramsObj[key] = this.activeFilters[key];
264
- });
265
- }
266
- params = new HttpParams({ fromObject: paramsObj });
267
-
268
- } else {
269
- // --- Default Behavior (Backward Compatibility) ---
270
- let paramsObj: any = {};
271
-
272
- if (this.config.pagination?.enabled) {
273
- paramsObj['page'] = this.currentPage;
274
- paramsObj['pageSize'] = this.config.pagination.pageSize;
275
- }
276
-
277
- if (this.activeSort.key) {
278
- paramsObj['sortBy'] = this.activeSort.key;
279
- // Only include orderBy if explicitly configured or user has sorted a column
280
- if (this.isSortActive) paramsObj['orderBy'] = this.activeSort.direction;
281
- }
282
-
283
- if (this.searchTerm) {
284
- const searchKey = this.config.searchConfig?.searchKey || 'search';
285
- paramsObj[searchKey] = this.searchTerm;
286
- }
287
-
288
- Object.keys(this.activeFilters).forEach(key => {
289
- if (this.activeFilters[key]) {
290
- paramsObj[key] = this.activeFilters[key];
291
- }
292
- });
293
-
294
- // Custom Request Params Mapper (if provided)
295
- if (this.config.requestParams) {
296
- const customParams = this.config.requestParams(
297
- this.currentPage,
298
- this.config.pagination?.pageSize || 10,
299
- this.activeSort.key,
300
- this.activeSort.direction,
301
- this.searchTerm,
302
- this.activeFilters
303
- );
304
- if (customParams instanceof HttpParams) {
305
- // If function returns HttpParams, use it directly (caveat: might lose previous params if not careful in mapper)
306
- params = customParams;
307
- } else {
308
- paramsObj = { ...paramsObj, ...customParams };
309
- params = new HttpParams({ fromObject: paramsObj });
310
- }
311
- } else {
312
- params = new HttpParams({ fromObject: paramsObj });
313
- }
314
- }
315
-
316
- // --- Data Fetching ---
317
-
318
- // Check for separate count API
319
- const totalCountConfig = this.config.pagination?.totalCountConfig;
320
- let request$: any; // Observable
321
-
322
- if (totalCountConfig?.source === 'separate' && totalCountConfig.apiUrl) {
323
- const headers = this.getHeaders();
324
- const method = this.config.apiMethod || 'GET';
325
- const body = this.config.apiPayload || {};
326
-
327
- const dataRequest$ = method === 'POST' ? this.http.post<any>(this.config.apiUrl, body, { params, headers }) : this.http.get<any>(this.config.apiUrl, { params, headers });
328
- const countRequest$ = method === 'POST' ? this.http.post<any>(totalCountConfig.apiUrl, body, { params, headers }) : this.http.get<any>(totalCountConfig.apiUrl, { params, headers });
329
-
330
- request$ = forkJoin({
331
- data: dataRequest$,
332
- count: countRequest$
333
- }).pipe(
334
- map(({ data, count }) => {
335
- const dataPath = this.config.dataResponsePath !== undefined ? this.config.dataResponsePath : '';
336
- return {
337
- data: this.getValueByPath(data, dataPath),
338
- total: this.getValueByPath(count, totalCountConfig.responsePath || '')
339
- };
340
- })
341
- );
342
- } else {
343
-
344
- const headers = this.getHeaders();
345
- const method = this.config.apiMethod || 'GET';
346
- const body = this.config.apiPayload || {};
347
-
348
- const baseRequest$ = method === 'POST' ? this.http.post<any>(this.config.apiUrl, body, { params, headers }) : this.http.get<any>(this.config.apiUrl, { params, headers });
349
-
350
- request$ = baseRequest$.pipe(
351
- map(response => {
352
- const dataPath = this.config.dataResponsePath !== undefined ? this.config.dataResponsePath : '';
353
- const totalPath = totalCountConfig?.responsePath || '';
354
- return {
355
- data: this.getValueByPath(response, dataPath),
356
- // If source is 'same', try to get total from response, else default 0
357
- total: totalCountConfig ? this.getValueByPath(response, totalPath) : 0
358
- };
359
- })
360
- );
361
- }
362
-
363
- request$.pipe(
364
- finalize(() => this.loading = false),
365
- catchError(err => {
366
- console.error('Table Data Fetch Error', err);
367
- return of({ data: [], total: 0 });
368
- })
369
- )
370
- .subscribe((result: any) => {
371
- this.data = result.data || [];
372
- if (this.config.pagination) {
373
- this.totalItems = result.total || 0;
374
- }
375
- if (!this.config.columns || this.config.columns.length === 0) {
376
- if (this.data && this.data.length > 0) {
377
- this.config.columns = Object.keys(this.data[0]).map(key => ({
378
- key: key,
379
- label: this.toTitleCase(key),
380
- type: 'text',
381
- sortable: false,
382
- editable: false
383
- }));
384
- }
385
- }
386
- this.syncSelection();
387
- });
388
- }
389
-
390
- /**
391
- * Syncs the locally loaded data set with the externally provided selectedRows.
392
- * Marks 'selected' property on row objects to reflect checkbox state.
393
- */
394
- private syncSelection(): void {
395
- if (!this.data || !this.data.length || !this.config) return;
396
-
397
- const idField = (this.config as any).rowIdField || 'id';
398
- const selectedIds = new Set((this.selectedRows || []).map(r => r[idField]));
399
-
400
- this.data.forEach(row => {
401
- row.selected = selectedIds.has(row[idField]);
402
- });
403
- }
404
-
405
- // --- Actions ---
406
-
407
- onPageChange(page: number): void {
408
- this.currentPage = page;
409
- if (this.tableData !== undefined) {
410
- this.pageChange.emit(this.buildChangeEvent());
411
- } else {
412
- this.loadData();
413
- }
414
- }
415
-
416
- onPageSizeChange(size: number): void {
417
- if (this.config.pagination) {
418
- this.config.pagination.pageSize = size;
419
- this.currentPage = 1; // Reset to first page
420
- if (this.tableData !== undefined) {
421
- this.pageChange.emit(this.buildChangeEvent());
422
- } else {
423
- this.loadData();
424
- }
425
- }
426
- }
427
-
428
- onSort(col: TableColumn): void {
429
- if (!col.sortable) return;
430
-
431
- if (this.activeSort.key === col.key) {
432
- this.activeSort.direction = this.activeSort.direction === 'ASC' ? 'DESC' : 'ASC';
433
- } else {
434
- this.activeSort.key = col.key;
435
- this.activeSort.direction = 'ASC';
436
- }
437
- this.isSortActive = true; // User has actively sorted, include orderBy in API call
438
-
439
- if (this.tableData !== undefined) {
440
- // External-data mode: let the parent handle the API call
441
- this.sortChange.emit(this.buildChangeEvent());
442
- } else {
443
- this.loadData();
444
- }
445
- }
446
-
447
- onSearch(event: Event): void {
448
- const value = (event.target as HTMLInputElement).value;
449
- this.searchSubject.next(value);
450
- }
451
-
452
- onFilterChange(key: string, event: Event): void {
453
- const value = (event.target as HTMLSelectElement).value;
454
- this.applyFilter(key, value);
455
- }
456
-
457
- private applyFilter(key: string, value: any): void {
458
- this.activeFilters[key] = value;
459
- this.currentPage = 1;
460
- this.filterChange.emit({ key, value });
461
- if (this.tableData === undefined) {
462
- this.loadData();
463
- }
464
- }
465
-
466
- toggleFilter(key: string, event: Event): void {
467
- event.stopPropagation();
468
- if (this.openFilterKey === key) {
469
- this.openFilterKey = null;
470
- this.activeFilterData = null;
471
- return;
472
- }
473
- const trigger = event.currentTarget as HTMLElement;
474
- const rect = trigger.getBoundingClientRect();
475
- this.filterPosition = { top: rect.bottom + 6, left: rect.left };
476
- this.openFilterKey = key;
477
- this.activeFilterData = this.config.filters?.find(f => f.key === key) || null;
478
- }
479
-
480
- selectFilterOption(filter: any, opt: { label: string; value: any } | null): void {
481
- const value = opt ? opt.value : '';
482
- this.activeFilterLabels[filter.key] = opt ? opt.label : '';
483
- this.applyFilter(filter.key, value);
484
- this.openFilterKey = null;
485
- }
486
-
487
- getFilterDisplay(filter: any): string {
488
- return this.activeFilterLabels[filter.key] || filter.label;
489
- }
490
-
491
- getValidFilterOptions(filter: any): any[] {
492
- return (filter?.options || []).filter((opt: any) => opt.label != null && opt.label !== '');
493
- }
494
-
495
- isFilterActive(filter: any): boolean {
496
- return !!this.activeFilterLabels[filter.key];
497
- }
498
-
499
- // --- Private helpers ---
500
-
501
- /**
502
- * Assembles the current table state into a `TableDataChangeEvent` object.
503
- * Emitted to the parent in external-data mode so it can fetch and supply new data.
504
- */
505
- private buildChangeEvent(): TableDataChangeEvent {
506
- return {
507
- page: this.currentPage,
508
- pageSize: this.config.pagination?.pageSize || 10,
509
- sortBy: this.activeSort.key || undefined,
510
- orderBy: this.isSortActive ? this.activeSort.direction : undefined,
511
- searchTerm: this.searchTerm || undefined,
512
- filters: { ...this.activeFilters }
513
- };
514
- }
515
-
516
- onAction(action: TableAction, row: any): void {
517
- if (action.type === 'edit-row' as any) {
518
- const idField = (this.config as any).rowIdField || 'id';
519
- this.originalRowsCache.set(row[idField], JSON.parse(JSON.stringify(row)));
520
- this.prepareDateFieldsForEdit(row);
521
- row.isEditing = true;
522
- this.action.emit({ action, row });
523
- return;
524
- }
525
-
526
- if (action.type === 'callback' && action.callback) {
527
- action.callback(row);
528
- }
529
-
530
- if (action.type === 'route' && action.route) {
531
- const url = this.replaceParams(action.route, row);
532
- this.router.navigateByUrl(url);
533
- return;
534
- }
535
-
536
- if (action.type === 'api') {
537
- if (action.confirmationNeeded) {
538
- const message = action.confirmationMessage || this.config.labels?.defaultConfirmationMessage || 'Are you sure?';
539
- if (!confirm(message)) return;
540
- }
541
- if (action.apiUrl) {
542
- this.executeApiAction(action, row);
543
- } else {
544
- this.action.emit({ action, row });
545
- }
546
- } else {
547
- this.action.emit({ action, row });
548
- }
549
- }
550
-
551
- onActionItemClick(item: any, row: any, event: Event): void {
552
- event.stopPropagation();
553
-
554
- if (item.type === 'edit-row') {
555
- const idField = (this.config as any).rowIdField || 'id';
556
- this.originalRowsCache.set(row[idField], JSON.parse(JSON.stringify(row)));
557
- this.prepareDateFieldsForEdit(row);
558
- row.isEditing = true;
559
- this.action.emit({ action: item as TableAction, row });
560
- this.closeDropdown();
561
- return;
562
- }
563
-
564
- if (item.type === 'delete') {
565
- this.openDeleteConfirmModal(item, row);
566
- return;
567
- }
568
-
569
- if (item.type === 'callback' && item.callback) {
570
- item.callback(row);
571
- }
572
-
573
- if (item.type === 'route' && item.route) {
574
- const url = this.replaceParams(item.route, row);
575
- this.router.navigateByUrl(url);
576
- return;
577
- }
578
-
579
- if (item.type === 'api') {
580
- if (item.confirmationNeeded) {
581
- const message = item.confirmationMessage || this.config.labels?.defaultConfirmationMessage || 'Are you sure?';
582
- if (!confirm(message)) return;
583
- }
584
- if (item.apiUrl) {
585
- this.executeApiAction(item as TableAction, row);
586
- } else {
587
- this.action.emit({ action: item as TableAction, row });
588
- }
589
- } else {
590
- this.action.emit({ action: item as TableAction, row });
591
- }
592
- }
593
-
594
- private openDeleteConfirmModal(item: any, row: any): void {
595
- const cfg = item.deleteConfig || {};
596
- this.deleteModalConfig = {
597
- title: cfg.modalTitle || 'Confirm Delete',
598
- size: 'sm' as const,
599
- headerTheme: 'dark' as const,
600
- confirmButton: {
601
- label: cfg.confirmLabel || 'Delete',
602
- type: 'danger' as const
603
- },
604
- cancelButton: {
605
- label: cfg.cancelLabel || 'Cancel',
606
- show: true
607
- }
608
- };
609
- this.deleteModalMessage = cfg.modalMessage || 'Are you sure you want to delete this item?';
610
- this.pendingDeleteAction = { item, row };
611
- this.deleteModalOpen = true;
612
- }
613
-
614
- deleteModalMessage = '';
615
-
616
- onDeleteConfirm(): void {
617
- this.deleteModalOpen = false;
618
- if (!this.pendingDeleteAction) return;
619
- const { item, row } = this.pendingDeleteAction;
620
- const cfg = item.deleteConfig || {};
621
- const idField = cfg.idField || 'id';
622
- const url = cfg.apiUrl.replace(`:${idField}`, row[idField]);
623
- this.pendingDeleteAction = null;
624
-
625
- this.loading = true;
626
- this.http.delete(url, { headers: this.getHeaders() }).pipe(
627
- finalize(() => this.loading = false)
628
- ).subscribe({
629
- next: () => {
630
- if (this.data.length === 1 && this.currentPage > 1) {
631
- this.currentPage--;
632
- }
633
- this.loadData();
634
- this.action.emit({ action: item as TableAction, row });
635
- },
636
- error: (err) => console.error('[SmartTable] Delete API Error', err)
637
- });
638
- }
639
-
640
- onDeleteCancel(): void {
641
- this.deleteModalOpen = false;
642
- this.pendingDeleteAction = null;
643
- }
644
-
645
- onTopAction(action: TableAction): void {
646
- if (action.type === 'add-row' as any) {
647
- const idField = (this.config as any).rowIdField || 'id';
648
- const newRow: any = {
649
- [idField]: 0,
650
- isNew: true,
651
- isEditing: true
652
- };
653
- if (this.config.columns) {
654
- this.config.columns.forEach(col => {
655
- if (col.editConfig && col.editConfig.defaultValue !== undefined) {
656
- newRow[col.key] = col.dataType === 'date'
657
- ? this.toDateObject(col.editConfig.defaultValue)
658
- : col.editConfig.defaultValue;
659
- } else {
660
- newRow[col.key] = col.dataType === 'date' ? null : '';
661
- }
662
- if (col.subFields) {
663
- col.subFields.forEach(sub => {
664
- if (sub.editConfig && sub.editConfig.defaultValue !== undefined) {
665
- newRow[sub.key] = sub.dataType === 'date'
666
- ? this.toDateObject(sub.editConfig.defaultValue)
667
- : sub.editConfig.defaultValue;
668
- } else {
669
- newRow[sub.key] = sub.dataType === 'date' ? null : '';
670
- }
671
- });
672
- }
673
- });
674
- }
675
- this.data = [newRow, ...this.data];
676
- setTimeout(() => this.calculateStickyPositions());
677
- this.topAction.emit(action);
678
- return;
679
- }
680
-
681
- if (action.type === 'callback' && action.callback) {
682
- action.callback(null); // No row for top action
683
- }
684
-
685
- if (action.type === 'route' && action.route) {
686
- // Since it's a top action, replaceParams with an empty object or handle statically
687
- const url = this.replaceParams(action.route, {});
688
- this.router.navigateByUrl(url);
689
- return;
690
- }
691
-
692
- if (action.type === 'api') {
693
- if (action.confirmationNeeded) {
694
- const message = action.confirmationMessage || this.config.labels?.defaultConfirmationMessage || 'Are you sure?';
695
- if (!confirm(message)) return;
696
- }
697
- if (action.apiUrl) {
698
- this.executeApiAction(action, null);
699
- } else {
700
- this.topAction.emit(action);
701
- }
702
- } else {
703
- this.topAction.emit(action);
704
- }
705
- }
706
-
707
- private executeApiAction(action: TableAction, row: any): void {
708
- if (!action.apiUrl) return;
709
-
710
- const url = row ? this.replaceParams(action.apiUrl, row) : action.apiUrl;
711
- const method = action.apiMethod || 'POST';
712
- const headers = this.getHeaders();
713
- const body = row ? row : {};
714
-
715
- this.loading = true;
716
- this.http.request(method, url, { body, headers }).pipe(
717
- finalize(() => this.loading = false)
718
- ).subscribe({
719
- next: () => {
720
- this.loadData(); // reload on success
721
- if (row) {
722
- this.action.emit({ action, row });
723
- } else {
724
- this.topAction.emit(action);
725
- }
726
- },
727
- error: (err) => {
728
- console.error('API Action Error', err);
729
- }
730
- });
731
- }
732
-
733
- // --- Selection ---
734
-
735
- onSelectAll(event: Event): void {
736
- const checked = (event.target as HTMLInputElement).checked;
737
- this.data.forEach(row => row.selected = checked);
738
- this.updateSelectedRows();
739
- }
740
-
741
- onRowSelect(row: any): void {
742
- this.updateSelectedRows();
743
- }
744
-
745
- updateSelectedRows(): void {
746
- const idField = (this.config as any).rowIdField || 'id';
747
-
748
- // 1. Identify rows from the global selection that are NOT on the current page
749
- const currentPageIds = new Set(this.data.map(r => r[idField]));
750
- const otherPagesSelected = (this.selectedRows || []).filter(r => !currentPageIds.has(r[idField]));
751
-
752
- // 2. Identify currently selected rows on the current page
753
- const currentPageSelected = this.data.filter(row => row.selected);
754
-
755
- // 3. Merge them to form the new global selection
756
- this.selectedRows = [...otherPagesSelected, ...currentPageSelected];
757
-
758
- this.rowSelect.emit(this.selectedRows);
759
- }
760
-
761
- // --- Helpers ---
762
-
763
- getCellValue(row: any, col: TableColumn): any {
764
- // Support nested properties via labelPath or key
765
- const path = col.labelPath || col.key;
766
- let val = this.getValueByPath(row, path);
767
-
768
- // Formatting (Date, etc.)
769
- if (col.type === 'date' && val) {
770
- if (col.dateFormat) {
771
- try {
772
- return formatDate(val, col.dateFormat, this.locale);
773
- } catch (e) {
774
- console.warn('Invalid date format or value', val, col.dateFormat);
775
- return val;
776
- }
777
- }
778
- return new Date(val).toLocaleDateString();
779
- }
780
-
781
- if (val === null || val === undefined || val === '') {
782
- return col.emptyValue ?? this.config.emptyValue ?? '-';
783
- }
784
- return val;
785
- }
786
-
787
- getBadgeClass(row: any, col: TableColumn): string {
788
- const val = this.getCellValue(row, col);
789
- const strVal = String(val);
790
-
791
- // Config approach
792
- if (col.badgeConfig && col.badgeConfig[strVal]) {
793
- return `badge-${col.badgeConfig[strVal]}`;
794
- }
795
-
796
- // Default Logic
797
- const status = strVal.toLowerCase();
798
-
799
- if (['active', 'completed', 'success', 'approved'].includes(status)) return 'badge-success';
800
- if (['pending', 'in progress', 'waiting'].includes(status)) return 'badge-warning';
801
- if (['rejected', 'failed', 'error', 'deleted'].includes(status)) return 'badge-danger';
802
- if (['draft', 'inactive'].includes(status)) return 'badge-neutral';
803
-
804
- return 'badge-info'; // default
805
- }
806
-
807
- getAscOpacity(key: string): number {
808
- if (this.activeSort.key !== key) return 0.45;
809
- return this.activeSort.direction === 'ASC' ? 1 : 0.2;
810
- }
811
-
812
- getDescOpacity(key: string): number {
813
- if (this.activeSort.key !== key) return 0.45;
814
- return this.activeSort.direction === 'DESC' ? 1 : 0.2;
815
- }
816
-
817
- private replaceParams(template: string, row: any): string {
818
- return template.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => row[key] || '');
819
- }
820
-
821
- private loadFilterOptions(): void {
822
- if (!this.config.filters) return;
823
-
824
- this.config.filters.forEach(filter => {
825
- if (filter.apiUrl && !filter.options) {
826
- const headers = this.getHeaders();
827
- let params = new HttpParams();
828
-
829
- if (filter.handling === 'nested_string' && filter.nestedStringConfig) {
830
- const { paramName, baseValue } = filter.nestedStringConfig;
831
- if (baseValue) {
832
- params = params.set(paramName, baseValue);
833
- }
834
- }
835
-
836
- const method = filter.apiMethod || 'GET';
837
- const body = filter.apiPayload || {};
838
-
839
- const request$ = method === 'POST'
840
- ? this.http.post<any>(filter.apiUrl, body, { headers, params })
841
- : this.http.get<any>(filter.apiUrl, { headers, params });
842
-
843
- request$.subscribe({
844
- next: (response) => {
845
- const data = filter.dataPath ? this.getValueByPath(response, filter.dataPath) : response;
846
- if (!Array.isArray(data)) {
847
- console.error(`Filter data for ${filter.key} is not an array`, data);
848
- return;
849
- }
850
- filter.options = data.map((item: any) => {
851
- const label = filter.labelPath ? this.getValueByPath(item, filter.labelPath) :
852
- (filter.labelKey ? item[filter.labelKey] : item.label || item.name);
853
- const value = filter.valuePath ? this.getValueByPath(item, filter.valuePath) :
854
- (filter.valueKey ? item[filter.valueKey] : item.value || item.code);
855
- return { label, value };
856
- });
857
-
858
- // Auto-populate matching column options
859
- if (this.config.columns) {
860
- const matchingColumn = this.config.columns.find(col => col.key === filter.key);
861
- if (matchingColumn && matchingColumn.dataType === 'select' && !matchingColumn.options) {
862
- matchingColumn.options = filter.options;
863
- }
864
- }
865
- },
866
- error: (err) => console.error(`Failed to load filter options for ${filter.key}:`, err)
867
- });
868
- }
869
- });
870
- }
871
-
872
- private getValueByPath(obj: any, path: string): any {
873
- if (!path || path === '') return obj;
874
- return path.split('.').reduce((acc, part) => {
875
- const match = part.match(/(\w+)\[(\d+)\]/);
876
- if (match) {
877
- return acc?.[match[1]]?.[parseInt(match[2])];
878
- }
879
- return acc?.[part];
880
- }, obj);
881
- }
882
-
883
- private calculateStickyPositions(): void {
884
- // We calculate positions based on rendered widths
885
- if (!this.stickyHeaders || this.stickyHeaders.length === 0) return;
886
-
887
- this.stickyColumnStyles = {};
888
- let leftOffset = 0;
889
- this.hasStickyColumns = false;
890
-
891
- // Default: only the first data column is sticky
892
- const stickyCount = this.config.stickyColumnCount !== undefined ? this.config.stickyColumnCount : 1;
893
-
894
- // Checkbox width handling
895
- if (this.config.selectable) {
896
- const firstSticky = this.config.columns.find((c, i) => i < stickyCount || c.sticky);
897
- if (firstSticky) {
898
- // We can try to measure checkbox col if needed, but usually fixed 40px
899
- // Or better, if we have a checkbox col ref, assume 40px for now as it is fixed in CSS
900
- leftOffset = 40;
901
- }
902
- }
903
-
904
- const headerElements = this.stickyHeaders.toArray();
905
-
906
- this.config.columns.forEach((col, index) => {
907
- if (col.sticky || index < stickyCount) {
908
- col.sticky = true;
909
- this.hasStickyColumns = false; // Reset to true only after we set styles? No, wait.
910
- // Actually property is used in template to add class 'sticky-col'
911
- // but we need to update Styles based on previous cols widths
912
-
913
- // Find corresponding header element
914
- // Note: headerElements corresponds to columns indices
915
- const headerEl = headerElements[index]?.nativeElement;
916
-
917
- if (headerEl) {
918
- // Set style for current column
919
- this.stickyColumnStyles[col.key] = {
920
- left: `${leftOffset}px`
921
- // We DO NOT set width here, allowing it to be dynamic/auto
922
- };
923
-
924
- // Add THIS column's width to offset for the NEXT column
925
- // use getBoundingClientRect or offsetWidth
926
- leftOffset += headerEl.offsetWidth;
927
- }
928
- this.hasStickyColumns = true;
929
- }
930
- });
931
-
932
- this.cdr.detectChanges();
933
- }
934
-
935
- private toTitleCase(str: string): string {
936
- return str
937
- .replace(/([A-Z])/g, ' $1') // insert space before capital letters
938
- .replace(/^./, (str) => str.toUpperCase()) // capitalize the first letter
939
- .trim(); // remove any leading/trailing whitespace
940
- }
941
-
942
- get columnCount(): number {
943
- return this.config.columns.length;
944
- }
945
-
946
- /** Filters out actions/action-items explicitly disabled via `isEnabled: false`. */
947
- getVisibleActions<T extends { isEnabled?: boolean }>(actions?: T[] | null): T[] {
948
- return (actions || []).filter(a => a.isEnabled !== false);
949
- }
950
-
951
- get showPagination(): boolean {
952
- if (!this.config?.pagination?.enabled) return false;
953
- const pageSize = this.config.pagination.pageSize;
954
- const effectiveTotal = this.totalItems > 0 ? this.totalItems : this.data.length;
955
- return effectiveTotal > 10;
956
- }
957
-
958
- private applyPaginationDefaults(): void {
959
- if (this.config?.pagination?.enabled) {
960
- if (!this.config.pagination.pageSize) {
961
- this.config.pagination.pageSize = 10;
962
- }
963
- if (!this.config.pagination.pageSizeOptions || this.config.pagination.pageSizeOptions.length === 0) {
964
- this.config.pagination.pageSizeOptions = [5, 10, 20, 50, 100];
965
- }
966
- }
967
- }
968
-
969
- onColumnClick(row: any, col: TableColumn): void {
970
- if (col.clickAction === 'callback') {
971
- this.columnClick.emit({ row, column: col.key });
972
- } else if (col.clickAction === 'route' && col.clickRoute) {
973
- const url = this.replaceParams(col.clickRoute, row);
974
- this.router.navigateByUrl(url);
975
- }
976
- }
977
-
978
- private getHeaders(): HttpHeaders {
979
- let headers = new HttpHeaders();
980
- if (this.config.token) {
981
- const headerName = this.config.tokenHeader || 'Authorization';
982
- headers = headers.set(headerName, this.config.token);
983
- }
984
- return headers;
985
- }
986
-
987
- toggleDropdown(id: string, event: Event, items: any[], row: any): void {
988
- event.stopPropagation();
989
- if (this.openDropdownId === id) {
990
- this.openDropdownId = null;
991
- this.activeDropdownItems = null;
992
- this.activeDropdownRow = null;
993
- return;
994
- }
995
- const btn = event.currentTarget as HTMLElement;
996
- const rect = btn.getBoundingClientRect();
997
- const estimatedHeight = (items?.length || 1) * 44 + 8;
998
- const spaceBelow = window.innerHeight - rect.bottom;
999
- const top = spaceBelow < estimatedHeight + 4
1000
- ? Math.max(4, rect.top - estimatedHeight - 4)
1001
- : rect.bottom + 4;
1002
- this.dropdownPosition = {
1003
- top,
1004
- right: window.innerWidth - rect.right
1005
- };
1006
- this.openDropdownId = id;
1007
- this.activeDropdownItems = items || [];
1008
- this.activeDropdownRow = row;
1009
- }
1010
-
1011
- @HostListener('document:click')
1012
- closeDropdown(): void {
1013
- this.openDropdownId = null;
1014
- this.activeDropdownItems = null;
1015
- this.activeDropdownRow = null;
1016
- this.openFilterKey = null;
1017
- this.activeFilterData = null;
1018
- }
1019
-
1020
- onCancelRow(row: any, index: number): void {
1021
- if (row.isNew) {
1022
- this.data.splice(index, 1);
1023
- } else {
1024
- const idField = (this.config as any).rowIdField || 'id';
1025
- const original = this.originalRowsCache.get(row[idField]);
1026
- if (original) {
1027
- Object.assign(row, original);
1028
- this.originalRowsCache.delete(row[idField]);
1029
- }
1030
- row.isEditing = false;
1031
- }
1032
- }
1033
-
1034
- onSaveRow(row: any): void {
1035
- const { isNew, isEditing, selected, ...payload } = row;
1036
- this.normalizeDateFieldsForSave(payload);
1037
- const idField = (this.config as any).rowIdField || 'id';
1038
-
1039
- if (this.tableData !== undefined) {
1040
- this.rowSave.emit({ row: payload, isNew: !!isNew });
1041
- row.isEditing = false;
1042
- row.isNew = false;
1043
- } else {
1044
- this.loading = true;
1045
- const headers = this.getHeaders();
1046
-
1047
- if (isNew) {
1048
- const url = this.config.apiUrl || '';
1049
- this.http.post(url, payload, { headers }).pipe(
1050
- finalize(() => this.loading = false)
1051
- ).subscribe({
1052
- next: () => {
1053
- row.isEditing = false;
1054
- row.isNew = false;
1055
- this.loadData();
1056
- },
1057
- error: (err) => {
1058
- console.error('[SmartTable] Save Row Error', err);
1059
- }
1060
- });
1061
- } else {
1062
- let url = this.config.apiUrl || '';
1063
- if (url) {
1064
- if (url.includes(':id') || url.includes(':' + idField)) {
1065
- url = this.replaceParams(url, row);
1066
- } else {
1067
- url = url.endsWith('/') ? `${url}${row[idField]}` : `${url}/${row[idField]}`;
1068
- }
1069
- }
1070
- const method = this.config.apiMethod === 'POST' ? 'POST' : 'PUT';
1071
- this.http.request(method, url, { body: payload, headers }).pipe(
1072
- finalize(() => this.loading = false)
1073
- ).subscribe({
1074
- next: () => {
1075
- row.isEditing = false;
1076
- this.originalRowsCache.delete(row[idField]);
1077
- this.loadData();
1078
- },
1079
- error: (err) => {
1080
- console.error('[SmartTable] Update Row Error', err);
1081
- }
1082
- });
1083
- }
1084
- }
1085
- }
1086
-
1087
- // ── Date field helpers ─────────────────────────────────────────────────
1088
-
1089
- /** Convert any parseable date value to a native Date object for mat-datepicker binding. */
1090
- private toDateObject(val: any): Date | null {
1091
- if (!val) return null;
1092
- if (val instanceof Date) return val;
1093
- const d = new Date(val);
1094
- return isNaN(d.getTime()) ? null : d;
1095
- }
1096
-
1097
- /** Pre-process all date columns on a row before entering edit mode. */
1098
- private prepareDateFieldsForEdit(row: any): void {
1099
- this.config.columns.forEach(col => {
1100
- if (col.dataType === 'date' && row[col.key]) {
1101
- row[col.key] = this.toDateObject(row[col.key]);
1102
- }
1103
- if (col.subFields) {
1104
- col.subFields.forEach(sub => {
1105
- if (sub.dataType === 'date' && row[sub.key]) {
1106
- row[sub.key] = this.toDateObject(row[sub.key]);
1107
- }
1108
- });
1109
- }
1110
- });
1111
- }
1112
-
1113
- /** Normalize Date objects back to ISO strings before sending payloads. */
1114
- private normalizeDateFieldsForSave(payload: any): void {
1115
- this.config.columns.forEach(col => {
1116
- if (col.dataType === 'date' && payload[col.key] instanceof Date) {
1117
- payload[col.key] = payload[col.key].toISOString();
1118
- }
1119
- if (col.subFields) {
1120
- col.subFields.forEach(sub => {
1121
- if (sub.dataType === 'date' && payload[sub.key] instanceof Date) {
1122
- payload[sub.key] = payload[sub.key].toISOString();
1123
- }
1124
- });
1125
- }
1126
- });
1127
- }
1128
- }