@memberjunction/ng-entity-viewer 3.0.0 → 3.1.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/dist/lib/entity-cards/entity-cards.component.js +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +8 -0
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.js +76 -27
- package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts +44 -12
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.js +250 -133
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
- package/dist/lib/pagination/pagination.component.js +1 -1
- package/dist/lib/pill/pill.component.js +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.d.ts +87 -2
- package/dist/lib/view-config-panel/view-config-panel.component.d.ts.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.js +719 -425
- package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
- package/package.json +9 -9
- package/dist/lib/entity-grid/entity-grid.component.d.ts +0 -216
- package/dist/lib/entity-grid/entity-grid.component.d.ts.map +0 -1
- package/dist/lib/entity-grid/entity-grid.component.js +0 -676
- package/dist/lib/entity-grid/entity-grid.component.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
|
2
2
|
import { Subject } from 'rxjs';
|
|
3
3
|
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
|
4
|
-
import { EntityFieldTSType, RunView } from '@memberjunction/core';
|
|
4
|
+
import { EntityFieldTSType, RunView, Metadata } from '@memberjunction/core';
|
|
5
5
|
import { TimelineGroup } from '@memberjunction/ng-timeline';
|
|
6
6
|
import { DEFAULT_VIEWER_CONFIG } from '../types';
|
|
7
7
|
import { EntityDataGridComponent } from '../entity-data-grid/entity-data-grid.component';
|
|
@@ -165,7 +165,7 @@ function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
|
165
165
|
i0.ɵɵadvance();
|
|
166
166
|
i0.ɵɵconditional(ctx_r1.effectiveConfig.showFilter ? 1 : -1);
|
|
167
167
|
i0.ɵɵadvance();
|
|
168
|
-
i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.
|
|
168
|
+
i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.effectiveEntity ? 2 : -1);
|
|
169
169
|
i0.ɵɵadvance();
|
|
170
170
|
i0.ɵɵconditional(ctx_r1.effectiveConfig.showViewModeToggle ? 3 : -1);
|
|
171
171
|
i0.ɵɵadvance();
|
|
@@ -216,20 +216,70 @@ function EntityViewerComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
|
|
|
216
216
|
export class EntityViewerComponent {
|
|
217
217
|
cdr;
|
|
218
218
|
// ========================================
|
|
219
|
-
// INPUTS
|
|
219
|
+
// INPUTS (using getter/setter pattern)
|
|
220
220
|
// ========================================
|
|
221
|
+
_entity = null;
|
|
222
|
+
_records = null;
|
|
223
|
+
_config = {};
|
|
224
|
+
_viewMode = null;
|
|
225
|
+
_filterText = null;
|
|
226
|
+
_sortState = null;
|
|
227
|
+
_viewEntity = null;
|
|
228
|
+
_timelineConfig = null;
|
|
229
|
+
_initialized = false;
|
|
221
230
|
/**
|
|
222
231
|
* The entity to display records for
|
|
223
232
|
*/
|
|
224
|
-
entity
|
|
233
|
+
get entity() {
|
|
234
|
+
return this._entity;
|
|
235
|
+
}
|
|
236
|
+
set entity(value) {
|
|
237
|
+
this._entity = value;
|
|
238
|
+
// Detect date fields for timeline support
|
|
239
|
+
this.detectDateFields();
|
|
240
|
+
if (this._initialized) {
|
|
241
|
+
if (value && !this._records) {
|
|
242
|
+
// Reset state for new entity - synchronously clear all data and force change detection
|
|
243
|
+
// before starting the async load to prevent stale data display
|
|
244
|
+
this.resetPaginationState();
|
|
245
|
+
this.cdr.detectChanges();
|
|
246
|
+
this.loadData();
|
|
247
|
+
}
|
|
248
|
+
else if (!value) {
|
|
249
|
+
this.internalRecords = [];
|
|
250
|
+
this.totalRecordCount = 0;
|
|
251
|
+
this.filteredRecordCount = 0;
|
|
252
|
+
this.resetPaginationState();
|
|
253
|
+
this.cdr.detectChanges();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
225
257
|
/**
|
|
226
258
|
* Pre-loaded records (optional - if not provided, component loads data)
|
|
227
259
|
*/
|
|
228
|
-
records
|
|
260
|
+
get records() {
|
|
261
|
+
return this._records;
|
|
262
|
+
}
|
|
263
|
+
set records(value) {
|
|
264
|
+
this._records = value;
|
|
265
|
+
if (value) {
|
|
266
|
+
this.internalRecords = value;
|
|
267
|
+
this.totalRecordCount = value.length;
|
|
268
|
+
this.filteredRecordCount = value.length;
|
|
269
|
+
// Update timeline with new records
|
|
270
|
+
this.updateTimelineGroups();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
229
273
|
/**
|
|
230
274
|
* Configuration options for the viewer
|
|
231
275
|
*/
|
|
232
|
-
config
|
|
276
|
+
get config() {
|
|
277
|
+
return this._config;
|
|
278
|
+
}
|
|
279
|
+
set config(value) {
|
|
280
|
+
this._config = value;
|
|
281
|
+
this.applyConfig();
|
|
282
|
+
}
|
|
233
283
|
/**
|
|
234
284
|
* Currently selected record ID (primary key string)
|
|
235
285
|
*/
|
|
@@ -238,16 +288,67 @@ export class EntityViewerComponent {
|
|
|
238
288
|
* External view mode - allows parent to control view mode
|
|
239
289
|
* Supports two-way binding: [(viewMode)]="state.viewMode"
|
|
240
290
|
*/
|
|
241
|
-
viewMode
|
|
291
|
+
get viewMode() {
|
|
292
|
+
return this._viewMode;
|
|
293
|
+
}
|
|
294
|
+
set viewMode(value) {
|
|
295
|
+
this._viewMode = value;
|
|
296
|
+
if (value !== null) {
|
|
297
|
+
this.internalViewMode = value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
242
300
|
/**
|
|
243
301
|
* External filter text - allows parent to control filter
|
|
244
302
|
* Supports two-way binding: [(filterText)]="state.filterText"
|
|
245
303
|
*/
|
|
246
|
-
filterText
|
|
304
|
+
get filterText() {
|
|
305
|
+
return this._filterText;
|
|
306
|
+
}
|
|
307
|
+
set filterText(value) {
|
|
308
|
+
const oldFilter = this.debouncedFilterText;
|
|
309
|
+
this._filterText = value;
|
|
310
|
+
const newFilter = value ?? '';
|
|
311
|
+
this.internalFilterText = newFilter;
|
|
312
|
+
this.debouncedFilterText = newFilter;
|
|
313
|
+
if (this._initialized) {
|
|
314
|
+
// If server-side filtering and filter changed, reload from page 1
|
|
315
|
+
// Keep existing records visible during refresh for better UX
|
|
316
|
+
if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this._records) {
|
|
317
|
+
this.resetPaginationState(false);
|
|
318
|
+
this.loadData();
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.updateFilteredCount();
|
|
322
|
+
}
|
|
323
|
+
this.cdr.detectChanges();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
247
326
|
/**
|
|
248
327
|
* External sort state - allows parent to control sorting
|
|
249
328
|
*/
|
|
250
|
-
sortState
|
|
329
|
+
get sortState() {
|
|
330
|
+
return this._sortState;
|
|
331
|
+
}
|
|
332
|
+
set sortState(value) {
|
|
333
|
+
const oldSort = this.internalSortState;
|
|
334
|
+
this._sortState = value;
|
|
335
|
+
if (value !== null) {
|
|
336
|
+
this.internalSortState = value;
|
|
337
|
+
if (this._initialized) {
|
|
338
|
+
// If sort changed and using server-side sorting, reload
|
|
339
|
+
// Keep existing records visible during refresh for better UX
|
|
340
|
+
if (this.effectiveConfig.serverSideSorting && !this._records) {
|
|
341
|
+
const sortChanged = !oldSort || !value ||
|
|
342
|
+
oldSort.field !== value.field ||
|
|
343
|
+
oldSort.direction !== value.direction;
|
|
344
|
+
if (sortChanged) {
|
|
345
|
+
this.resetPaginationState(false);
|
|
346
|
+
this.loadData();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
251
352
|
/**
|
|
252
353
|
* Custom grid column definitions
|
|
253
354
|
*/
|
|
@@ -261,7 +362,26 @@ export class EntityViewerComponent {
|
|
|
261
362
|
* When provided, the component will use the view's WhereClause, GridState, SortState, etc.
|
|
262
363
|
* The view's filter is additive - UserSearchString is applied ON TOP of the view's WhereClause
|
|
263
364
|
*/
|
|
264
|
-
viewEntity
|
|
365
|
+
get viewEntity() {
|
|
366
|
+
return this._viewEntity;
|
|
367
|
+
}
|
|
368
|
+
set viewEntity(value) {
|
|
369
|
+
this._viewEntity = value;
|
|
370
|
+
if (this._initialized && this._entity && !this._records) {
|
|
371
|
+
// Apply view's sort state if available
|
|
372
|
+
if (value) {
|
|
373
|
+
const viewSortInfo = value.ViewSortInfo;
|
|
374
|
+
if (viewSortInfo && viewSortInfo.length > 0) {
|
|
375
|
+
this.internalSortState = {
|
|
376
|
+
field: viewSortInfo[0].field,
|
|
377
|
+
direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this.resetPaginationState();
|
|
382
|
+
this.loadData();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
265
385
|
/**
|
|
266
386
|
* Grid state configuration from a User View
|
|
267
387
|
* Controls column visibility, widths, order, and sort settings
|
|
@@ -285,13 +405,12 @@ export class EntityViewerComponent {
|
|
|
285
405
|
prev.segmentGrouping === value.segmentGrouping);
|
|
286
406
|
if (!isEqual) {
|
|
287
407
|
this._timelineConfig = value;
|
|
288
|
-
if (value && this.
|
|
408
|
+
if (value && this._entity) {
|
|
289
409
|
this.configureTimeline();
|
|
290
410
|
this.cdr.markForCheck();
|
|
291
411
|
}
|
|
292
412
|
}
|
|
293
413
|
}
|
|
294
|
-
_timelineConfig = null;
|
|
295
414
|
/**
|
|
296
415
|
* Whether to show the grid toolbar.
|
|
297
416
|
* When false, the grid is displayed without its own toolbar - useful when
|
|
@@ -463,6 +582,51 @@ export class EntityViewerComponent {
|
|
|
463
582
|
// ========================================
|
|
464
583
|
// COMPUTED PROPERTIES
|
|
465
584
|
// ========================================
|
|
585
|
+
/**
|
|
586
|
+
* Get the effective entity - uses entity input if provided, otherwise derives from viewEntity
|
|
587
|
+
* This allows callers to provide just a viewEntity without explicitly setting the entity input.
|
|
588
|
+
* Uses fallback resolution when ViewEntityInfo is not available.
|
|
589
|
+
*/
|
|
590
|
+
get effectiveEntity() {
|
|
591
|
+
if (this.entity) {
|
|
592
|
+
return this.entity;
|
|
593
|
+
}
|
|
594
|
+
// Auto-derive from viewEntity if available
|
|
595
|
+
if (this.viewEntity) {
|
|
596
|
+
return this.getEntityInfoFromViewEntity(this.viewEntity);
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Gets EntityInfo from a ViewEntity with multiple fallback strategies.
|
|
602
|
+
* Priority: 1) ViewEntityInfo property (set by Load)
|
|
603
|
+
* 2) Entity name lookup (virtual field)
|
|
604
|
+
* 3) EntityID lookup
|
|
605
|
+
* Returns null if entity cannot be determined.
|
|
606
|
+
*/
|
|
607
|
+
getEntityInfoFromViewEntity(viewEntity) {
|
|
608
|
+
// First try: ViewEntityInfo is the preferred source (set by UserViewEntityExtended.Load)
|
|
609
|
+
if (viewEntity.ViewEntityInfo) {
|
|
610
|
+
return viewEntity.ViewEntityInfo;
|
|
611
|
+
}
|
|
612
|
+
const md = new Metadata();
|
|
613
|
+
// Second try: Look up by Entity name (virtual field that returns entity name)
|
|
614
|
+
if (viewEntity.Entity) {
|
|
615
|
+
const entityByName = md.Entities.find(e => e.Name === viewEntity.Entity);
|
|
616
|
+
if (entityByName) {
|
|
617
|
+
return entityByName;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Third try: Look up by EntityID
|
|
621
|
+
if (viewEntity.EntityID) {
|
|
622
|
+
const entityById = md.Entities.find(e => e.ID === viewEntity.EntityID);
|
|
623
|
+
if (entityById) {
|
|
624
|
+
return entityById;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
console.warn(`[EntityViewer] Could not determine entity for view "${viewEntity.Name}" (ID: ${viewEntity.ID})`);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
466
630
|
/**
|
|
467
631
|
* Get the effective view mode (external or internal)
|
|
468
632
|
*/
|
|
@@ -524,17 +688,18 @@ export class EntityViewerComponent {
|
|
|
524
688
|
* which would cause the grid to reinitialize
|
|
525
689
|
*/
|
|
526
690
|
get gridParams() {
|
|
527
|
-
|
|
691
|
+
const entity = this.effectiveEntity;
|
|
692
|
+
if (!entity) {
|
|
528
693
|
return null;
|
|
529
694
|
}
|
|
530
695
|
// Check if we need to recreate the params object
|
|
531
|
-
const entityChanged = this._lastGridParamsEntity !==
|
|
696
|
+
const entityChanged = this._lastGridParamsEntity !== entity.Name;
|
|
532
697
|
const viewEntityChanged = this._lastGridParamsViewEntity !== this.viewEntity;
|
|
533
698
|
if (entityChanged || viewEntityChanged || !this._cachedGridParams) {
|
|
534
|
-
this._lastGridParamsEntity =
|
|
699
|
+
this._lastGridParamsEntity = entity.Name;
|
|
535
700
|
this._lastGridParamsViewEntity = this.viewEntity ?? null;
|
|
536
701
|
this._cachedGridParams = {
|
|
537
|
-
EntityName:
|
|
702
|
+
EntityName: entity.Name,
|
|
538
703
|
ViewEntity: this.viewEntity || undefined
|
|
539
704
|
};
|
|
540
705
|
}
|
|
@@ -701,91 +866,11 @@ export class EntityViewerComponent {
|
|
|
701
866
|
direction: this.effectiveConfig.defaultSortDirection ?? 'asc'
|
|
702
867
|
};
|
|
703
868
|
}
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (changes['config']) {
|
|
710
|
-
this.applyConfig();
|
|
711
|
-
}
|
|
712
|
-
if (changes['entity']) {
|
|
713
|
-
// Detect date fields for timeline support
|
|
714
|
-
this.detectDateFields();
|
|
715
|
-
if (this.entity && !this.records) {
|
|
716
|
-
// Reset state for new entity - synchronously clear all data and force change detection
|
|
717
|
-
// before starting the async load to prevent stale data display
|
|
718
|
-
this.resetPaginationState();
|
|
719
|
-
this.cdr.detectChanges();
|
|
720
|
-
this.loadData();
|
|
721
|
-
}
|
|
722
|
-
else if (!this.entity) {
|
|
723
|
-
this.internalRecords = [];
|
|
724
|
-
this.totalRecordCount = 0;
|
|
725
|
-
this.filteredRecordCount = 0;
|
|
726
|
-
this.resetPaginationState();
|
|
727
|
-
this.cdr.detectChanges();
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (changes['records'] && this.records) {
|
|
731
|
-
this.internalRecords = this.records;
|
|
732
|
-
this.totalRecordCount = this.records.length;
|
|
733
|
-
this.filteredRecordCount = this.records.length;
|
|
734
|
-
// Update timeline with new records
|
|
735
|
-
this.updateTimelineGroups();
|
|
736
|
-
}
|
|
737
|
-
// Timeline config is now handled by setter - no ngOnChanges handling needed
|
|
738
|
-
// Handle external filter text changes (from parent component)
|
|
739
|
-
if (changes['filterText']) {
|
|
740
|
-
const newFilter = this.filterText ?? '';
|
|
741
|
-
const oldFilter = this.debouncedFilterText;
|
|
742
|
-
this.internalFilterText = newFilter;
|
|
743
|
-
this.debouncedFilterText = newFilter;
|
|
744
|
-
// If server-side filtering and filter changed, reload from page 1
|
|
745
|
-
if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this.records) {
|
|
746
|
-
this.resetPaginationState();
|
|
747
|
-
this.loadData();
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
this.updateFilteredCount();
|
|
751
|
-
}
|
|
752
|
-
this.cdr.detectChanges();
|
|
753
|
-
}
|
|
754
|
-
// Handle external view mode changes
|
|
755
|
-
if (changes['viewMode'] && this.viewMode !== null) {
|
|
756
|
-
this.internalViewMode = this.viewMode;
|
|
757
|
-
}
|
|
758
|
-
// Handle external sort state changes
|
|
759
|
-
if (changes['sortState'] && this.sortState !== null) {
|
|
760
|
-
const oldSort = this.internalSortState;
|
|
761
|
-
this.internalSortState = this.sortState;
|
|
762
|
-
// If sort changed and using server-side sorting, reload
|
|
763
|
-
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
764
|
-
const sortChanged = !oldSort || !this.sortState ||
|
|
765
|
-
oldSort.field !== this.sortState.field ||
|
|
766
|
-
oldSort.direction !== this.sortState.direction;
|
|
767
|
-
if (sortChanged) {
|
|
768
|
-
this.resetPaginationState();
|
|
769
|
-
this.loadData();
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
// Handle viewEntity changes - reload data when view changes
|
|
774
|
-
if (changes['viewEntity']) {
|
|
775
|
-
if (this.entity && !this.records) {
|
|
776
|
-
// Apply view's sort state if available
|
|
777
|
-
if (this.viewEntity) {
|
|
778
|
-
const viewSortInfo = this.viewEntity.ViewSortInfo;
|
|
779
|
-
if (viewSortInfo && viewSortInfo.length > 0) {
|
|
780
|
-
this.internalSortState = {
|
|
781
|
-
field: viewSortInfo[0].field,
|
|
782
|
-
direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
this.resetPaginationState();
|
|
787
|
-
this.loadData();
|
|
788
|
-
}
|
|
869
|
+
// Mark as initialized - setters will now trigger data loading
|
|
870
|
+
this._initialized = true;
|
|
871
|
+
// If entity was set before initialization, load data now
|
|
872
|
+
if (this._entity && !this._records) {
|
|
873
|
+
this.loadData();
|
|
789
874
|
}
|
|
790
875
|
}
|
|
791
876
|
ngOnDestroy() {
|
|
@@ -810,8 +895,9 @@ export class EntityViewerComponent {
|
|
|
810
895
|
this.debouncedFilterText = filterText;
|
|
811
896
|
this.filterTextChange.emit(filterText);
|
|
812
897
|
// If server-side filtering and filter changed, reload from page 1
|
|
898
|
+
// Keep existing records visible during refresh for better UX
|
|
813
899
|
if (this.effectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.records) {
|
|
814
|
-
this.resetPaginationState();
|
|
900
|
+
this.resetPaginationState(false);
|
|
815
901
|
this.loadData();
|
|
816
902
|
}
|
|
817
903
|
else {
|
|
@@ -835,42 +921,54 @@ export class EntityViewerComponent {
|
|
|
835
921
|
}
|
|
836
922
|
/**
|
|
837
923
|
* Reset pagination state for a fresh load.
|
|
838
|
-
*
|
|
924
|
+
* When clearRecords is true (default), clears all record data - use for entity switches.
|
|
925
|
+
* When clearRecords is false, keeps existing records visible during refresh - use for sort/filter changes.
|
|
839
926
|
*/
|
|
840
|
-
resetPaginationState() {
|
|
927
|
+
resetPaginationState(clearRecords = true) {
|
|
841
928
|
this.pagination = {
|
|
842
929
|
currentPage: 0,
|
|
843
930
|
pageSize: this.effectiveConfig.pageSize,
|
|
844
|
-
totalRecords: 0,
|
|
931
|
+
totalRecords: clearRecords ? 0 : this.pagination.totalRecords,
|
|
845
932
|
hasMore: false,
|
|
846
933
|
isLoading: false
|
|
847
934
|
};
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
935
|
+
if (clearRecords) {
|
|
936
|
+
this.internalRecords = [];
|
|
937
|
+
this.totalRecordCount = 0;
|
|
938
|
+
this.filteredRecordCount = 0;
|
|
939
|
+
}
|
|
851
940
|
this.isInitialLoad = true;
|
|
852
941
|
}
|
|
853
942
|
// ========================================
|
|
854
943
|
// DATA LOADING
|
|
855
944
|
// ========================================
|
|
945
|
+
// Sequence counter for tracking load requests and detecting stale responses
|
|
946
|
+
_loadSequence = 0;
|
|
947
|
+
// Flag to indicate a reload is pending (requested while another load was in progress)
|
|
948
|
+
_pendingReload = false;
|
|
856
949
|
/**
|
|
857
950
|
* Load data for the current entity with server-side filtering/sorting/pagination
|
|
858
951
|
*/
|
|
859
952
|
async loadData() {
|
|
860
|
-
|
|
953
|
+
const entity = this.effectiveEntity;
|
|
954
|
+
if (!entity) {
|
|
861
955
|
this.internalRecords = [];
|
|
862
956
|
this.totalRecordCount = 0;
|
|
863
957
|
this.filteredRecordCount = 0;
|
|
864
958
|
return;
|
|
865
959
|
}
|
|
866
|
-
//
|
|
960
|
+
// Increment sequence to track this load request
|
|
961
|
+
const loadId = ++this._loadSequence;
|
|
962
|
+
// If a load is already in progress, mark that we need to reload when it completes
|
|
963
|
+
// This handles the case where view/filter changes occur during an active load
|
|
867
964
|
if (this.isLoading) {
|
|
965
|
+
this._pendingReload = true;
|
|
868
966
|
return;
|
|
869
967
|
}
|
|
870
968
|
this.isLoading = true;
|
|
871
969
|
this.pagination.isLoading = true;
|
|
872
970
|
this.loadingMessage = this.isInitialLoad
|
|
873
|
-
? `Loading ${
|
|
971
|
+
? `Loading ${entity.Name}...`
|
|
874
972
|
: 'Loading more records...';
|
|
875
973
|
this.cdr.detectChanges();
|
|
876
974
|
const startTime = Date.now();
|
|
@@ -893,14 +991,22 @@ export class EntityViewerComponent {
|
|
|
893
991
|
// The view's WhereClause is the "business filter" - UserSearchString is additive
|
|
894
992
|
const extraFilter = this.viewEntity?.WhereClause || undefined;
|
|
895
993
|
const result = await rv.RunView({
|
|
896
|
-
EntityName:
|
|
994
|
+
EntityName: entity.Name,
|
|
897
995
|
ResultType: 'entity_object',
|
|
898
996
|
MaxRows: config.pageSize,
|
|
899
997
|
StartRow: startRow,
|
|
900
998
|
OrderBy: orderBy,
|
|
901
999
|
ExtraFilter: extraFilter,
|
|
902
|
-
UserSearchString
|
|
1000
|
+
// Only use UserSearchString for regular text search, NOT for smart filters
|
|
1001
|
+
// Smart filters generate WhereClause via AI on the server, so the prompt text should not be passed as UserSearchString
|
|
1002
|
+
UserSearchString: config.serverSideFiltering && !this.viewEntity?.SmartFilterEnabled
|
|
1003
|
+
? this.debouncedFilterText || undefined
|
|
1004
|
+
: undefined
|
|
903
1005
|
});
|
|
1006
|
+
// Check if this load is still the current one (detect stale responses)
|
|
1007
|
+
if (loadId !== this._loadSequence) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
904
1010
|
if (result.Success) {
|
|
905
1011
|
// Append or replace records based on whether this is initial load
|
|
906
1012
|
if (this.isInitialLoad) {
|
|
@@ -928,7 +1034,6 @@ export class EntityViewerComponent {
|
|
|
928
1034
|
this.updateTimelineGroups();
|
|
929
1035
|
}
|
|
930
1036
|
else {
|
|
931
|
-
console.error('Failed to load records:', result.ErrorMessage);
|
|
932
1037
|
if (this.isInitialLoad) {
|
|
933
1038
|
this.internalRecords = [];
|
|
934
1039
|
}
|
|
@@ -937,7 +1042,6 @@ export class EntityViewerComponent {
|
|
|
937
1042
|
}
|
|
938
1043
|
}
|
|
939
1044
|
catch (error) {
|
|
940
|
-
console.error('Error loading records:', error);
|
|
941
1045
|
if (this.isInitialLoad) {
|
|
942
1046
|
this.internalRecords = [];
|
|
943
1047
|
}
|
|
@@ -949,6 +1053,13 @@ export class EntityViewerComponent {
|
|
|
949
1053
|
this.pagination.isLoading = false;
|
|
950
1054
|
this.isInitialLoad = false;
|
|
951
1055
|
this.cdr.detectChanges();
|
|
1056
|
+
// If a reload was requested while we were loading, trigger it now
|
|
1057
|
+
if (this._pendingReload) {
|
|
1058
|
+
this._pendingReload = false;
|
|
1059
|
+
this.resetPaginationState();
|
|
1060
|
+
// Use setTimeout to break the call stack and allow Angular to process
|
|
1061
|
+
setTimeout(() => this.loadData(), 0);
|
|
1062
|
+
}
|
|
952
1063
|
}
|
|
953
1064
|
}
|
|
954
1065
|
/**
|
|
@@ -963,10 +1074,11 @@ export class EntityViewerComponent {
|
|
|
963
1074
|
}
|
|
964
1075
|
/**
|
|
965
1076
|
* Refresh data (re-load from server, starting at page 1)
|
|
1077
|
+
* Keeps existing records visible during refresh for better UX
|
|
966
1078
|
*/
|
|
967
1079
|
refresh() {
|
|
968
1080
|
if (!this.records) {
|
|
969
|
-
this.resetPaginationState();
|
|
1081
|
+
this.resetPaginationState(false);
|
|
970
1082
|
this.loadData();
|
|
971
1083
|
}
|
|
972
1084
|
}
|
|
@@ -1012,12 +1124,13 @@ export class EntityViewerComponent {
|
|
|
1012
1124
|
this.internalSortState = event.sort;
|
|
1013
1125
|
this.sortChanged.emit(event);
|
|
1014
1126
|
// If server-side sorting, reload from page 1
|
|
1127
|
+
// Keep existing records visible during refresh for better UX
|
|
1015
1128
|
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
1016
1129
|
const sortChanged = !oldSort || !event.sort ||
|
|
1017
1130
|
oldSort.field !== event.sort?.field ||
|
|
1018
1131
|
oldSort.direction !== event.sort?.direction;
|
|
1019
1132
|
if (sortChanged) {
|
|
1020
|
-
this.resetPaginationState();
|
|
1133
|
+
this.resetPaginationState(false);
|
|
1021
1134
|
this.loadData();
|
|
1022
1135
|
}
|
|
1023
1136
|
}
|
|
@@ -1057,11 +1170,12 @@ export class EntityViewerComponent {
|
|
|
1057
1170
|
* Maps to recordSelected event for parent components
|
|
1058
1171
|
*/
|
|
1059
1172
|
onDataGridRowClick(event) {
|
|
1060
|
-
|
|
1173
|
+
const entity = this.effectiveEntity;
|
|
1174
|
+
if (!entity || !event.row)
|
|
1061
1175
|
return;
|
|
1062
1176
|
this.recordSelected.emit({
|
|
1063
1177
|
record: event.row,
|
|
1064
|
-
entity:
|
|
1178
|
+
entity: entity,
|
|
1065
1179
|
compositeKey: event.row.PrimaryKey
|
|
1066
1180
|
});
|
|
1067
1181
|
}
|
|
@@ -1070,11 +1184,12 @@ export class EntityViewerComponent {
|
|
|
1070
1184
|
* Maps to recordOpened event for parent components
|
|
1071
1185
|
*/
|
|
1072
1186
|
onDataGridRowDoubleClick(event) {
|
|
1073
|
-
|
|
1187
|
+
const entity = this.effectiveEntity;
|
|
1188
|
+
if (!entity || !event.row)
|
|
1074
1189
|
return;
|
|
1075
1190
|
this.recordOpened.emit({
|
|
1076
1191
|
record: event.row,
|
|
1077
|
-
entity:
|
|
1192
|
+
entity: entity,
|
|
1078
1193
|
compositeKey: event.row.PrimaryKey
|
|
1079
1194
|
});
|
|
1080
1195
|
}
|
|
@@ -1093,8 +1208,9 @@ export class EntityViewerComponent {
|
|
|
1093
1208
|
this.internalSortState = newSort;
|
|
1094
1209
|
this.sortChanged.emit({ sort: newSort });
|
|
1095
1210
|
// If server-side sorting, reload from page 1
|
|
1211
|
+
// Keep existing records visible during refresh for better UX
|
|
1096
1212
|
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
1097
|
-
this.resetPaginationState();
|
|
1213
|
+
this.resetPaginationState(false);
|
|
1098
1214
|
this.loadData();
|
|
1099
1215
|
}
|
|
1100
1216
|
}
|
|
@@ -1153,10 +1269,11 @@ export class EntityViewerComponent {
|
|
|
1153
1269
|
*/
|
|
1154
1270
|
onTimelineEventClick(event) {
|
|
1155
1271
|
const record = event.event.entity;
|
|
1156
|
-
|
|
1272
|
+
const entity = this.effectiveEntity;
|
|
1273
|
+
if (record && entity) {
|
|
1157
1274
|
this.recordSelected.emit({
|
|
1158
1275
|
record,
|
|
1159
|
-
entity:
|
|
1276
|
+
entity: entity,
|
|
1160
1277
|
compositeKey: record.PrimaryKey
|
|
1161
1278
|
});
|
|
1162
1279
|
}
|
|
@@ -1395,7 +1512,7 @@ export class EntityViewerComponent {
|
|
|
1395
1512
|
} if (rf & 2) {
|
|
1396
1513
|
let _t;
|
|
1397
1514
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dataGridRef = _t.first);
|
|
1398
|
-
} }, 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", timelineConfig: "timelineConfig", showGridToolbar: "showGridToolbar", gridToolbarConfig: "gridToolbarConfig", gridSelectionMode: "gridSelectionMode", showAddToListButton: "showAddToListButton" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", dataLoaded: "dataLoaded", viewModeChange: "viewModeChange", filterTextChange: "filterTextChange", filteredCountChanged: "filteredCountChanged", sortChanged: "sortChanged", gridStateChanged: "gridStateChanged", timelineConfigChange: "timelineConfigChange", addRequested: "addRequested", deleteRequested: "deleteRequested", refreshRequested: "refreshRequested", exportRequested: "exportRequested", addToListRequested: "addToListRequested", selectionChanged: "selectionChanged" },
|
|
1515
|
+
} }, 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", timelineConfig: "timelineConfig", showGridToolbar: "showGridToolbar", gridToolbarConfig: "gridToolbarConfig", gridSelectionMode: "gridSelectionMode", showAddToListButton: "showAddToListButton" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", dataLoaded: "dataLoaded", viewModeChange: "viewModeChange", filterTextChange: "filterTextChange", filteredCountChanged: "filteredCountChanged", sortChanged: "sortChanged", gridStateChanged: "gridStateChanged", timelineConfigChange: "timelineConfigChange", addRequested: "addRequested", deleteRequested: "deleteRequested", refreshRequested: "refreshRequested", exportRequested: "exportRequested", addToListRequested: "addToListRequested", selectionChanged: "selectionChanged" }, decls: 19, vars: 38, consts: [[1, "entity-viewer-container"], [1, "viewer-header"], [1, "viewer-content"], [1, "loading-container", 3, "hidden"], ["size", "medium", 3, "text"], [1, "loading-overlay", 3, "hidden"], ["size", "small", 3, "text"], [1, "empty-state", 3, "hidden"], [1, "fa-solid", "fa-database"], [1, "fa-solid", "fa-inbox"], [3, "AfterRowClick", "AfterRowDoubleClick", "AfterSort", "GridStateChanged", "SelectionChange", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "ExportButtonClick", "AddToListRequested", "hidden", "Data", "Params", "FilterText", "GridState", "Height", "ShowToolbar", "ToolbarConfig", "SelectionMode", "ShowAddToListButton", "AllowLoad"], [3, "recordSelected", "recordOpened", "hidden", "entity", "records", "selectedRecordId", "cardTemplate", "hiddenFieldMatches", "filterText"], [3, "afterEventClick", "hidden", "groups", "orientation", "layout", "sortOrder", "segmentGrouping", "segmentsCollapsible", "segmentsDefaultExpanded", "selectedEventId"], [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"], ["title", "Timeline View", 1, "toggle-btn", 3, "active"], ["title", "Timeline View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-timeline"], [1, "timeline-date-selector"], [1, "fa-solid", "fa-calendar-days"], [1, "date-field-label"], [1, "date-field-select", 3, "value"], [1, "timeline-orientation-toggle"], [1, "toggle-btn", 3, "click", "title"], [1, "timeline-sort-toggle"], [1, "date-field-select", 3, "change", "value"], [3, "value"], [3, "loadMore", "pagination", "loadedRecordCount"]], template: function EntityViewerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1399
1516
|
i0.ɵɵelementStart(0, "div", 0);
|
|
1400
1517
|
i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Template, 5, 4, "div", 1);
|
|
1401
1518
|
i0.ɵɵelementStart(2, "div", 2)(3, "div", 3);
|
|
@@ -1438,26 +1555,26 @@ export class EntityViewerComponent {
|
|
|
1438
1555
|
i0.ɵɵadvance();
|
|
1439
1556
|
i0.ɵɵproperty("text", ctx.loadingMessage);
|
|
1440
1557
|
i0.ɵɵadvance();
|
|
1441
|
-
i0.ɵɵproperty("hidden", !!ctx.
|
|
1558
|
+
i0.ɵɵproperty("hidden", !!ctx.effectiveEntity);
|
|
1442
1559
|
i0.ɵɵadvance(4);
|
|
1443
|
-
i0.ɵɵproperty("hidden", !ctx.
|
|
1560
|
+
i0.ɵɵproperty("hidden", !ctx.effectiveEntity || ctx.filteredRecords.length > 0 || ctx.isLoading);
|
|
1444
1561
|
i0.ɵɵadvance(3);
|
|
1445
1562
|
i0.ɵɵtextInterpolate(ctx.debouncedFilterText ? "No matching records" : "No records found");
|
|
1446
1563
|
i0.ɵɵadvance();
|
|
1447
|
-
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "grid" || !ctx.
|
|
1564
|
+
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "grid" || !ctx.effectiveEntity)("Data", ctx.filteredRecords)("Params", ctx.gridParams)("FilterText", ctx.debouncedFilterText)("GridState", ctx.gridState)("Height", "auto")("ShowToolbar", ctx.showGridToolbar)("ToolbarConfig", ctx.effectiveGridToolbarConfig)("SelectionMode", ctx.gridSelectionMode)("ShowAddToListButton", ctx.showAddToListButton)("AllowLoad", false);
|
|
1448
1565
|
i0.ɵɵadvance();
|
|
1449
|
-
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "cards" || !ctx.
|
|
1566
|
+
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "cards" || !ctx.effectiveEntity)("entity", ctx.effectiveEntity)("records", ctx.filteredRecords)("selectedRecordId", ctx.selectedRecordId)("cardTemplate", ctx.cardTemplate)("hiddenFieldMatches", ctx.hiddenFieldMatches)("filterText", ctx.debouncedFilterText);
|
|
1450
1567
|
i0.ɵɵadvance();
|
|
1451
1568
|
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "timeline" || !ctx.hasDateFields)("groups", ctx.timelineGroups)("orientation", ctx.timelineOrientation)("layout", ctx.timelineOrientation === "vertical" ? "alternating" : "single")("sortOrder", ctx.timelineSortOrder)("segmentGrouping", ctx.timelineSegmentGrouping)("segmentsCollapsible", true)("segmentsDefaultExpanded", true)("selectedEventId", ctx.timelineSelectedEventId);
|
|
1452
1569
|
i0.ɵɵadvance();
|
|
1453
|
-
i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.
|
|
1454
|
-
} }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i2.LoadingComponent, i3.TimelineComponent, i4.EntityCardsComponent, i5.PaginationComponent, i6.EntityDataGridComponent, i7.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n\n\nmj-pagination[_ngcontent-%COMP%] {\n flex-shrink: 0;\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.timeline-date-selector[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #9e9e9e;\n}\n\n.date-field-label[_ngcontent-%COMP%] {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select[_ngcontent-%COMP%] {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select[_ngcontent-%COMP%]:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n\n\n.timeline-orientation-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\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.loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\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}\n\n\n\nmj-entity-cards[hidden][_ngcontent-%COMP%], \nmj-timeline[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\nmj-entity-cards[_ngcontent-%COMP%]:not([hidden]), \nmj-timeline[_ngcontent-%COMP%]:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}"] });
|
|
1570
|
+
i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.effectiveEntity && (ctx.pagination.hasMore || ctx.pagination.totalRecords > ctx.effectiveConfig.pageSize) ? 18 : -1);
|
|
1571
|
+
} }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i2.LoadingComponent, i3.TimelineComponent, i4.EntityCardsComponent, i5.PaginationComponent, i6.EntityDataGridComponent, i7.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n\n\nmj-pagination[_ngcontent-%COMP%] {\n flex-shrink: 0;\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.timeline-date-selector[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #9e9e9e;\n}\n\n.date-field-label[_ngcontent-%COMP%] {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select[_ngcontent-%COMP%] {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select[_ngcontent-%COMP%]:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n\n\n.timeline-orientation-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\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.loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n\n\n.loading-container[hidden][_ngcontent-%COMP%], \n.loading-overlay[hidden][_ngcontent-%COMP%] {\n display: none !important;\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}\n\n\n\nmj-entity-cards[hidden][_ngcontent-%COMP%], \nmj-timeline[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\nmj-entity-cards[_ngcontent-%COMP%]:not([hidden]), \nmj-timeline[_ngcontent-%COMP%]:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}"] });
|
|
1455
1572
|
}
|
|
1456
1573
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
|
|
1457
1574
|
type: Component,
|
|
1458
1575
|
args: [{ selector: 'mj-entity-viewer', host: {
|
|
1459
1576
|
'style': 'display: block; height: 100%;'
|
|
1460
|
-
}, 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 @if (hasDateFields) {\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'timeline'\"\n (click)=\"setViewMode('timeline')\"\n title=\"Timeline View\">\n <i class=\"fa-solid fa-timeline\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Timeline Controls (only shown when timeline is active) -->\n @if (effectiveViewMode === 'timeline' && hasDateFields) {\n <!-- Date Field Selector -->\n <div class=\"timeline-date-selector\">\n <i class=\"fa-solid fa-calendar-days\"></i>\n @if (availableDateFields.length === 1) {\n <span class=\"date-field-label\">{{ selectedDateFieldDisplayName }}</span>\n } @else {\n <select\n class=\"date-field-select\"\n [value]=\"selectedTimelineDateField\"\n (change)=\"setTimelineDateField($any($event.target).value)\">\n @for (field of availableDateFields; track field.Name) {\n <option [value]=\"field.Name\">{{ field.DisplayNameOrName }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Orientation Toggle -->\n <div class=\"timeline-orientation-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineOrientation()\"\n [title]=\"timelineOrientation === 'vertical' ? 'Switch to Horizontal' : 'Switch to Vertical'\">\n <i [class]=\"timelineOrientation === 'vertical' ? 'fa-solid fa-ellipsis-vertical' : 'fa-solid fa-ellipsis'\"></i>\n </button>\n </div>\n\n <!-- Sort Order Toggle -->\n <div class=\"timeline-sort-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineSortOrder()\"\n [title]=\"timelineSortOrder === 'desc' ? 'Showing Newest First (click for Oldest First)' : 'Showing Oldest First (click for Newest First)'\">\n <i [class]=\"timelineSortOrder === 'desc' ? 'fa-solid fa-arrow-down-wide-short' : 'fa-solid fa-arrow-up-wide-short'\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Content -->\n <div class=\"viewer-content\">\n <!-- Loading container - full page when no data exists -->\n <div class=\"loading-container\" [hidden]=\"!(isLoading && filteredRecords.length === 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"medium\"></mj-loading>\n </div>\n\n <!-- Loading overlay - shown on top of content when loading with existing data -->\n <div class=\"loading-overlay\" [hidden]=\"!(isLoading && filteredRecords.length > 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"small\"></mj-loading>\n </div>\n\n <!-- Empty state: no entity selected -->\n <div class=\"empty-state\" [hidden]=\"!!entity\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n\n <!-- Empty state: no records found -->\n <div class=\"empty-state\" [hidden]=\"!entity || filteredRecords.length > 0 || isLoading\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ debouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n\n <!-- Grid View - always rendered, visibility controlled by hidden -->\n <mj-entity-data-grid\n [hidden]=\"effectiveViewMode !== 'grid' || !entity\"\n [Data]=\"filteredRecords\"\n [Params]=\"gridParams\"\n [FilterText]=\"debouncedFilterText\"\n [GridState]=\"gridState\"\n [Height]=\"'auto'\"\n [ShowToolbar]=\"showGridToolbar\"\n [ToolbarConfig]=\"effectiveGridToolbarConfig\"\n [SelectionMode]=\"gridSelectionMode\"\n [ShowAddToListButton]=\"showAddToListButton\"\n [AllowLoad]=\"false\"\n (AfterRowClick)=\"onDataGridRowClick($event)\"\n (AfterRowDoubleClick)=\"onDataGridRowDoubleClick($event)\"\n (AfterSort)=\"onDataGridSortChanged($event)\"\n (GridStateChanged)=\"onGridStateChanged($event)\"\n (SelectionChange)=\"onGridSelectionChange($event)\"\n (NewButtonClick)=\"onGridAddRequested()\"\n (RefreshButtonClick)=\"onGridRefreshRequested()\"\n (DeleteButtonClick)=\"onGridDeleteRequested($event)\"\n (ExportButtonClick)=\"onGridExportRequested()\"\n (AddToListRequested)=\"onGridAddToListRequested($event)\">\n </mj-entity-data-grid>\n\n <!-- Cards View - always rendered, visibility controlled by hidden -->\n <mj-entity-cards\n [hidden]=\"effectiveViewMode !== 'cards' || !entity\"\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 <!-- Timeline View - always rendered when date fields exist, visibility controlled by hidden -->\n <mj-timeline\n [hidden]=\"effectiveViewMode !== 'timeline' || !hasDateFields\"\n [groups]=\"timelineGroups\"\n [orientation]=\"timelineOrientation\"\n [layout]=\"timelineOrientation === 'vertical' ? 'alternating' : 'single'\"\n [sortOrder]=\"timelineSortOrder\"\n [segmentGrouping]=\"timelineSegmentGrouping\"\n [segmentsCollapsible]=\"true\"\n [segmentsDefaultExpanded]=\"true\"\n [selectedEventId]=\"timelineSelectedEventId\"\n (afterEventClick)=\"onTimelineEventClick($event)\">\n </mj-timeline>\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 height: 100%;\n background: #fafafa;\n}\n\n/* Pagination footer - always visible at bottom */\nmj-pagination {\n flex-shrink: 0;\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/* Timeline Date Field Selector */\n.timeline-date-selector {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector i {\n color: #9e9e9e;\n}\n\n.date-field-label {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select:focus {\n border-color: #1976d2;\n}\n\n/* Timeline Orientation Toggle */\n.timeline-orientation-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n/* Loading State - positioned as overlay when data exists */\n.loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n/* Loading overlay when shown on top of content */\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\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\n/* Hidden components should not display - ensure [hidden] attribute works properly */\nmj-entity-cards[hidden],\nmj-timeline[hidden] {\n display: none !important;\n}\n\n/* Visible view components should fill available space */\nmj-entity-cards:not([hidden]),\nmj-timeline:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n"] }]
|
|
1577
|
+
}, 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 && effectiveEntity) {\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 @if (hasDateFields) {\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'timeline'\"\n (click)=\"setViewMode('timeline')\"\n title=\"Timeline View\">\n <i class=\"fa-solid fa-timeline\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Timeline Controls (only shown when timeline is active) -->\n @if (effectiveViewMode === 'timeline' && hasDateFields) {\n <!-- Date Field Selector -->\n <div class=\"timeline-date-selector\">\n <i class=\"fa-solid fa-calendar-days\"></i>\n @if (availableDateFields.length === 1) {\n <span class=\"date-field-label\">{{ selectedDateFieldDisplayName }}</span>\n } @else {\n <select\n class=\"date-field-select\"\n [value]=\"selectedTimelineDateField\"\n (change)=\"setTimelineDateField($any($event.target).value)\">\n @for (field of availableDateFields; track field.Name) {\n <option [value]=\"field.Name\">{{ field.DisplayNameOrName }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Orientation Toggle -->\n <div class=\"timeline-orientation-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineOrientation()\"\n [title]=\"timelineOrientation === 'vertical' ? 'Switch to Horizontal' : 'Switch to Vertical'\">\n <i [class]=\"timelineOrientation === 'vertical' ? 'fa-solid fa-ellipsis-vertical' : 'fa-solid fa-ellipsis'\"></i>\n </button>\n </div>\n\n <!-- Sort Order Toggle -->\n <div class=\"timeline-sort-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineSortOrder()\"\n [title]=\"timelineSortOrder === 'desc' ? 'Showing Newest First (click for Oldest First)' : 'Showing Oldest First (click for Newest First)'\">\n <i [class]=\"timelineSortOrder === 'desc' ? 'fa-solid fa-arrow-down-wide-short' : 'fa-solid fa-arrow-up-wide-short'\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Content -->\n <div class=\"viewer-content\">\n <!-- Loading container - full page when no data exists -->\n <div class=\"loading-container\" [hidden]=\"!(isLoading && filteredRecords.length === 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"medium\"></mj-loading>\n </div>\n\n <!-- Loading overlay - shown on top of content when loading with existing data -->\n <div class=\"loading-overlay\" [hidden]=\"!(isLoading && filteredRecords.length > 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"small\"></mj-loading>\n </div>\n\n <!-- Empty state: no entity selected -->\n <div class=\"empty-state\" [hidden]=\"!!effectiveEntity\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n\n <!-- Empty state: no records found -->\n <div class=\"empty-state\" [hidden]=\"!effectiveEntity || filteredRecords.length > 0 || isLoading\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ debouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n\n <!-- Grid View - always rendered, visibility controlled by hidden -->\n <mj-entity-data-grid\n [hidden]=\"effectiveViewMode !== 'grid' || !effectiveEntity\"\n [Data]=\"filteredRecords\"\n [Params]=\"gridParams\"\n [FilterText]=\"debouncedFilterText\"\n [GridState]=\"gridState\"\n [Height]=\"'auto'\"\n [ShowToolbar]=\"showGridToolbar\"\n [ToolbarConfig]=\"effectiveGridToolbarConfig\"\n [SelectionMode]=\"gridSelectionMode\"\n [ShowAddToListButton]=\"showAddToListButton\"\n [AllowLoad]=\"false\"\n (AfterRowClick)=\"onDataGridRowClick($event)\"\n (AfterRowDoubleClick)=\"onDataGridRowDoubleClick($event)\"\n (AfterSort)=\"onDataGridSortChanged($event)\"\n (GridStateChanged)=\"onGridStateChanged($event)\"\n (SelectionChange)=\"onGridSelectionChange($event)\"\n (NewButtonClick)=\"onGridAddRequested()\"\n (RefreshButtonClick)=\"onGridRefreshRequested()\"\n (DeleteButtonClick)=\"onGridDeleteRequested($event)\"\n (ExportButtonClick)=\"onGridExportRequested()\"\n (AddToListRequested)=\"onGridAddToListRequested($event)\">\n </mj-entity-data-grid>\n\n <!-- Cards View - always rendered, visibility controlled by hidden -->\n <mj-entity-cards\n [hidden]=\"effectiveViewMode !== 'cards' || !effectiveEntity\"\n [entity]=\"effectiveEntity\"\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 <!-- Timeline View - always rendered when date fields exist, visibility controlled by hidden -->\n <mj-timeline\n [hidden]=\"effectiveViewMode !== 'timeline' || !hasDateFields\"\n [groups]=\"timelineGroups\"\n [orientation]=\"timelineOrientation\"\n [layout]=\"timelineOrientation === 'vertical' ? 'alternating' : 'single'\"\n [sortOrder]=\"timelineSortOrder\"\n [segmentGrouping]=\"timelineSegmentGrouping\"\n [segmentsCollapsible]=\"true\"\n [segmentsDefaultExpanded]=\"true\"\n [selectedEventId]=\"timelineSelectedEventId\"\n (afterEventClick)=\"onTimelineEventClick($event)\">\n </mj-timeline>\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 && effectiveEntity && (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 height: 100%;\n background: #fafafa;\n}\n\n/* Pagination footer - always visible at bottom */\nmj-pagination {\n flex-shrink: 0;\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/* Timeline Date Field Selector */\n.timeline-date-selector {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector i {\n color: #9e9e9e;\n}\n\n.date-field-label {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select:focus {\n border-color: #1976d2;\n}\n\n/* Timeline Orientation Toggle */\n.timeline-orientation-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n/* Loading State - full-page centered loading for initial load when no data exists */\n.loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n/* Loading overlay - semi-transparent overlay on top of existing content during refresh */\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Ensure [hidden] attribute works properly on loading elements */\n.loading-container[hidden],\n.loading-overlay[hidden] {\n display: none !important;\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\n/* Hidden components should not display - ensure [hidden] attribute works properly */\nmj-entity-cards[hidden],\nmj-timeline[hidden] {\n display: none !important;\n}\n\n/* Visible view components should fill available space */\nmj-entity-cards:not([hidden]),\nmj-timeline:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n"] }]
|
|
1461
1578
|
}], () => [{ type: i0.ChangeDetectorRef }], { entity: [{
|
|
1462
1579
|
type: Input
|
|
1463
1580
|
}], records: [{
|
|
@@ -1524,5 +1641,5 @@ export class EntityViewerComponent {
|
|
|
1524
1641
|
type: ViewChild,
|
|
1525
1642
|
args: [EntityDataGridComponent]
|
|
1526
1643
|
}] }); })();
|
|
1527
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent" }); })();
|
|
1644
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 76 }); })();
|
|
1528
1645
|
//# sourceMappingURL=entity-viewer.component.js.map
|