@memberjunction/ng-entity-viewer 0.0.1 → 2.122.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/README.md +224 -43
- package/dist/lib/entity-cards/entity-cards.component.d.ts +163 -0
- package/dist/lib/entity-cards/entity-cards.component.d.ts.map +1 -0
- package/dist/lib/entity-cards/entity-cards.component.js +797 -0
- package/dist/lib/entity-cards/entity-cards.component.js.map +1 -0
- package/dist/lib/entity-grid/entity-grid.component.d.ts +216 -0
- package/dist/lib/entity-grid/entity-grid.component.d.ts.map +1 -0
- package/dist/lib/entity-grid/entity-grid.component.js +676 -0
- package/dist/lib/entity-grid/entity-grid.component.js.map +1 -0
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +182 -0
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -0
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +787 -0
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -0
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts +252 -0
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -0
- package/dist/lib/entity-viewer/entity-viewer.component.js +883 -0
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -0
- package/dist/lib/pagination/pagination.component.d.ts +60 -0
- package/dist/lib/pagination/pagination.component.d.ts.map +1 -0
- package/dist/lib/pagination/pagination.component.js +199 -0
- package/dist/lib/pagination/pagination.component.js.map +1 -0
- package/dist/lib/pill/pill.component.d.ts +58 -0
- package/dist/lib/pill/pill.component.d.ts.map +1 -0
- package/dist/lib/pill/pill.component.js +125 -0
- package/dist/lib/pill/pill.component.js.map +1 -0
- package/dist/lib/types.d.ts +316 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +30 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils/highlight.util.d.ts +69 -0
- package/dist/lib/utils/highlight.util.d.ts.map +1 -0
- package/dist/lib/utils/highlight.util.js +214 -0
- package/dist/lib/utils/highlight.util.js.map +1 -0
- package/dist/module.d.ts +38 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +83 -0
- package/dist/module.js.map +1 -0
- package/dist/public-api.d.ts +16 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +20 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +45 -6
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
|
4
|
+
import { RunView } from '@memberjunction/core';
|
|
5
|
+
import { DEFAULT_VIEWER_CONFIG } from '../types';
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
import * as i1 from "@memberjunction/ng-shared-generic";
|
|
8
|
+
import * as i2 from "../entity-grid/entity-grid.component";
|
|
9
|
+
import * as i3 from "../entity-cards/entity-cards.component";
|
|
10
|
+
import * as i4 from "../pagination/pagination.component";
|
|
11
|
+
import * as i5 from "@angular/common";
|
|
12
|
+
function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
13
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
14
|
+
i0.ɵɵelementStart(0, "button", 12);
|
|
15
|
+
i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.clearFilter()); });
|
|
16
|
+
i0.ɵɵelement(1, "i", 13);
|
|
17
|
+
i0.ɵɵelementEnd();
|
|
18
|
+
} }
|
|
19
|
+
function EntityViewerComponent_Conditional_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
20
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
21
|
+
i0.ɵɵelementStart(0, "div", 6);
|
|
22
|
+
i0.ɵɵelement(1, "i", 9);
|
|
23
|
+
i0.ɵɵelementStart(2, "input", 10);
|
|
24
|
+
i0.ɵɵlistener("input", function EntityViewerComponent_Conditional_1_Conditional_1_Template_input_input_2_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onFilterChange($event.target.value)); });
|
|
25
|
+
i0.ɵɵelementEnd();
|
|
26
|
+
i0.ɵɵtemplate(3, EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button", 11);
|
|
27
|
+
i0.ɵɵelementEnd();
|
|
28
|
+
} if (rf & 2) {
|
|
29
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
30
|
+
i0.ɵɵadvance(2);
|
|
31
|
+
i0.ɵɵproperty("placeholder", ctx_r1.effectiveConfig.filterPlaceholder)("value", ctx_r1.effectiveFilterText);
|
|
32
|
+
i0.ɵɵadvance();
|
|
33
|
+
i0.ɵɵconditional(ctx_r1.effectiveFilterText ? 3 : -1);
|
|
34
|
+
} }
|
|
35
|
+
function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
36
|
+
i0.ɵɵelementStart(0, "span");
|
|
37
|
+
i0.ɵɵtext(1);
|
|
38
|
+
i0.ɵɵpipe(2, "number");
|
|
39
|
+
i0.ɵɵpipe(3, "number");
|
|
40
|
+
i0.ɵɵelementEnd();
|
|
41
|
+
} if (rf & 2) {
|
|
42
|
+
const ctx_r1 = i0.ɵɵnextContext(3);
|
|
43
|
+
i0.ɵɵadvance();
|
|
44
|
+
i0.ɵɵtextInterpolate2("", i0.ɵɵpipeBind1(2, 2, ctx_r1.filteredRecordCount), " of ", i0.ɵɵpipeBind1(3, 4, ctx_r1.totalRecordCount), " records");
|
|
45
|
+
} }
|
|
46
|
+
function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
47
|
+
i0.ɵɵelementStart(0, "span");
|
|
48
|
+
i0.ɵɵtext(1);
|
|
49
|
+
i0.ɵɵpipe(2, "number");
|
|
50
|
+
i0.ɵɵelementEnd();
|
|
51
|
+
} if (rf & 2) {
|
|
52
|
+
const ctx_r1 = i0.ɵɵnextContext(3);
|
|
53
|
+
i0.ɵɵadvance();
|
|
54
|
+
i0.ɵɵtextInterpolate1("", i0.ɵɵpipeBind1(2, 1, ctx_r1.totalRecordCount), " records");
|
|
55
|
+
} }
|
|
56
|
+
function EntityViewerComponent_Conditional_1_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
57
|
+
i0.ɵɵelementStart(0, "div", 7);
|
|
58
|
+
i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Conditional_2_Conditional_1_Template, 4, 6, "span")(2, EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Template, 3, 3, "span");
|
|
59
|
+
i0.ɵɵelementEnd();
|
|
60
|
+
} if (rf & 2) {
|
|
61
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
62
|
+
i0.ɵɵadvance();
|
|
63
|
+
i0.ɵɵconditional(ctx_r1.filteredRecordCount !== ctx_r1.totalRecordCount ? 1 : 2);
|
|
64
|
+
} }
|
|
65
|
+
function EntityViewerComponent_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
66
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
67
|
+
i0.ɵɵelementStart(0, "div", 8)(1, "button", 14);
|
|
68
|
+
i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.setViewMode("grid")); });
|
|
69
|
+
i0.ɵɵelement(2, "i", 15);
|
|
70
|
+
i0.ɵɵelementEnd();
|
|
71
|
+
i0.ɵɵelementStart(3, "button", 16);
|
|
72
|
+
i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.setViewMode("cards")); });
|
|
73
|
+
i0.ɵɵelement(4, "i", 17);
|
|
74
|
+
i0.ɵɵelementEnd()();
|
|
75
|
+
} if (rf & 2) {
|
|
76
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
77
|
+
i0.ɵɵadvance();
|
|
78
|
+
i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "grid");
|
|
79
|
+
i0.ɵɵadvance(2);
|
|
80
|
+
i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "cards");
|
|
81
|
+
} }
|
|
82
|
+
function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
83
|
+
i0.ɵɵelementStart(0, "div", 1);
|
|
84
|
+
i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div", 6)(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div", 7)(3, EntityViewerComponent_Conditional_1_Conditional_3_Template, 5, 4, "div", 8);
|
|
85
|
+
i0.ɵɵelementEnd();
|
|
86
|
+
} if (rf & 2) {
|
|
87
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
88
|
+
i0.ɵɵadvance();
|
|
89
|
+
i0.ɵɵconditional(ctx_r1.effectiveConfig.showFilter ? 1 : -1);
|
|
90
|
+
i0.ɵɵadvance();
|
|
91
|
+
i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.entity ? 2 : -1);
|
|
92
|
+
i0.ɵɵadvance();
|
|
93
|
+
i0.ɵɵconditional(ctx_r1.effectiveConfig.showViewModeToggle ? 3 : -1);
|
|
94
|
+
} }
|
|
95
|
+
function EntityViewerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
96
|
+
i0.ɵɵelementStart(0, "div", 3);
|
|
97
|
+
i0.ɵɵelement(1, "mj-loading", 18);
|
|
98
|
+
i0.ɵɵelementEnd();
|
|
99
|
+
} if (rf & 2) {
|
|
100
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
101
|
+
i0.ɵɵadvance();
|
|
102
|
+
i0.ɵɵproperty("text", ctx_r1.loadingMessage);
|
|
103
|
+
} }
|
|
104
|
+
function EntityViewerComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
105
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
106
|
+
i0.ɵɵelement(1, "i", 19);
|
|
107
|
+
i0.ɵɵelementStart(2, "p");
|
|
108
|
+
i0.ɵɵtext(3, "Select an entity to view records");
|
|
109
|
+
i0.ɵɵelementEnd()();
|
|
110
|
+
} }
|
|
111
|
+
function EntityViewerComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
112
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
113
|
+
i0.ɵɵelement(1, "i", 20);
|
|
114
|
+
i0.ɵɵelementStart(2, "p");
|
|
115
|
+
i0.ɵɵtext(3);
|
|
116
|
+
i0.ɵɵelementEnd()();
|
|
117
|
+
} if (rf & 2) {
|
|
118
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
119
|
+
i0.ɵɵadvance(3);
|
|
120
|
+
i0.ɵɵtextInterpolate(ctx_r1.debouncedFilterText ? "No matching records" : "No records found");
|
|
121
|
+
} }
|
|
122
|
+
function EntityViewerComponent_Conditional_6_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
123
|
+
const _r5 = i0.ɵɵgetCurrentView();
|
|
124
|
+
i0.ɵɵelementStart(0, "mj-entity-grid", 23);
|
|
125
|
+
i0.ɵɵlistener("recordSelected", function EntityViewerComponent_Conditional_6_Conditional_0_Template_mj_entity_grid_recordSelected_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onRecordSelected($event)); })("recordOpened", function EntityViewerComponent_Conditional_6_Conditional_0_Template_mj_entity_grid_recordOpened_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onRecordOpened($event)); })("sortChanged", function EntityViewerComponent_Conditional_6_Conditional_0_Template_mj_entity_grid_sortChanged_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onSortChanged($event)); })("gridStateChanged", function EntityViewerComponent_Conditional_6_Conditional_0_Template_mj_entity_grid_gridStateChanged_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onGridStateChanged($event)); });
|
|
126
|
+
i0.ɵɵelementEnd();
|
|
127
|
+
} if (rf & 2) {
|
|
128
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
129
|
+
i0.ɵɵproperty("entity", ctx_r1.entity)("records", ctx_r1.filteredRecords)("selectedRecordId", ctx_r1.selectedRecordId)("columns", ctx_r1.gridColumns)("height", "100%")("filterText", ctx_r1.debouncedFilterText)("sortState", ctx_r1.effectiveSortState)("serverSideSorting", ctx_r1.effectiveConfig.serverSideSorting)("gridState", ctx_r1.gridState);
|
|
130
|
+
} }
|
|
131
|
+
function EntityViewerComponent_Conditional_6_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
132
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
133
|
+
i0.ɵɵelementStart(0, "mj-entity-cards", 24);
|
|
134
|
+
i0.ɵɵlistener("recordSelected", function EntityViewerComponent_Conditional_6_Conditional_1_Template_mj_entity_cards_recordSelected_0_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onRecordSelected($event)); })("recordOpened", function EntityViewerComponent_Conditional_6_Conditional_1_Template_mj_entity_cards_recordOpened_0_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onRecordOpened($event)); });
|
|
135
|
+
i0.ɵɵelementEnd();
|
|
136
|
+
} if (rf & 2) {
|
|
137
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
138
|
+
i0.ɵɵproperty("entity", ctx_r1.entity)("records", ctx_r1.filteredRecords)("selectedRecordId", ctx_r1.selectedRecordId)("cardTemplate", ctx_r1.cardTemplate)("hiddenFieldMatches", ctx_r1.hiddenFieldMatches)("filterText", ctx_r1.debouncedFilterText);
|
|
139
|
+
} }
|
|
140
|
+
function EntityViewerComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
141
|
+
i0.ɵɵtemplate(0, EntityViewerComponent_Conditional_6_Conditional_0_Template, 1, 9, "mj-entity-grid", 21)(1, EntityViewerComponent_Conditional_6_Conditional_1_Template, 1, 6, "mj-entity-cards", 22);
|
|
142
|
+
} if (rf & 2) {
|
|
143
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
144
|
+
i0.ɵɵconditional(ctx_r1.effectiveViewMode === "grid" ? 0 : 1);
|
|
145
|
+
} }
|
|
146
|
+
function EntityViewerComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
147
|
+
const _r7 = i0.ɵɵgetCurrentView();
|
|
148
|
+
i0.ɵɵelementStart(0, "mj-pagination", 25);
|
|
149
|
+
i0.ɵɵlistener("loadMore", function EntityViewerComponent_Conditional_7_Template_mj_pagination_loadMore_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onLoadMore()); });
|
|
150
|
+
i0.ɵɵelementEnd();
|
|
151
|
+
} if (rf & 2) {
|
|
152
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
153
|
+
i0.ɵɵproperty("pagination", ctx_r1.pagination)("loadedRecordCount", ctx_r1.filteredRecords.length);
|
|
154
|
+
} }
|
|
155
|
+
/**
|
|
156
|
+
* EntityViewerComponent - Full-featured composite component for viewing entity data
|
|
157
|
+
*
|
|
158
|
+
* This component provides a complete data viewing experience with:
|
|
159
|
+
* - Switchable grid (AG Grid) and card views
|
|
160
|
+
* - Server-side filtering with UserSearchString
|
|
161
|
+
* - Server-side pagination with StartRow/MaxRows
|
|
162
|
+
* - Server-side sorting with OrderBy
|
|
163
|
+
* - Selection handling with configurable behavior
|
|
164
|
+
* - Loading, empty, and error states
|
|
165
|
+
* - Beautiful pagination UI with "Load More" pattern
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```html
|
|
169
|
+
* <!-- Basic usage - loads data automatically -->
|
|
170
|
+
* <mj-entity-viewer
|
|
171
|
+
* [entity]="selectedEntity"
|
|
172
|
+
* (recordSelected)="onRecordSelected($event)"
|
|
173
|
+
* (recordOpened)="onRecordOpened($event)">
|
|
174
|
+
* </mj-entity-viewer>
|
|
175
|
+
*
|
|
176
|
+
* <!-- With external state control (like Data Explorer) -->
|
|
177
|
+
* <mj-entity-viewer
|
|
178
|
+
* [entity]="selectedEntity"
|
|
179
|
+
* [(viewMode)]="state.viewMode"
|
|
180
|
+
* [filterText]="state.filterText"
|
|
181
|
+
* [selectedRecordId]="state.selectedRecordId"
|
|
182
|
+
* (recordSelected)="onRecordSelected($event)"
|
|
183
|
+
* (recordOpened)="onRecordOpened($event)"
|
|
184
|
+
* (sortChanged)="onSortChanged($event)">
|
|
185
|
+
* </mj-entity-viewer>
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export class EntityViewerComponent {
|
|
189
|
+
cdr;
|
|
190
|
+
// ========================================
|
|
191
|
+
// INPUTS
|
|
192
|
+
// ========================================
|
|
193
|
+
/**
|
|
194
|
+
* The entity to display records for
|
|
195
|
+
*/
|
|
196
|
+
entity = null;
|
|
197
|
+
/**
|
|
198
|
+
* Pre-loaded records (optional - if not provided, component loads data)
|
|
199
|
+
*/
|
|
200
|
+
records = null;
|
|
201
|
+
/**
|
|
202
|
+
* Configuration options for the viewer
|
|
203
|
+
*/
|
|
204
|
+
config = {};
|
|
205
|
+
/**
|
|
206
|
+
* Currently selected record ID (primary key string)
|
|
207
|
+
*/
|
|
208
|
+
selectedRecordId = null;
|
|
209
|
+
/**
|
|
210
|
+
* External view mode - allows parent to control view mode
|
|
211
|
+
* Supports two-way binding: [(viewMode)]="state.viewMode"
|
|
212
|
+
*/
|
|
213
|
+
viewMode = null;
|
|
214
|
+
/**
|
|
215
|
+
* External filter text - allows parent to control filter
|
|
216
|
+
* Supports two-way binding: [(filterText)]="state.filterText"
|
|
217
|
+
*/
|
|
218
|
+
filterText = null;
|
|
219
|
+
/**
|
|
220
|
+
* External sort state - allows parent to control sorting
|
|
221
|
+
*/
|
|
222
|
+
sortState = null;
|
|
223
|
+
/**
|
|
224
|
+
* Custom grid column definitions
|
|
225
|
+
*/
|
|
226
|
+
gridColumns = [];
|
|
227
|
+
/**
|
|
228
|
+
* Custom card template
|
|
229
|
+
*/
|
|
230
|
+
cardTemplate = null;
|
|
231
|
+
/**
|
|
232
|
+
* Optional User View entity that provides view configuration
|
|
233
|
+
* When provided, the component will use the view's WhereClause, GridState, SortState, etc.
|
|
234
|
+
* The view's filter is additive - UserSearchString is applied ON TOP of the view's WhereClause
|
|
235
|
+
*/
|
|
236
|
+
viewEntity = null;
|
|
237
|
+
/**
|
|
238
|
+
* Grid state configuration from a User View
|
|
239
|
+
* Controls column visibility, widths, order, and sort settings
|
|
240
|
+
*/
|
|
241
|
+
gridState = null;
|
|
242
|
+
// ========================================
|
|
243
|
+
// OUTPUTS
|
|
244
|
+
// ========================================
|
|
245
|
+
/**
|
|
246
|
+
* Emitted when a record is selected (single click)
|
|
247
|
+
*/
|
|
248
|
+
recordSelected = new EventEmitter();
|
|
249
|
+
/**
|
|
250
|
+
* Emitted when a record should be opened (double-click or open button)
|
|
251
|
+
*/
|
|
252
|
+
recordOpened = new EventEmitter();
|
|
253
|
+
/**
|
|
254
|
+
* Emitted when data is loaded
|
|
255
|
+
*/
|
|
256
|
+
dataLoaded = new EventEmitter();
|
|
257
|
+
/**
|
|
258
|
+
* Emitted when the view mode changes (for two-way binding)
|
|
259
|
+
*/
|
|
260
|
+
viewModeChange = new EventEmitter();
|
|
261
|
+
/**
|
|
262
|
+
* Emitted when filter text changes (for two-way binding)
|
|
263
|
+
*/
|
|
264
|
+
filterTextChange = new EventEmitter();
|
|
265
|
+
/**
|
|
266
|
+
* Emitted when filtered count changes
|
|
267
|
+
*/
|
|
268
|
+
filteredCountChanged = new EventEmitter();
|
|
269
|
+
/**
|
|
270
|
+
* Emitted when sort state changes
|
|
271
|
+
*/
|
|
272
|
+
sortChanged = new EventEmitter();
|
|
273
|
+
/**
|
|
274
|
+
* Emitted when grid state changes (column resize, reorder, etc.)
|
|
275
|
+
*/
|
|
276
|
+
gridStateChanged = new EventEmitter();
|
|
277
|
+
// ========================================
|
|
278
|
+
// INTERNAL STATE
|
|
279
|
+
// ========================================
|
|
280
|
+
internalViewMode = 'grid';
|
|
281
|
+
internalFilterText = '';
|
|
282
|
+
debouncedFilterText = '';
|
|
283
|
+
isLoading = false;
|
|
284
|
+
loadingMessage = 'Loading...';
|
|
285
|
+
internalRecords = [];
|
|
286
|
+
totalRecordCount = 0;
|
|
287
|
+
filteredRecordCount = 0;
|
|
288
|
+
/** Track which records matched on hidden (non-visible) fields */
|
|
289
|
+
hiddenFieldMatches = new Map();
|
|
290
|
+
/** Current sort state */
|
|
291
|
+
internalSortState = null;
|
|
292
|
+
/** Pagination state */
|
|
293
|
+
pagination = {
|
|
294
|
+
currentPage: 0,
|
|
295
|
+
pageSize: 100,
|
|
296
|
+
totalRecords: 0,
|
|
297
|
+
hasMore: false,
|
|
298
|
+
isLoading: false
|
|
299
|
+
};
|
|
300
|
+
destroy$ = new Subject();
|
|
301
|
+
filterInput$ = new Subject();
|
|
302
|
+
/** Track if this is the first load (vs. load more) */
|
|
303
|
+
isInitialLoad = true;
|
|
304
|
+
constructor(cdr) {
|
|
305
|
+
this.cdr = cdr;
|
|
306
|
+
}
|
|
307
|
+
// ========================================
|
|
308
|
+
// COMPUTED PROPERTIES
|
|
309
|
+
// ========================================
|
|
310
|
+
/**
|
|
311
|
+
* Get the effective view mode (external or internal)
|
|
312
|
+
*/
|
|
313
|
+
get effectiveViewMode() {
|
|
314
|
+
return this.viewMode ?? this.internalViewMode;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get the effective filter text (external or internal)
|
|
318
|
+
*/
|
|
319
|
+
get effectiveFilterText() {
|
|
320
|
+
return this.filterText ?? this.internalFilterText;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get the effective sort state (external or internal)
|
|
324
|
+
*/
|
|
325
|
+
get effectiveSortState() {
|
|
326
|
+
return this.sortState ?? this.internalSortState;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get merged configuration with defaults
|
|
330
|
+
*/
|
|
331
|
+
get effectiveConfig() {
|
|
332
|
+
return { ...DEFAULT_VIEWER_CONFIG, ...this.config };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get the records to display (external or internal)
|
|
336
|
+
*/
|
|
337
|
+
get displayRecords() {
|
|
338
|
+
return this.records ?? this.internalRecords;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get filtered records - when using server-side filtering, records are already filtered
|
|
342
|
+
* When using client-side filtering, apply filter locally
|
|
343
|
+
*/
|
|
344
|
+
get filteredRecords() {
|
|
345
|
+
const records = this.displayRecords;
|
|
346
|
+
// If server-side filtering is enabled, records are already filtered
|
|
347
|
+
if (this.effectiveConfig.serverSideFiltering) {
|
|
348
|
+
return records;
|
|
349
|
+
}
|
|
350
|
+
// Client-side filtering fallback
|
|
351
|
+
const filterText = this.debouncedFilterText?.trim().toLowerCase();
|
|
352
|
+
this.hiddenFieldMatches.clear();
|
|
353
|
+
if (!filterText || !this.entity) {
|
|
354
|
+
return records;
|
|
355
|
+
}
|
|
356
|
+
const visibleFields = this.getVisibleFieldNames();
|
|
357
|
+
return records.filter(record => {
|
|
358
|
+
const matchResult = this.recordMatchesFilter(record, filterText, visibleFields);
|
|
359
|
+
if (matchResult.matches && matchResult.matchedField && !matchResult.matchedInVisibleField) {
|
|
360
|
+
const recordKey = record.PrimaryKey.ToConcatenatedString();
|
|
361
|
+
this.hiddenFieldMatches.set(recordKey, matchResult.matchedField);
|
|
362
|
+
}
|
|
363
|
+
return matchResult.matches;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check if a record matches the filter text (client-side)
|
|
368
|
+
*/
|
|
369
|
+
recordMatchesFilter(record, filterText, visibleFields) {
|
|
370
|
+
if (!this.entity)
|
|
371
|
+
return { matches: true, matchedField: null, matchedInVisibleField: false };
|
|
372
|
+
let matchedField = null;
|
|
373
|
+
let matchedInVisibleField = false;
|
|
374
|
+
for (const field of this.entity.Fields) {
|
|
375
|
+
if (!this.shouldSearchField(field))
|
|
376
|
+
continue;
|
|
377
|
+
const value = record.Get(field.Name);
|
|
378
|
+
if (value == null)
|
|
379
|
+
continue;
|
|
380
|
+
const stringValue = String(value).toLowerCase();
|
|
381
|
+
if (this.matchesSearchTerm(stringValue, filterText)) {
|
|
382
|
+
matchedField = field.Name;
|
|
383
|
+
if (visibleFields.has(field.Name)) {
|
|
384
|
+
matchedInVisibleField = true;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
matches: matchedField !== null,
|
|
391
|
+
matchedField,
|
|
392
|
+
matchedInVisibleField
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Determine if a field should be included in search
|
|
397
|
+
*/
|
|
398
|
+
shouldSearchField(field) {
|
|
399
|
+
if (field.Name.startsWith('__mj_'))
|
|
400
|
+
return false;
|
|
401
|
+
if (field.TSType === 'Date')
|
|
402
|
+
return false;
|
|
403
|
+
if (field.SQLFullType?.trim().toLowerCase() === 'uniqueidentifier')
|
|
404
|
+
return false;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Check if a value matches the search term (supports SQL-style % wildcards)
|
|
409
|
+
*/
|
|
410
|
+
matchesSearchTerm(value, searchTerm) {
|
|
411
|
+
if (!searchTerm.includes('%')) {
|
|
412
|
+
return value.includes(searchTerm);
|
|
413
|
+
}
|
|
414
|
+
const fragments = searchTerm.split('%').filter(s => s.length > 0);
|
|
415
|
+
if (fragments.length === 0)
|
|
416
|
+
return true;
|
|
417
|
+
let searchStartIndex = 0;
|
|
418
|
+
for (const fragment of fragments) {
|
|
419
|
+
const foundIndex = value.indexOf(fragment, searchStartIndex);
|
|
420
|
+
if (foundIndex === -1)
|
|
421
|
+
return false;
|
|
422
|
+
searchStartIndex = foundIndex + fragment.length;
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get set of field names that are visible in the current view
|
|
428
|
+
*/
|
|
429
|
+
getVisibleFieldNames() {
|
|
430
|
+
const visible = new Set();
|
|
431
|
+
if (!this.entity)
|
|
432
|
+
return visible;
|
|
433
|
+
for (const field of this.entity.Fields) {
|
|
434
|
+
if (field.DefaultInView === true) {
|
|
435
|
+
visible.add(field.Name);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (this.entity.NameField) {
|
|
439
|
+
visible.add(this.entity.NameField.Name);
|
|
440
|
+
}
|
|
441
|
+
return visible;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Check if a record matched on a hidden field
|
|
445
|
+
*/
|
|
446
|
+
hasHiddenFieldMatch(record) {
|
|
447
|
+
if (!this.debouncedFilterText)
|
|
448
|
+
return false;
|
|
449
|
+
return this.hiddenFieldMatches.has(record.PrimaryKey.ToConcatenatedString());
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get the name of the hidden field that matched for display
|
|
453
|
+
*/
|
|
454
|
+
getHiddenMatchFieldName(record) {
|
|
455
|
+
const fieldName = this.hiddenFieldMatches.get(record.PrimaryKey.ToConcatenatedString());
|
|
456
|
+
if (!fieldName || !this.entity)
|
|
457
|
+
return '';
|
|
458
|
+
const field = this.entity.Fields.find(f => f.Name === fieldName);
|
|
459
|
+
return field ? field.DisplayNameOrName : fieldName;
|
|
460
|
+
}
|
|
461
|
+
// ========================================
|
|
462
|
+
// LIFECYCLE HOOKS
|
|
463
|
+
// ========================================
|
|
464
|
+
ngOnInit() {
|
|
465
|
+
this.applyConfig();
|
|
466
|
+
this.setupFilterDebounce();
|
|
467
|
+
// Initialize debounced filter from external filter text if provided
|
|
468
|
+
if (this.filterText !== null) {
|
|
469
|
+
this.debouncedFilterText = this.filterText;
|
|
470
|
+
}
|
|
471
|
+
// Initialize sort state from config
|
|
472
|
+
if (this.effectiveConfig.defaultSortField) {
|
|
473
|
+
this.internalSortState = {
|
|
474
|
+
field: this.effectiveConfig.defaultSortField,
|
|
475
|
+
direction: this.effectiveConfig.defaultSortDirection ?? 'asc'
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// Note: We don't call loadData() here because ngOnChanges runs before ngOnInit
|
|
479
|
+
// and already handles the initial entity binding. Calling loadData() here would
|
|
480
|
+
// result in duplicate data loading (200 records instead of 100).
|
|
481
|
+
}
|
|
482
|
+
ngOnChanges(changes) {
|
|
483
|
+
if (changes['config']) {
|
|
484
|
+
this.applyConfig();
|
|
485
|
+
}
|
|
486
|
+
if (changes['entity']) {
|
|
487
|
+
if (this.entity && !this.records) {
|
|
488
|
+
// Reset state for new entity - synchronously clear all data and force change detection
|
|
489
|
+
// before starting the async load to prevent stale data display
|
|
490
|
+
this.resetPaginationState();
|
|
491
|
+
this.cdr.detectChanges();
|
|
492
|
+
this.loadData();
|
|
493
|
+
}
|
|
494
|
+
else if (!this.entity) {
|
|
495
|
+
this.internalRecords = [];
|
|
496
|
+
this.totalRecordCount = 0;
|
|
497
|
+
this.filteredRecordCount = 0;
|
|
498
|
+
this.resetPaginationState();
|
|
499
|
+
this.cdr.detectChanges();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (changes['records'] && this.records) {
|
|
503
|
+
this.internalRecords = this.records;
|
|
504
|
+
this.totalRecordCount = this.records.length;
|
|
505
|
+
this.filteredRecordCount = this.records.length;
|
|
506
|
+
}
|
|
507
|
+
// Handle external filter text changes (from parent component)
|
|
508
|
+
if (changes['filterText']) {
|
|
509
|
+
const newFilter = this.filterText ?? '';
|
|
510
|
+
const oldFilter = this.debouncedFilterText;
|
|
511
|
+
this.internalFilterText = newFilter;
|
|
512
|
+
this.debouncedFilterText = newFilter;
|
|
513
|
+
// If server-side filtering and filter changed, reload from page 1
|
|
514
|
+
if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this.records) {
|
|
515
|
+
this.resetPaginationState();
|
|
516
|
+
this.loadData();
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
this.updateFilteredCount();
|
|
520
|
+
}
|
|
521
|
+
this.cdr.detectChanges();
|
|
522
|
+
}
|
|
523
|
+
// Handle external view mode changes
|
|
524
|
+
if (changes['viewMode'] && this.viewMode !== null) {
|
|
525
|
+
this.internalViewMode = this.viewMode;
|
|
526
|
+
}
|
|
527
|
+
// Handle external sort state changes
|
|
528
|
+
if (changes['sortState'] && this.sortState !== null) {
|
|
529
|
+
const oldSort = this.internalSortState;
|
|
530
|
+
this.internalSortState = this.sortState;
|
|
531
|
+
// If sort changed and using server-side sorting, reload
|
|
532
|
+
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
533
|
+
const sortChanged = !oldSort || !this.sortState ||
|
|
534
|
+
oldSort.field !== this.sortState.field ||
|
|
535
|
+
oldSort.direction !== this.sortState.direction;
|
|
536
|
+
if (sortChanged) {
|
|
537
|
+
this.resetPaginationState();
|
|
538
|
+
this.loadData();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Handle viewEntity changes - reload data when view changes
|
|
543
|
+
if (changes['viewEntity']) {
|
|
544
|
+
if (this.entity && !this.records) {
|
|
545
|
+
// Apply view's sort state if available
|
|
546
|
+
if (this.viewEntity) {
|
|
547
|
+
const viewSortInfo = this.viewEntity.ViewSortInfo;
|
|
548
|
+
if (viewSortInfo && viewSortInfo.length > 0) {
|
|
549
|
+
this.internalSortState = {
|
|
550
|
+
field: viewSortInfo[0].field,
|
|
551
|
+
direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.resetPaginationState();
|
|
556
|
+
this.loadData();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
ngOnDestroy() {
|
|
561
|
+
this.destroy$.next();
|
|
562
|
+
this.destroy$.complete();
|
|
563
|
+
}
|
|
564
|
+
// ========================================
|
|
565
|
+
// CONFIGURATION
|
|
566
|
+
// ========================================
|
|
567
|
+
applyConfig() {
|
|
568
|
+
const config = this.effectiveConfig;
|
|
569
|
+
this.pagination.pageSize = config.pageSize;
|
|
570
|
+
if (this.viewMode === null) {
|
|
571
|
+
this.internalViewMode = config.defaultViewMode;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
setupFilterDebounce() {
|
|
575
|
+
this.filterInput$
|
|
576
|
+
.pipe(debounceTime(this.effectiveConfig.filterDebounceMs), distinctUntilChanged(), takeUntil(this.destroy$))
|
|
577
|
+
.subscribe(filterText => {
|
|
578
|
+
const oldFilter = this.debouncedFilterText;
|
|
579
|
+
this.debouncedFilterText = filterText;
|
|
580
|
+
this.filterTextChange.emit(filterText);
|
|
581
|
+
// If server-side filtering and filter changed, reload from page 1
|
|
582
|
+
if (this.effectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.records) {
|
|
583
|
+
this.resetPaginationState();
|
|
584
|
+
this.loadData();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
this.updateFilteredCount();
|
|
588
|
+
}
|
|
589
|
+
this.cdr.detectChanges();
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Update the filtered record count and emit event
|
|
594
|
+
*/
|
|
595
|
+
updateFilteredCount() {
|
|
596
|
+
const newCount = this.filteredRecords.length;
|
|
597
|
+
if (this.filteredRecordCount !== newCount) {
|
|
598
|
+
this.filteredRecordCount = newCount;
|
|
599
|
+
this.filteredCountChanged.emit({
|
|
600
|
+
filteredCount: newCount,
|
|
601
|
+
totalCount: this.totalRecordCount
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Reset pagination state for a fresh load.
|
|
607
|
+
* Clears all record data and counts to prevent stale data display during entity switches.
|
|
608
|
+
*/
|
|
609
|
+
resetPaginationState() {
|
|
610
|
+
this.pagination = {
|
|
611
|
+
currentPage: 0,
|
|
612
|
+
pageSize: this.effectiveConfig.pageSize,
|
|
613
|
+
totalRecords: 0,
|
|
614
|
+
hasMore: false,
|
|
615
|
+
isLoading: false
|
|
616
|
+
};
|
|
617
|
+
this.internalRecords = [];
|
|
618
|
+
this.totalRecordCount = 0;
|
|
619
|
+
this.filteredRecordCount = 0;
|
|
620
|
+
this.isInitialLoad = true;
|
|
621
|
+
}
|
|
622
|
+
// ========================================
|
|
623
|
+
// DATA LOADING
|
|
624
|
+
// ========================================
|
|
625
|
+
/**
|
|
626
|
+
* Load data for the current entity with server-side filtering/sorting/pagination
|
|
627
|
+
*/
|
|
628
|
+
async loadData() {
|
|
629
|
+
if (!this.entity) {
|
|
630
|
+
this.internalRecords = [];
|
|
631
|
+
this.totalRecordCount = 0;
|
|
632
|
+
this.filteredRecordCount = 0;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Prevent concurrent loads which can cause duplicate records
|
|
636
|
+
if (this.isLoading) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.isLoading = true;
|
|
640
|
+
this.pagination.isLoading = true;
|
|
641
|
+
this.loadingMessage = this.isInitialLoad
|
|
642
|
+
? `Loading ${this.entity.Name}...`
|
|
643
|
+
: 'Loading more records...';
|
|
644
|
+
this.cdr.detectChanges();
|
|
645
|
+
const startTime = Date.now();
|
|
646
|
+
const config = this.effectiveConfig;
|
|
647
|
+
try {
|
|
648
|
+
const rv = new RunView();
|
|
649
|
+
// Build OrderBy clause
|
|
650
|
+
// Priority: 1) External sort state 2) View's OrderByClause 3) undefined
|
|
651
|
+
let orderBy;
|
|
652
|
+
const sortState = this.effectiveSortState;
|
|
653
|
+
if (config.serverSideSorting && sortState?.field && sortState.direction) {
|
|
654
|
+
orderBy = `${sortState.field} ${sortState.direction.toUpperCase()}`;
|
|
655
|
+
}
|
|
656
|
+
else if (this.viewEntity?.OrderByClause) {
|
|
657
|
+
orderBy = this.viewEntity.OrderByClause;
|
|
658
|
+
}
|
|
659
|
+
// Calculate StartRow for pagination
|
|
660
|
+
const startRow = this.pagination.currentPage * config.pageSize;
|
|
661
|
+
// Build ExtraFilter from view's WhereClause if available
|
|
662
|
+
// The view's WhereClause is the "business filter" - UserSearchString is additive
|
|
663
|
+
const extraFilter = this.viewEntity?.WhereClause || undefined;
|
|
664
|
+
const result = await rv.RunView({
|
|
665
|
+
EntityName: this.entity.Name,
|
|
666
|
+
ResultType: 'entity_object',
|
|
667
|
+
MaxRows: config.pageSize,
|
|
668
|
+
StartRow: startRow,
|
|
669
|
+
OrderBy: orderBy,
|
|
670
|
+
ExtraFilter: extraFilter,
|
|
671
|
+
UserSearchString: config.serverSideFiltering ? this.debouncedFilterText || undefined : undefined
|
|
672
|
+
});
|
|
673
|
+
if (result.Success) {
|
|
674
|
+
// Append or replace records based on whether this is initial load
|
|
675
|
+
if (this.isInitialLoad) {
|
|
676
|
+
this.internalRecords = result.Results;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
this.internalRecords = [...this.internalRecords, ...result.Results];
|
|
680
|
+
}
|
|
681
|
+
this.totalRecordCount = result.TotalRowCount;
|
|
682
|
+
this.filteredRecordCount = this.internalRecords.length;
|
|
683
|
+
// Update pagination state
|
|
684
|
+
this.pagination.totalRecords = result.TotalRowCount;
|
|
685
|
+
this.pagination.hasMore = this.internalRecords.length < result.TotalRowCount;
|
|
686
|
+
this.dataLoaded.emit({
|
|
687
|
+
totalRowCount: result.TotalRowCount,
|
|
688
|
+
loadedRowCount: this.internalRecords.length,
|
|
689
|
+
loadTime: Date.now() - startTime,
|
|
690
|
+
records: this.internalRecords
|
|
691
|
+
});
|
|
692
|
+
this.filteredCountChanged.emit({
|
|
693
|
+
filteredCount: this.internalRecords.length,
|
|
694
|
+
totalCount: result.TotalRowCount
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.error('Failed to load records:', result.ErrorMessage);
|
|
699
|
+
if (this.isInitialLoad) {
|
|
700
|
+
this.internalRecords = [];
|
|
701
|
+
}
|
|
702
|
+
this.totalRecordCount = 0;
|
|
703
|
+
this.filteredRecordCount = 0;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
console.error('Error loading records:', error);
|
|
708
|
+
if (this.isInitialLoad) {
|
|
709
|
+
this.internalRecords = [];
|
|
710
|
+
}
|
|
711
|
+
this.totalRecordCount = 0;
|
|
712
|
+
this.filteredRecordCount = 0;
|
|
713
|
+
}
|
|
714
|
+
finally {
|
|
715
|
+
this.isLoading = false;
|
|
716
|
+
this.pagination.isLoading = false;
|
|
717
|
+
this.isInitialLoad = false;
|
|
718
|
+
this.cdr.detectChanges();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Load more records (next page)
|
|
723
|
+
*/
|
|
724
|
+
loadMore() {
|
|
725
|
+
if (this.pagination.isLoading || !this.pagination.hasMore) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.pagination.currentPage++;
|
|
729
|
+
this.loadData();
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Refresh data (re-load from server, starting at page 1)
|
|
733
|
+
*/
|
|
734
|
+
refresh() {
|
|
735
|
+
if (!this.records) {
|
|
736
|
+
this.resetPaginationState();
|
|
737
|
+
this.loadData();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// ========================================
|
|
741
|
+
// VIEW MODE
|
|
742
|
+
// ========================================
|
|
743
|
+
/**
|
|
744
|
+
* Set the view mode and emit change event
|
|
745
|
+
*/
|
|
746
|
+
setViewMode(mode) {
|
|
747
|
+
if (this.effectiveViewMode !== mode) {
|
|
748
|
+
this.internalViewMode = mode;
|
|
749
|
+
this.viewModeChange.emit(mode);
|
|
750
|
+
this.cdr.detectChanges();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// ========================================
|
|
754
|
+
// FILTERING
|
|
755
|
+
// ========================================
|
|
756
|
+
/**
|
|
757
|
+
* Handle filter input change
|
|
758
|
+
*/
|
|
759
|
+
onFilterChange(value) {
|
|
760
|
+
this.internalFilterText = value;
|
|
761
|
+
this.filterInput$.next(value);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Clear the filter
|
|
765
|
+
*/
|
|
766
|
+
clearFilter() {
|
|
767
|
+
this.internalFilterText = '';
|
|
768
|
+
this.filterInput$.next('');
|
|
769
|
+
this.cdr.detectChanges();
|
|
770
|
+
}
|
|
771
|
+
// ========================================
|
|
772
|
+
// SORTING
|
|
773
|
+
// ========================================
|
|
774
|
+
/**
|
|
775
|
+
* Handle sort change from grid component
|
|
776
|
+
*/
|
|
777
|
+
onSortChanged(event) {
|
|
778
|
+
const oldSort = this.internalSortState;
|
|
779
|
+
this.internalSortState = event.sort;
|
|
780
|
+
this.sortChanged.emit(event);
|
|
781
|
+
// If server-side sorting, reload from page 1
|
|
782
|
+
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
783
|
+
const sortChanged = !oldSort || !event.sort ||
|
|
784
|
+
oldSort.field !== event.sort?.field ||
|
|
785
|
+
oldSort.direction !== event.sort?.direction;
|
|
786
|
+
if (sortChanged) {
|
|
787
|
+
this.resetPaginationState();
|
|
788
|
+
this.loadData();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// ========================================
|
|
793
|
+
// EVENT HANDLERS
|
|
794
|
+
// ========================================
|
|
795
|
+
/**
|
|
796
|
+
* Handle record selection from child components (grid or cards)
|
|
797
|
+
*/
|
|
798
|
+
onRecordSelected(event) {
|
|
799
|
+
this.recordSelected.emit(event);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Handle record opened from child components (grid or cards)
|
|
803
|
+
*/
|
|
804
|
+
onRecordOpened(event) {
|
|
805
|
+
this.recordOpened.emit(event);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Handle grid state changes (column resize, reorder, etc.)
|
|
809
|
+
*/
|
|
810
|
+
onGridStateChanged(event) {
|
|
811
|
+
this.gridStateChanged.emit(event);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Handle load more from pagination component
|
|
815
|
+
*/
|
|
816
|
+
onLoadMore() {
|
|
817
|
+
this.loadMore();
|
|
818
|
+
}
|
|
819
|
+
static ɵfac = function EntityViewerComponent_Factory(t) { return new (t || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
820
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityViewerComponent, selectors: [["mj-entity-viewer"]], hostAttrs: [2, "display", "block", "height", "100%"], inputs: { entity: "entity", records: "records", config: "config", selectedRecordId: "selectedRecordId", viewMode: "viewMode", filterText: "filterText", sortState: "sortState", gridColumns: "gridColumns", cardTemplate: "cardTemplate", viewEntity: "viewEntity", gridState: "gridState" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", dataLoaded: "dataLoaded", viewModeChange: "viewModeChange", filterTextChange: "filterTextChange", filteredCountChanged: "filteredCountChanged", sortChanged: "sortChanged", gridStateChanged: "gridStateChanged" }, features: [i0.ɵɵNgOnChangesFeature], decls: 8, vars: 5, consts: [[1, "entity-viewer-container"], [1, "viewer-header"], [1, "viewer-content"], [1, "loading-container"], [1, "empty-state"], [3, "pagination", "loadedRecordCount"], [1, "filter-container"], [1, "record-count"], [1, "view-mode-toggle"], [1, "fa-solid", "fa-search", "filter-icon"], ["type", "text", 1, "filter-input", 3, "input", "placeholder", "value"], ["title", "Clear filter", 1, "clear-filter-btn"], ["title", "Clear filter", 1, "clear-filter-btn", 3, "click"], [1, "fa-solid", "fa-times"], ["title", "Grid View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-list"], ["title", "Cards View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-grip"], ["size", "medium", 3, "text"], [1, "fa-solid", "fa-database"], [1, "fa-solid", "fa-inbox"], [3, "entity", "records", "selectedRecordId", "columns", "height", "filterText", "sortState", "serverSideSorting", "gridState"], [3, "entity", "records", "selectedRecordId", "cardTemplate", "hiddenFieldMatches", "filterText"], [3, "recordSelected", "recordOpened", "sortChanged", "gridStateChanged", "entity", "records", "selectedRecordId", "columns", "height", "filterText", "sortState", "serverSideSorting", "gridState"], [3, "recordSelected", "recordOpened", "entity", "records", "selectedRecordId", "cardTemplate", "hiddenFieldMatches", "filterText"], [3, "loadMore", "pagination", "loadedRecordCount"]], template: function EntityViewerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
821
|
+
i0.ɵɵelementStart(0, "div", 0);
|
|
822
|
+
i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Template, 4, 3, "div", 1);
|
|
823
|
+
i0.ɵɵelementStart(2, "div", 2);
|
|
824
|
+
i0.ɵɵtemplate(3, EntityViewerComponent_Conditional_3_Template, 2, 1, "div", 3)(4, EntityViewerComponent_Conditional_4_Template, 4, 0, "div", 4)(5, EntityViewerComponent_Conditional_5_Template, 4, 1, "div", 4)(6, EntityViewerComponent_Conditional_6_Template, 2, 1);
|
|
825
|
+
i0.ɵɵelementEnd();
|
|
826
|
+
i0.ɵɵtemplate(7, EntityViewerComponent_Conditional_7_Template, 1, 2, "mj-pagination", 5);
|
|
827
|
+
i0.ɵɵelementEnd();
|
|
828
|
+
} if (rf & 2) {
|
|
829
|
+
i0.ɵɵstyleProp("height", ctx.effectiveConfig.height);
|
|
830
|
+
i0.ɵɵadvance();
|
|
831
|
+
i0.ɵɵconditional(ctx.effectiveConfig.showFilter || ctx.effectiveConfig.showViewModeToggle || ctx.effectiveConfig.showRecordCount ? 1 : -1);
|
|
832
|
+
i0.ɵɵadvance(2);
|
|
833
|
+
i0.ɵɵconditional(ctx.isLoading && ctx.filteredRecords.length === 0 ? 3 : !ctx.entity ? 4 : ctx.filteredRecords.length === 0 && !ctx.isLoading ? 5 : 6);
|
|
834
|
+
i0.ɵɵadvance(4);
|
|
835
|
+
i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.entity && (ctx.pagination.hasMore || ctx.pagination.totalRecords > ctx.effectiveConfig.pageSize) ? 7 : -1);
|
|
836
|
+
} }, dependencies: [i1.LoadingComponent, i2.EntityGridComponent, i3.EntityCardsComponent, i4.PaginationComponent, i5.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n background: #fafafa;\n}\n\n\n\n.viewer-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n\n\n.filter-container[_ngcontent-%COMP%] {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n.filter-input[_ngcontent-%COMP%]::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%] {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%]:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n\n\n.record-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n\n\n.view-mode-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn[_ngcontent-%COMP%]:hover {\n color: #424242;\n}\n\n.toggle-btn.active[_ngcontent-%COMP%] {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n}\n\n\n\n.loading-container[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n\n\n.empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 14px;\n}"] });
|
|
837
|
+
}
|
|
838
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
|
|
839
|
+
type: Component,
|
|
840
|
+
args: [{ selector: 'mj-entity-viewer', host: {
|
|
841
|
+
'style': 'display: block; height: 100%;'
|
|
842
|
+
}, template: "<div class=\"entity-viewer-container\" [style.height]=\"effectiveConfig.height\">\n <!-- Header -->\n @if (effectiveConfig.showFilter || effectiveConfig.showViewModeToggle || effectiveConfig.showRecordCount) {\n <div class=\"viewer-header\">\n <!-- Filter Input -->\n @if (effectiveConfig.showFilter) {\n <div class=\"filter-container\">\n <i class=\"fa-solid fa-search filter-icon\"></i>\n <input\n type=\"text\"\n class=\"filter-input\"\n [placeholder]=\"effectiveConfig.filterPlaceholder\"\n [value]=\"effectiveFilterText\"\n (input)=\"onFilterChange($any($event.target).value)\"\n />\n @if (effectiveFilterText) {\n <button class=\"clear-filter-btn\" (click)=\"clearFilter()\" title=\"Clear filter\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Record Count -->\n @if (effectiveConfig.showRecordCount && entity) {\n <div class=\"record-count\">\n @if (filteredRecordCount !== totalRecordCount) {\n <span>{{ filteredRecordCount | number }} of {{ totalRecordCount | number }} records</span>\n } @else {\n <span>{{ totalRecordCount | number }} records</span>\n }\n </div>\n }\n\n <!-- View Mode Toggle -->\n @if (effectiveConfig.showViewModeToggle) {\n <div class=\"view-mode-toggle\">\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'grid'\"\n (click)=\"setViewMode('grid')\"\n title=\"Grid View\">\n <i class=\"fa-solid fa-list\"></i>\n </button>\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'cards'\"\n (click)=\"setViewMode('cards')\"\n title=\"Cards View\">\n <i class=\"fa-solid fa-grip\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Content -->\n <div class=\"viewer-content\">\n @if (isLoading && filteredRecords.length === 0) {\n <!-- Initial loading state -->\n <div class=\"loading-container\">\n <mj-loading [text]=\"loadingMessage\" size=\"medium\"></mj-loading>\n </div>\n } @else if (!entity) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n } @else if (filteredRecords.length === 0 && !isLoading) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ debouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n } @else {\n @if (effectiveViewMode === 'grid') {\n <mj-entity-grid\n [entity]=\"entity\"\n [records]=\"filteredRecords\"\n [selectedRecordId]=\"selectedRecordId\"\n [columns]=\"gridColumns\"\n [height]=\"'100%'\"\n [filterText]=\"debouncedFilterText\"\n [sortState]=\"effectiveSortState\"\n [serverSideSorting]=\"effectiveConfig.serverSideSorting\"\n [gridState]=\"gridState\"\n (recordSelected)=\"onRecordSelected($event)\"\n (recordOpened)=\"onRecordOpened($event)\"\n (sortChanged)=\"onSortChanged($event)\"\n (gridStateChanged)=\"onGridStateChanged($event)\">\n </mj-entity-grid>\n } @else {\n <mj-entity-cards\n [entity]=\"entity\"\n [records]=\"filteredRecords\"\n [selectedRecordId]=\"selectedRecordId\"\n [cardTemplate]=\"cardTemplate\"\n [hiddenFieldMatches]=\"hiddenFieldMatches\"\n [filterText]=\"debouncedFilterText\"\n (recordSelected)=\"onRecordSelected($event)\"\n (recordOpened)=\"onRecordOpened($event)\">\n </mj-entity-cards>\n }\n }\n </div>\n\n <!-- Pagination - only show when there's more data to load OR we've loaded more than one page -->\n @if (effectiveConfig.showPagination && entity && (pagination.hasMore || pagination.totalRecords > effectiveConfig.pageSize)) {\n <mj-pagination\n [pagination]=\"pagination\"\n [loadedRecordCount]=\"filteredRecords.length\"\n (loadMore)=\"onLoadMore()\">\n </mj-pagination>\n }\n</div>\n", styles: [".entity-viewer-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n background: #fafafa;\n}\n\n/* Header */\n.viewer-header {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n/* Filter */\n.filter-container {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input:focus {\n border-color: #1976d2;\n}\n\n.filter-input::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n/* Record Count */\n.record-count {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn:hover {\n color: #424242;\n}\n\n.toggle-btn.active {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n}\n\n/* Loading State */\n.loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n/* Empty State */\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state p {\n margin: 0;\n font-size: 14px;\n}\n"] }]
|
|
843
|
+
}], () => [{ type: i0.ChangeDetectorRef }], { entity: [{
|
|
844
|
+
type: Input
|
|
845
|
+
}], records: [{
|
|
846
|
+
type: Input
|
|
847
|
+
}], config: [{
|
|
848
|
+
type: Input
|
|
849
|
+
}], selectedRecordId: [{
|
|
850
|
+
type: Input
|
|
851
|
+
}], viewMode: [{
|
|
852
|
+
type: Input
|
|
853
|
+
}], filterText: [{
|
|
854
|
+
type: Input
|
|
855
|
+
}], sortState: [{
|
|
856
|
+
type: Input
|
|
857
|
+
}], gridColumns: [{
|
|
858
|
+
type: Input
|
|
859
|
+
}], cardTemplate: [{
|
|
860
|
+
type: Input
|
|
861
|
+
}], viewEntity: [{
|
|
862
|
+
type: Input
|
|
863
|
+
}], gridState: [{
|
|
864
|
+
type: Input
|
|
865
|
+
}], recordSelected: [{
|
|
866
|
+
type: Output
|
|
867
|
+
}], recordOpened: [{
|
|
868
|
+
type: Output
|
|
869
|
+
}], dataLoaded: [{
|
|
870
|
+
type: Output
|
|
871
|
+
}], viewModeChange: [{
|
|
872
|
+
type: Output
|
|
873
|
+
}], filterTextChange: [{
|
|
874
|
+
type: Output
|
|
875
|
+
}], filteredCountChanged: [{
|
|
876
|
+
type: Output
|
|
877
|
+
}], sortChanged: [{
|
|
878
|
+
type: Output
|
|
879
|
+
}], gridStateChanged: [{
|
|
880
|
+
type: Output
|
|
881
|
+
}] }); })();
|
|
882
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 65 }); })();
|
|
883
|
+
//# sourceMappingURL=entity-viewer.component.js.map
|