@memberjunction/ng-entity-viewer 0.0.1 → 2.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +224 -43
  2. package/dist/lib/entity-cards/entity-cards.component.d.ts +163 -0
  3. package/dist/lib/entity-cards/entity-cards.component.d.ts.map +1 -0
  4. package/dist/lib/entity-cards/entity-cards.component.js +797 -0
  5. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -0
  6. package/dist/lib/entity-grid/entity-grid.component.d.ts +216 -0
  7. package/dist/lib/entity-grid/entity-grid.component.d.ts.map +1 -0
  8. package/dist/lib/entity-grid/entity-grid.component.js +676 -0
  9. package/dist/lib/entity-grid/entity-grid.component.js.map +1 -0
  10. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +182 -0
  11. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -0
  12. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +787 -0
  13. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -0
  14. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +252 -0
  15. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -0
  16. package/dist/lib/entity-viewer/entity-viewer.component.js +883 -0
  17. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -0
  18. package/dist/lib/pagination/pagination.component.d.ts +60 -0
  19. package/dist/lib/pagination/pagination.component.d.ts.map +1 -0
  20. package/dist/lib/pagination/pagination.component.js +199 -0
  21. package/dist/lib/pagination/pagination.component.js.map +1 -0
  22. package/dist/lib/pill/pill.component.d.ts +58 -0
  23. package/dist/lib/pill/pill.component.d.ts.map +1 -0
  24. package/dist/lib/pill/pill.component.js +125 -0
  25. package/dist/lib/pill/pill.component.js.map +1 -0
  26. package/dist/lib/types.d.ts +316 -0
  27. package/dist/lib/types.d.ts.map +1 -0
  28. package/dist/lib/types.js +30 -0
  29. package/dist/lib/types.js.map +1 -0
  30. package/dist/lib/utils/highlight.util.d.ts +69 -0
  31. package/dist/lib/utils/highlight.util.d.ts.map +1 -0
  32. package/dist/lib/utils/highlight.util.js +214 -0
  33. package/dist/lib/utils/highlight.util.js.map +1 -0
  34. package/dist/module.d.ts +38 -0
  35. package/dist/module.d.ts.map +1 -0
  36. package/dist/module.js +83 -0
  37. package/dist/module.js.map +1 -0
  38. package/dist/public-api.d.ts +16 -0
  39. package/dist/public-api.d.ts.map +1 -0
  40. package/dist/public-api.js +20 -0
  41. package/dist/public-api.js.map +1 -0
  42. 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" }); })();
883
+ //# sourceMappingURL=entity-viewer.component.js.map