@memberjunction/ng-entity-viewer 2.133.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.
Files changed (23) hide show
  1. package/dist/lib/entity-cards/entity-cards.component.js +1 -1
  2. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
  3. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +10 -1
  4. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  5. package/dist/lib/entity-data-grid/entity-data-grid.component.js +87 -35
  6. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  7. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +1 -1
  8. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
  9. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +89 -13
  10. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  11. package/dist/lib/entity-viewer/entity-viewer.component.js +317 -136
  12. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  13. package/dist/lib/pagination/pagination.component.js +1 -1
  14. package/dist/lib/pagination/pagination.component.js.map +1 -1
  15. package/dist/lib/pill/pill.component.js +1 -1
  16. package/dist/lib/pill/pill.component.js.map +1 -1
  17. package/dist/lib/view-config-panel/view-config-panel.component.d.ts +87 -2
  18. package/dist/lib/view-config-panel/view-config-panel.component.d.ts.map +1 -1
  19. package/dist/lib/view-config-panel/view-config-panel.component.js +720 -426
  20. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  21. package/dist/module.js +1 -1
  22. package/dist/module.js.map +1 -1
  23. package/package.json +15 -15
@@ -1,9 +1,10 @@
1
- import { Component, Input, Output, EventEmitter } from '@angular/core';
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
+ import { EntityDataGridComponent } from '../entity-data-grid/entity-data-grid.component';
7
8
  import * as i0 from "@angular/core";
8
9
  import * as i1 from "@angular/forms";
9
10
  import * as i2 from "@memberjunction/ng-shared-generic";
@@ -164,7 +165,7 @@ function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
164
165
  i0.ɵɵadvance();
165
166
  i0.ɵɵconditional(ctx_r1.effectiveConfig.showFilter ? 1 : -1);
166
167
  i0.ɵɵadvance();
167
- i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.entity ? 2 : -1);
168
+ i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.effectiveEntity ? 2 : -1);
168
169
  i0.ɵɵadvance();
169
170
  i0.ɵɵconditional(ctx_r1.effectiveConfig.showViewModeToggle ? 3 : -1);
170
171
  i0.ɵɵadvance();
@@ -215,20 +216,70 @@ function EntityViewerComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
215
216
  export class EntityViewerComponent {
216
217
  cdr;
217
218
  // ========================================
218
- // INPUTS
219
+ // INPUTS (using getter/setter pattern)
219
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;
220
230
  /**
221
231
  * The entity to display records for
222
232
  */
223
- entity = null;
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
+ }
224
257
  /**
225
258
  * Pre-loaded records (optional - if not provided, component loads data)
226
259
  */
227
- records = null;
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
+ }
228
273
  /**
229
274
  * Configuration options for the viewer
230
275
  */
231
- config = {};
276
+ get config() {
277
+ return this._config;
278
+ }
279
+ set config(value) {
280
+ this._config = value;
281
+ this.applyConfig();
282
+ }
232
283
  /**
233
284
  * Currently selected record ID (primary key string)
234
285
  */
@@ -237,16 +288,67 @@ export class EntityViewerComponent {
237
288
  * External view mode - allows parent to control view mode
238
289
  * Supports two-way binding: [(viewMode)]="state.viewMode"
239
290
  */
240
- viewMode = null;
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
+ }
241
300
  /**
242
301
  * External filter text - allows parent to control filter
243
302
  * Supports two-way binding: [(filterText)]="state.filterText"
244
303
  */
245
- filterText = null;
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
+ }
246
326
  /**
247
327
  * External sort state - allows parent to control sorting
248
328
  */
249
- sortState = null;
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
+ }
250
352
  /**
251
353
  * Custom grid column definitions
252
354
  */
