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