@@ -260,7 +362,26 @@ export class EntityViewerComponent {
260
362
  * When provided, the component will use the view's WhereClause, GridState, SortState, etc.
261
363
  * The view's filter is additive - UserSearchString is applied ON TOP of the view's WhereClause
262
364
  */
263
- viewEntity = null;
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
+ }
264
385
  /**
265
386
  * Grid state configuration from a User View
266
387
  * Controls column visibility, widths, order, and sort settings
@@ -284,13 +405,12 @@ export class EntityViewerComponent {
284
405
  prev.segmentGrouping === value.segmentGrouping);
285
406
  if (!isEqual) {
286
407
  this._timelineConfig = value;
287
- if (value && this.entity) {
408
+ if (value && this._entity) {
288
409
  this.configureTimeline();
289
410
  this.cdr.markForCheck();
290
411
  }
291
412
  }
292
413
  }
293
- _timelineConfig = null;
294
414
  /**
295
415
  * Whether to show the grid toolbar.
296
416
  * When false, the grid is displayed without its own toolbar - useful when
@@ -308,6 +428,12 @@ export class EntityViewerComponent {
308
428
  * @default 'single'
309
429
  */
310
430
  gridSelectionMode = 'single';
431
+ /**
432
+ * Show the "Add to List" button in the grid toolbar.
433
+ * Requires gridSelectionMode to be 'multiple' for best UX.
434
+ * @default false
435
+ */
436
+ showAddToListButton = false;
311
437
  // ========================================
312
438
  // OUTPUTS
313
439
  // ========================================
@@ -364,6 +490,16 @@ export class EntityViewerComponent {
364
490
  * Emitted when the Export button is clicked in the grid toolbar
365
491
  */
366
492
  exportRequested = new EventEmitter();
493
+ /**
494
+ * Emitted when the Add to List button is clicked in the grid toolbar.
495
+ * Parent components should handle this to show the list management dialog.
496
+ */
497
+ addToListRequested = new EventEmitter();
498
+ /**
499
+ * Emitted when grid selection changes.
500
+ * Parent components can use this to track selected records for their own toolbar buttons.
501
+ */
502
+ selectionChanged = new EventEmitter();
367
503
  // ========================================
368
504
  // INTERNAL STATE
369
505
  // ========================================
@@ -428,12 +564,69 @@ export class EntityViewerComponent {
428
564
  filterInput$ = new Subject();
429
565
  /** Track if this is the first load (vs. load more) */
430
566
  isInitialLoad = true;
567
+ /** Reference to the data grid component for flushing pending changes */
568
+ dataGridRef;
431
569
  constructor(cdr) {
432
570
  this.cdr = cdr;
433
571
  }
434
572
  // ========================================
573
+ // PUBLIC METHODS
574
+ // ========================================
575
+ /**
576
+ * Ensures any pending grid state changes are saved immediately without waiting for debounce.
577
+ * Call this before switching views or entities to ensure changes are saved.
578
+ */
579
+ EnsurePendingChangesSaved() {
580
+ this.dataGridRef?.EnsurePendingChangesSaved();
581
+ }
582
+ // ========================================
435
583
  // COMPUTED PROPERTIES
436
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
+ }
437
630
  /**
438
631
  * Get the effective view mode (external or internal)
439
632
  */
@@ -495,17 +688,18 @@ export class EntityViewerComponent {
495
688
  * which would cause the grid to reinitialize
496
689
  */
497
690
  get gridParams() {
498
- if (!this.entity) {
691
+ const entity = this.effectiveEntity;
692
+ if (!entity) {
499
693
  return null;
500
694
  }
501
695
  // Check if we need to recreate the params object
502
- const entityChanged = this._lastGridParamsEntity !== this.entity.Name;
696
+ const entityChanged = this._lastGridParamsEntity !== entity.Name;
503
697
  const viewEntityChanged = this._lastGridParamsViewEntity !== this.viewEntity;
504
698
  if (entityChanged || viewEntityChanged || !this._cachedGridParams) {
505
- this._lastGridParamsEntity = this.entity.Name;
699
+ this._lastGridParamsEntity = entity.Name;
506
700
  this._lastGridParamsViewEntity = this.viewEntity ?? null;
507
701
  this._cachedGridParams = {
508
- EntityName: this.entity.Name,
702
+ EntityName: entity.Name,
509
703
  ViewEntity: this.viewEntity || undefined
510
704
  };
511
705
  }
@@ -672,91 +866,11 @@ export class EntityViewerComponent {
672
866
  direction: this.effectiveConfig.defaultSortDirection ?? 'asc'
673
867
  };
674
868
  }
675
- // Note: We don't call loadData() here because ngOnChanges runs before ngOnInit
676
- // and already handles the initial entity binding. Calling loadData() here would
677
- // result in duplicate data loading (200 records instead of 100).
678
- }
679
- ngOnChanges(changes) {
680
- if (changes['config']) {
681
- this.applyConfig();
682
- }
683
- if (changes['entity']) {
684
- // Detect date fields for timeline support
685
- this.detectDateFields();
686
- if (this.entity && !this.records) {
687
- // Reset state for new entity - synchronously clear all data and force change detection
688
- // before starting the async load to prevent stale data display
689
- this.resetPaginationState();
690
- this.cdr.detectChanges();
691
- this.loadData();
692
- }
693
- else if (!this.entity) {
694
- this.internalRecords = [];
695
- this.totalRecordCount = 0;
696
- this.filteredRecordCount = 0;
697
- this.resetPaginationState();
698
- this.cdr.detectChanges();
699
- }
700
- }
701
- if (changes['records'] && this.records) {
702
- this.internalRecords = this.records;
703
- this.totalRecordCount = this.records.length;
704
- this.filteredRecordCount = this.records.length;
705
- // Update timeline with new records
706
- this.updateTimelineGroups();
707
- }
708
- // Timeline config is now handled by setter - no ngOnChanges handling needed
709
- // Handle external filter text changes (from parent component)
710
- if (changes['filterText']) {
711
- const newFilter = this.filterText ?? '';
712
- const oldFilter = this.debouncedFilterText;
713
- this.internalFilterText = newFilter;
714
- this.debouncedFilterText = newFilter;
715
- // If server-side filtering and filter changed, reload from page 1
716
- if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this.records) {
717
- this.resetPaginationState();
718
- this.loadData();
719
- }
720
- else {
721
- this.updateFilteredCount();
722
- }
723
- this.cdr.detectChanges();
724
- }
725
- // Handle external view mode changes
726
- if (changes['viewMode'] && this.viewMode !== null) {
727
- this.internalViewMode = this.viewMode;
728
- }
729
- // Handle external sort state changes
730
- if (changes['sortState'] && this.sortState !== null) {
731
- const oldSort = this.internalSortState;
732
- this.internalSortState = this.sortState;
733
- // If sort changed and using server-side sorting, reload
734
- if (this.effectiveConfig.serverSideSorting && !this.records) {
735
- const sortChanged = !oldSort || !this.sortState ||
736
- oldSort.field !== this.sortState.field ||
737
- oldSort.direction !== this.sortState.direction;
738
- if (sortChanged) {
739
- this.resetPaginationState();
740
- this.loadData();
741
- }
742
- }
743
- }
744
- // Handle viewEntity changes - reload data when view changes
745
- if (changes['viewEntity']) {
746
- if (this.entity && !this.records) {
747
- // Apply view's sort state if available
748
- if (this.viewEntity) {
749
- const viewSortInfo = this.viewEntity.ViewSortInfo;
750
- if (viewSortInfo && viewSortInfo.length > 0) {
751
- this.internalSortState = {
752
- field: viewSortInfo[0].field,
753
- direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
754
- };
755
- }
756
- }
757
- this.resetPaginationState();
758
- this.loadData();
759
- }
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();
760
874
  }
761
875
  }
762
876
  ngOnDestroy() {
@@ -781,8 +895,9 @@ export class EntityViewerComponent {
781
895
  this.debouncedFilterText = filterText;
782
896
  this.filterTextChange.emit(filterText);
783
897
  // If server-side filtering and filter changed, reload from page 1
898
+ // Keep existing records visible during refresh for better UX
784
899
  if (this.effectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.records) {
785
- this.resetPaginationState();
900
+ this.resetPaginationState(false);
786
901
  this.loadData();
787
902
  }
788
903
  else {
@@ -806,42 +921,54 @@ export class EntityViewerComponent {
806
921
  }
807
922
  /**
808
923
  * Reset pagination state for a fresh load.
809
- * Clears all record data and counts to prevent stale data display during entity switches.
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.
810
926
  */
811
- resetPaginationState() {
927
+ resetPaginationState(clearRecords = true) {
812
928
  this.pagination = {
813
929
  currentPage: 0,
814
930
  pageSize: this.effectiveConfig.pageSize,
815
- totalRecords: 0,
931
+ totalRecords: clearRecords ? 0 : this.pagination.totalRecords,
816
932
  hasMore: false,
817
933
  isLoading: false
818
934
  };
819
- this.internalRecords = [];
820
- this.totalRecordCount = 0;
821
- this.filteredRecordCount = 0;
935
+ if (clearRecords) {
936
+ this.internalRecords = [];
937
+ this.totalRecordCount = 0;
938
+ this.filteredRecordCount = 0;
939
+ }
822
940
  this.isInitialLoad = true;
823
941
  }
824
942
  // ========================================
825
943
  // DATA LOADING
826
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;
827
949
  /**
828
950
  * Load data for the current entity with server-side filtering/sorting/pagination
829
951
  */
830
952
  async loadData() {
831
- if (!this.entity) {
953
+ const entity = this.effectiveEntity;
954
+ if (!entity) {
832
955
  this.internalRecords = [];
833
956
  this.totalRecordCount = 0;
834
957
  this.filteredRecordCount = 0;
835
958
  return;
836
959
  }
837
- // Prevent concurrent loads which can cause duplicate records
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
838
964
  if (this.isLoading) {
965
+ this._pendingReload = true;
839
966
  return;
840
967
  }
841
968
  this.isLoading = true;
842
969
  this.pagination.isLoading = true;
843
970
  this.loadingMessage = this.isInitialLoad
844
- ? `Loading ${this.entity.Name}...`
971
+ ? `Loading ${entity.Name}...`
845
972
  : 'Loading more records...';
846
973
  this.cdr.detectChanges();
847
974
  const startTime = Date.now();
@@ -864,14 +991,22 @@ export class EntityViewerComponent {
864
991
  // The view's WhereClause is the "business filter" - UserSearchString is additive
865
992
  const extraFilter = this.viewEntity?.WhereClause || undefined;
866
993
  const result = await rv.RunView({
867
- EntityName: this.entity.Name,
994
+ EntityName: entity.Name,
868
995
  ResultType: 'entity_object',
869
996
  MaxRows: config.pageSize,
870
997
  StartRow: startRow,
871
998
  OrderBy: orderBy,
872
999
  ExtraFilter: extraFilter,
873
- UserSearchString: config.serverSideFiltering ? this.debouncedFilterText || undefined : undefined
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
874
1005
  });
1006
+ // Check if this load is still the current one (detect stale responses)
1007
+ if (loadId !== this._loadSequence) {
1008
+ return;
1009
+ }
875
1010
  if (result.Success) {
876
1011
  // Append or replace records based on whether this is initial load
877
1012
  if (this.isInitialLoad) {
@@ -899,7 +1034,6 @@ export class EntityViewerComponent {
899
1034
  this.updateTimelineGroups();
900
1035
  }
901
1036
  else {
902
- console.error('Failed to load records:', result.ErrorMessage);
903
1037
  if (this.isInitialLoad) {
904
1038
  this.internalRecords = [];
905
1039
  }
@@ -908,7 +1042,6 @@ export class EntityViewerComponent {
908
1042
  }
909
1043
  }
910
1044
  catch (error) {
911
- console.error('Error loading records:', error);
912
1045
  if (this.isInitialLoad) {
913
1046
  this.internalRecords = [];
914
1047
  }
@@ -920,6 +1053,13 @@ export class EntityViewerComponent {
920
1053
  this.pagination.isLoading = false;
921
1054
  this.isInitialLoad = false;
922
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
+ }
923
1063
  }
924
1064
  }
925
1065
  /**
@@ -934,10 +1074,11 @@ export class EntityViewerComponent {
934
1074
  }
935
1075
  /**
936
1076
  * Refresh data (re-load from server, starting at page 1)
1077
+ * Keeps existing records visible during refresh for better UX
937
1078
  */
938
1079
  refresh() {
939
1080
  if (!this.records) {
940
- this.resetPaginationState();
1081
+ this.resetPaginationState(false);
941
1082
  this.loadData();
942
1083
  }
943
1084
  }
@@ -983,12 +1124,13 @@ export class EntityViewerComponent {
983
1124
  this.internalSortState = event.sort;
984
1125
  this.sortChanged.emit(event);
985
1126
  // If server-side sorting, reload from page 1
1127
+ // Keep existing records visible during refresh for better UX
986
1128
  if (this.effectiveConfig.serverSideSorting && !this.records) {
987
1129
  const sortChanged = !oldSort || !event.sort ||
988
1130
  oldSort.field !== event.sort?.field ||
989
1131
  oldSort.direction !== event.sort?.direction;
990
1132
  if (sortChanged) {
991
- this.resetPaginationState();
1133
+ this.resetPaginationState(false);
992
1134
  this.loadData();
993
1135
  }
994
1136
  }
@@ -1028,11 +1170,12 @@ export class EntityViewerComponent {
1028
1170
  * Maps to recordSelected event for parent components
1029
1171
  */
1030
1172
  onDataGridRowClick(event) {
1031
- if (!this.entity || !event.row)
1173
+ const entity = this.effectiveEntity;
1174
+ if (!entity || !event.row)
1032
1175
  return;
1033
1176
  this.recordSelected.emit({
1034
1177
  record: event.row,
1035
- entity: this.entity,
1178
+ entity: entity,
1036
1179
  compositeKey: event.row.PrimaryKey
1037
1180
  });
1038
1181
  }
@@ -1041,11 +1184,12 @@ export class EntityViewerComponent {
1041
1184
  * Maps to recordOpened event for parent components
1042
1185
  */
1043
1186
  onDataGridRowDoubleClick(event) {
1044
- if (!this.entity || !event.row)
1187
+ const entity = this.effectiveEntity;
1188
+ if (!entity || !event.row)
1045
1189
  return;
1046
1190
  this.recordOpened.emit({
1047
1191
  record: event.row,
1048
- entity: this.entity,
1192
+ entity: entity,
1049
1193
  compositeKey: event.row.PrimaryKey
1050
1194
  });
1051
1195
  }
@@ -1064,8 +1208,9 @@ export class EntityViewerComponent {
1064
1208
  this.internalSortState = newSort;
1065
1209
  this.sortChanged.emit({ sort: newSort });
1066
1210
  // If server-side sorting, reload from page 1
1211
+ // Keep existing records visible during refresh for better UX
1067
1212
  if (this.effectiveConfig.serverSideSorting && !this.records) {
1068
- this.resetPaginationState();
1213
+ this.resetPaginationState(false);
1069
1214
  this.loadData();
1070
1215
  }
1071
1216
  }
@@ -1095,6 +1240,27 @@ export class EntityViewerComponent {
1095
1240
  onGridExportRequested() {
1096
1241
  this.exportRequested.emit({ format: 'excel' });
1097
1242
  }
1243
+ /**
1244
+ * Handle Add to List button click from data grid toolbar.
1245
+ * Forwards the event to parent components for list management.
1246
+ */
1247
+ onGridAddToListRequested(event) {
1248
+ this.addToListRequested.emit(event);
1249
+ }
1250
+ /**
1251
+ * Handle selection change from data grid.
1252
+ * Converts selected keys to records and forwards to parent components.
1253
+ */
1254
+ onGridSelectionChange(selectedKeys) {
1255
+ // Find the actual records from our filtered records
1256
+ const records = this.filteredRecords.filter(record => {
1257
+ const key = record.PrimaryKey?.ToConcatenatedString() || String(record.Get('ID'));
1258
+ return selectedKeys.includes(key);
1259
+ });
1260
+ // Get the raw primary key values for list management
1261
+ const recordIds = records.map(record => String(record.PrimaryKey.KeyValuePairs[0].Value));
1262
+ this.selectionChanged.emit({ records, recordIds });
1263
+ }
1098
1264
  // ========================================
1099
1265
  // TIMELINE METHODS
1100
1266
  // ========================================
@@ -1103,10 +1269,11 @@ export class EntityViewerComponent {
1103
1269
  */
1104
1270
  onTimelineEventClick(event) {
1105
1271
  const record = event.event.entity;
1106
- if (record && this.entity) {
1272
+ const entity = this.effectiveEntity;
1273
+ if (record && entity) {
1107
1274
  this.recordSelected.emit({
1108
1275
  record,
1109
- entity: this.entity,
1276
+ entity: entity,
1110
1277
  compositeKey: record.PrimaryKey
1111
1278
  });
1112
1279
  }
@@ -1339,8 +1506,13 @@ export class EntityViewerComponent {
1339
1506
  const firstOther = fields.find(f => f.Name !== excludeField);
1340
1507
  return firstOther?.Name || null;
1341
1508
  }
1342
- static ɵfac = function EntityViewerComponent_Factory(t) { return new (t || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1343
- 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", timelineConfig: "timelineConfig", showGridToolbar: "showGridToolbar", gridToolbarConfig: "gridToolbarConfig", gridSelectionMode: "gridSelectionMode" }, 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" }, features: [i0.ɵɵNgOnChangesFeature], decls: 19, vars: 37, 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", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "ExportButtonClick", "hidden", "Data", "Params", "FilterText", "GridState", "Height", "ShowToolbar", "ToolbarConfig", "SelectionMode", "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) {
1509
+ static ɵfac = function EntityViewerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1510
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityViewerComponent, selectors: [["mj-entity-viewer"]], viewQuery: function EntityViewerComponent_Query(rf, ctx) { if (rf & 1) {
1511
+ i0.ɵɵviewQuery(EntityDataGridComponent, 5);
1512
+ } if (rf & 2) {
1513
+ let _t;
1514
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dataGridRef = _t.first);
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) {
1344
1516
  i0.ɵɵelementStart(0, "div", 0);
1345
1517
  i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Template, 5, 4, "div", 1);
1346
1518
  i0.ɵɵelementStart(2, "div", 2)(3, "div", 3);
@@ -1360,7 +1532,7 @@ export class EntityViewerComponent {
1360
1532
  i0.ɵɵtext(14);
1361
1533
  i0.ɵɵelementEnd()();
1362
1534
  i0.ɵɵelementStart(15, "mj-entity-data-grid", 10);
1363
- i0.ɵɵlistener("AfterRowClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowClick_15_listener($event) { return ctx.onDataGridRowClick($event); })("AfterRowDoubleClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_15_listener($event) { return ctx.onDataGridRowDoubleClick($event); })("AfterSort", function EntityViewerComponent_Template_mj_entity_data_grid_AfterSort_15_listener($event) { return ctx.onDataGridSortChanged($event); })("GridStateChanged", function EntityViewerComponent_Template_mj_entity_data_grid_GridStateChanged_15_listener($event) { return ctx.onGridStateChanged($event); })("NewButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_NewButtonClick_15_listener() { return ctx.onGridAddRequested(); })("RefreshButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_RefreshButtonClick_15_listener() { return ctx.onGridRefreshRequested(); })("DeleteButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_DeleteButtonClick_15_listener($event) { return ctx.onGridDeleteRequested($event); })("ExportButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_ExportButtonClick_15_listener() { return ctx.onGridExportRequested(); });
1535
+ i0.ɵɵlistener("AfterRowClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowClick_15_listener($event) { return ctx.onDataGridRowClick($event); })("AfterRowDoubleClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_15_listener($event) { return ctx.onDataGridRowDoubleClick($event); })("AfterSort", function EntityViewerComponent_Template_mj_entity_data_grid_AfterSort_15_listener($event) { return ctx.onDataGridSortChanged($event); })("GridStateChanged", function EntityViewerComponent_Template_mj_entity_data_grid_GridStateChanged_15_listener($event) { return ctx.onGridStateChanged($event); })("SelectionChange", function EntityViewerComponent_Template_mj_entity_data_grid_SelectionChange_15_listener($event) { return ctx.onGridSelectionChange($event); })("NewButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_NewButtonClick_15_listener() { return ctx.onGridAddRequested(); })("RefreshButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_RefreshButtonClick_15_listener() { return ctx.onGridRefreshRequested(); })("DeleteButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_DeleteButtonClick_15_listener($event) { return ctx.onGridDeleteRequested($event); })("ExportButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_ExportButtonClick_15_listener() { return ctx.onGridExportRequested(); })("AddToListRequested", function EntityViewerComponent_Template_mj_entity_data_grid_AddToListRequested_15_listener($event) { return ctx.onGridAddToListRequested($event); });
1364
1536
  i0.ɵɵelementEnd();
1365
1537
  i0.ɵɵelementStart(16, "mj-entity-cards", 11);
1366
1538
  i0.ɵɵlistener("recordSelected", function EntityViewerComponent_Template_mj_entity_cards_recordSelected_16_listener($event) { return ctx.onRecordSelected($event); })("recordOpened", function EntityViewerComponent_Template_mj_entity_cards_recordOpened_16_listener($event) { return ctx.onRecordOpened($event); });
@@ -1383,26 +1555,26 @@ export class EntityViewerComponent {
1383
1555
  i0.ɵɵadvance();
1384
1556
  i0.ɵɵproperty("text", ctx.loadingMessage);
1385
1557
  i0.ɵɵadvance();
1386
- i0.ɵɵproperty("hidden", !!ctx.entity);
1558
+ i0.ɵɵproperty("hidden", !!ctx.effectiveEntity);
1387
1559
  i0.ɵɵadvance(4);
1388
- i0.ɵɵproperty("hidden", !ctx.entity || ctx.filteredRecords.length > 0 || ctx.isLoading);
1560
+ i0.ɵɵproperty("hidden", !ctx.effectiveEntity || ctx.filteredRecords.length > 0 || ctx.isLoading);
1389
1561
  i0.ɵɵadvance(3);
1390
1562
  i0.ɵɵtextInterpolate(ctx.debouncedFilterText ? "No matching records" : "No records found");
1391
1563
  i0.ɵɵadvance();
1392
- i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "grid" || !ctx.entity)("Data", ctx.filteredRecords)("Params", ctx.gridParams)("FilterText", ctx.debouncedFilterText)("GridState", ctx.gridState)("Height", "auto")("ShowToolbar", ctx.showGridToolbar)("ToolbarConfig", ctx.effectiveGridToolbarConfig)("SelectionMode", ctx.gridSelectionMode)("AllowLoad", false);
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);
1393
1565
  i0.ɵɵadvance();
1394
- i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "cards" || !ctx.entity)("entity", ctx.entity)("records", ctx.filteredRecords)("selectedRecordId", ctx.selectedRecordId)("cardTemplate", ctx.cardTemplate)("hiddenFieldMatches", ctx.hiddenFieldMatches)("filterText", ctx.debouncedFilterText);
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);
1395
1567
  i0.ɵɵadvance();
1396
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);
1397
1569
  i0.ɵɵadvance();
1398
- i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.entity && (ctx.pagination.hasMore || ctx.pagination.totalRecords > ctx.effectiveConfig.pageSize) ? 18 : -1);
1399
- } }, 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}"] });
1400
1572
  }
1401
1573
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
1402
1574
  type: Component,
1403
1575
  args: [{ selector: 'mj-entity-viewer', host: {
1404
1576
  'style': 'display: block; height: 100%;'
1405
- }, 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 [AllowLoad]=\"false\"\n (AfterRowClick)=\"onDataGridRowClick($event)\"\n (AfterRowDoubleClick)=\"onDataGridRowDoubleClick($event)\"\n (AfterSort)=\"onDataGridSortChanged($event)\"\n (GridStateChanged)=\"onGridStateChanged($event)\"\n (NewButtonClick)=\"onGridAddRequested()\"\n (RefreshButtonClick)=\"onGridRefreshRequested()\"\n (DeleteButtonClick)=\"onGridDeleteRequested($event)\"\n (ExportButtonClick)=\"onGridExportRequested()\">\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"] }]
1406
1578
  }], () => [{ type: i0.ChangeDetectorRef }], { entity: [{
1407
1579
  type: Input
1408
1580
  }], records: [{
@@ -1433,6 +1605,8 @@ export class EntityViewerComponent {
1433
1605
  type: Input
1434
1606
  }], gridSelectionMode: [{
1435
1607
  type: Input
1608
+ }], showAddToListButton: [{
1609
+ type: Input
1436
1610
  }], recordSelected: [{
1437
1611
  type: Output
1438
1612
  }], recordOpened: [{
@@ -1459,6 +1633,13 @@ export class EntityViewerComponent {
1459
1633
  type: Output
1460
1634
  }], exportRequested: [{
1461
1635
  type: Output
1636
+ }], addToListRequested: [{
1637
+ type: Output
1638
+ }], selectionChanged: [{
1639
+ type: Output
1640
+ }], dataGridRef: [{
1641
+ type: ViewChild,
1642
+ args: [EntityDataGridComponent]
1462
1643
  }] }); })();
1463
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 75 }); })();
1644
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 76 }); })();
1464
1645
  //# sourceMappingURL=entity-viewer.component.js.map