@memberjunction/ng-entity-viewer 5.38.0 → 5.40.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 (93) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/view-types.test.d.ts +2 -0
  3. package/dist/__tests__/view-types.test.d.ts.map +1 -0
  4. package/dist/__tests__/view-types.test.js +102 -0
  5. package/dist/__tests__/view-types.test.js.map +1 -0
  6. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.js +2 -2
  7. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.js.map +1 -1
  8. package/dist/lib/confirm-dialog/confirm-dialog.component.js +2 -2
  9. package/dist/lib/confirm-dialog/confirm-dialog.component.js.map +1 -1
  10. package/dist/lib/duplicate-view-dialog/duplicate-view-dialog.component.js +2 -2
  11. package/dist/lib/duplicate-view-dialog/duplicate-view-dialog.component.js.map +1 -1
  12. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  13. package/dist/lib/entity-data-grid/entity-data-grid.component.js +10 -2
  14. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  15. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +2 -2
  16. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
  17. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +356 -341
  18. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  19. package/dist/lib/entity-viewer/entity-viewer.component.js +993 -1097
  20. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  21. package/dist/lib/quick-save-dialog/quick-save-dialog.component.js +2 -2
  22. package/dist/lib/quick-save-dialog/quick-save-dialog.component.js.map +1 -1
  23. package/dist/lib/recycle-bin/recycle-bin.component.js +1 -1
  24. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js +2 -2
  25. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js.map +1 -1
  26. package/dist/lib/view-config-panel/view-config-panel.component.d.ts +126 -126
  27. package/dist/lib/view-config-panel/view-config-panel.component.js +635 -635
  28. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  29. package/dist/lib/view-selector/view-selector.component.d.ts +226 -0
  30. package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -0
  31. package/dist/lib/view-selector/view-selector.component.js +861 -0
  32. package/dist/lib/view-selector/view-selector.component.js.map +1 -0
  33. package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts +114 -0
  34. package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts.map +1 -0
  35. package/dist/lib/view-type-switcher/view-type-switcher.component.js +209 -0
  36. package/dist/lib/view-type-switcher/view-type-switcher.component.js.map +1 -0
  37. package/dist/lib/view-types/descriptors/cards-view-type.d.ts +18 -0
  38. package/dist/lib/view-types/descriptors/cards-view-type.d.ts.map +1 -0
  39. package/dist/lib/view-types/descriptors/cards-view-type.js +31 -0
  40. package/dist/lib/view-types/descriptors/cards-view-type.js.map +1 -0
  41. package/dist/lib/view-types/descriptors/grid-view-type.d.ts +17 -0
  42. package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -0
  43. package/dist/lib/view-types/descriptors/grid-view-type.js +30 -0
  44. package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -0
  45. package/dist/lib/view-types/descriptors/map-view-type.d.ts +21 -0
  46. package/dist/lib/view-types/descriptors/map-view-type.d.ts.map +1 -0
  47. package/dist/lib/view-types/descriptors/map-view-type.js +35 -0
  48. package/dist/lib/view-types/descriptors/map-view-type.js.map +1 -0
  49. package/dist/lib/view-types/descriptors/timeline-view-type.d.ts +22 -0
  50. package/dist/lib/view-types/descriptors/timeline-view-type.d.ts.map +1 -0
  51. package/dist/lib/view-types/descriptors/timeline-view-type.js +40 -0
  52. package/dist/lib/view-types/descriptors/timeline-view-type.js.map +1 -0
  53. package/dist/lib/view-types/index.d.ts +20 -0
  54. package/dist/lib/view-types/index.d.ts.map +1 -0
  55. package/dist/lib/view-types/index.js +29 -0
  56. package/dist/lib/view-types/index.js.map +1 -0
  57. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts +93 -0
  58. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts.map +1 -0
  59. package/dist/lib/view-types/renderers/cards-view-renderer.component.js +144 -0
  60. package/dist/lib/view-types/renderers/cards-view-renderer.component.js.map +1 -0
  61. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +273 -0
  62. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -0
  63. package/dist/lib/view-types/renderers/grid-view-renderer.component.js +558 -0
  64. package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -0
  65. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts +135 -0
  66. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -0
  67. package/dist/lib/view-types/renderers/map-view-renderer.component.js +216 -0
  68. package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -0
  69. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts +176 -0
  70. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -0
  71. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +535 -0
  72. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -0
  73. package/dist/lib/view-types/view-type.contracts.d.ts +235 -0
  74. package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -0
  75. package/dist/lib/view-types/view-type.contracts.js +51 -0
  76. package/dist/lib/view-types/view-type.contracts.js.map +1 -0
  77. package/dist/lib/view-types/view-type.engine.d.ts +76 -0
  78. package/dist/lib/view-types/view-type.engine.d.ts.map +1 -0
  79. package/dist/lib/view-types/view-type.engine.js +138 -0
  80. package/dist/lib/view-types/view-type.engine.js.map +1 -0
  81. package/dist/lib/view-workspace/view-workspace.component.d.ts +451 -0
  82. package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -0
  83. package/dist/lib/view-workspace/view-workspace.component.js +1212 -0
  84. package/dist/lib/view-workspace/view-workspace.component.js.map +1 -0
  85. package/dist/module.d.ts +20 -11
  86. package/dist/module.d.ts.map +1 -1
  87. package/dist/module.js +50 -8
  88. package/dist/module.js.map +1 -1
  89. package/dist/public-api.d.ts +8 -0
  90. package/dist/public-api.d.ts.map +1 -1
  91. package/dist/public-api.js +14 -0
  92. package/dist/public-api.js.map +1 -1
  93. package/package.json +19 -18
@@ -1,45 +1,41 @@
1
- import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
1
+ import { Component, Input, Output, EventEmitter, ViewChild, ViewContainerRef, reflectComponentType } from '@angular/core';
2
2
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
3
  import { Subject } from 'rxjs';
4
4
  import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
5
- import { EntityFieldTSType, RunView, CompositeKey } from '@memberjunction/core';
5
+ import { RunView, LogError } from '@memberjunction/core';
6
6
  import { UUIDsEqual } from '@memberjunction/global';
7
- import { buildCompositeKey, buildPkString, computeFieldsList } from '../utils/record.util';
8
- import { TimelineGroup } from '@memberjunction/ng-timeline';
7
+ import { UserInfoEngine } from '@memberjunction/core-entities';
8
+ import { buildCompositeKey, buildPkString } from '../utils/record.util';
9
9
  import { DEFAULT_VIEWER_CONFIG } from '../types';
10
- import { EntityDataGridComponent } from '../entity-data-grid/entity-data-grid.component';
10
+ import { ViewTypeEngine } from '../view-types';
11
11
  import * as i0 from "@angular/core";
12
- import * as i1 from "@angular/forms";
13
- import * as i2 from "@memberjunction/ng-shared-generic";
14
- import * as i3 from "@memberjunction/ng-timeline";
15
- import * as i4 from "@memberjunction/ng-map-view";
16
- import * as i5 from "../entity-cards/entity-cards.component";
17
- import * as i6 from "../entity-data-grid/entity-data-grid.component";
18
- import * as i7 from "../recycle-bin/recycle-bin-chip.component";
19
- import * as i8 from "@angular/common";
20
- const _forTrack0 = ($index, $item) => $item.Name;
12
+ import * as i1 from "@memberjunction/ng-shared-generic";
13
+ import * as i2 from "../recycle-bin/recycle-bin-chip.component";
14
+ import * as i3 from "../view-type-switcher/view-type-switcher.component";
15
+ import * as i4 from "@angular/common";
16
+ const _c0 = ["dynamicViewHost"];
21
17
  function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
22
18
  const _r3 = i0.ɵɵgetCurrentView();
23
- i0.ɵɵelementStart(0, "button", 21);
24
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.clearFilter()); });
25
- i0.ɵɵelement(1, "i", 22);
19
+ i0.ɵɵelementStart(0, "button", 19);
20
+ i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.ClearFilter()); });
21
+ i0.ɵɵelement(1, "i", 20);
26
22
  i0.ɵɵelementEnd();
27
23
  } }
28
24
  function EntityViewerComponent_Conditional_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
29
25
  const _r1 = i0.ɵɵgetCurrentView();
30
- i0.ɵɵelementStart(0, "div", 14);
31
- i0.ɵɵelement(1, "i", 18);
32
- i0.ɵɵelementStart(2, "input", 19);
33
- i0.ɵɵlistener("input", function EntityViewerComponent_Conditional_1_Conditional_1_Template_input_input_2_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onFilterChange($event.target.value)); });
26
+ i0.ɵɵelementStart(0, "div", 12);
27
+ i0.ɵɵelement(1, "i", 16);
28
+ i0.ɵɵelementStart(2, "input", 17);
29
+ i0.ɵɵlistener("input", function EntityViewerComponent_Conditional_1_Conditional_1_Template_input_input_2_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.OnFilterChange($event.target.value)); });
34
30
  i0.ɵɵelementEnd();
35
- i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button", 20);
31
+ i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button", 18);
36
32
  i0.ɵɵelementEnd();
37
33
  } if (rf & 2) {
38
34
  const ctx_r1 = i0.ɵɵnextContext(2);
39
35
  i0.ɵɵadvance(2);
40
- i0.ɵɵproperty("placeholder", ctx_r1.effectiveConfig.filterPlaceholder)("value", ctx_r1.effectiveFilterText);
36
+ i0.ɵɵproperty("placeholder", ctx_r1.EffectiveConfig.filterPlaceholder)("value", ctx_r1.EffectiveFilterText);
41
37
  i0.ɵɵadvance();
42
- i0.ɵɵconditional(ctx_r1.effectiveFilterText ? 3 : -1);
38
+ i0.ɵɵconditional(ctx_r1.EffectiveFilterText ? 3 : -1);
43
39
  } }
44
40
  function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
45
41
  i0.ɵɵelementStart(0, "span");
@@ -50,7 +46,7 @@ function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_1_Templat
50
46
  } if (rf & 2) {
51
47
  const ctx_r1 = i0.ɵɵnextContext(3);
52
48
  i0.ɵɵadvance();
53
- i0.ɵɵtextInterpolate2("", i0.ɵɵpipeBind1(2, 2, ctx_r1.filteredRecordCount), " of ", i0.ɵɵpipeBind1(3, 4, ctx_r1.totalRecordCount), " records");
49
+ i0.ɵɵtextInterpolate2("", i0.ɵɵpipeBind1(2, 2, ctx_r1.FilteredRecordCount), " of ", i0.ɵɵpipeBind1(3, 4, ctx_r1.TotalRecordCount), " records");
54
50
  } }
55
51
  function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Template(rf, ctx) { if (rf & 1) {
56
52
  i0.ɵɵelementStart(0, "span");
@@ -60,147 +56,51 @@ function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Templat
60
56
  } if (rf & 2) {
61
57
  const ctx_r1 = i0.ɵɵnextContext(3);
62
58
  i0.ɵɵadvance();
63
- i0.ɵɵtextInterpolate1("", i0.ɵɵpipeBind1(2, 1, ctx_r1.totalRecordCount), " records");
59
+ i0.ɵɵtextInterpolate1("", i0.ɵɵpipeBind1(2, 1, ctx_r1.TotalRecordCount), " records");
64
60
  } }
65
61
  function EntityViewerComponent_Conditional_1_Conditional_2_Template(rf, ctx) { if (rf & 1) {
66
- i0.ɵɵelementStart(0, "div", 15);
62
+ i0.ɵɵelementStart(0, "div", 13);
67
63
  i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_2_Conditional_1_Template, 4, 6, "span")(2, EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Template, 3, 3, "span");
68
64
  i0.ɵɵelementEnd();
69
65
  } if (rf & 2) {
70
66
  const ctx_r1 = i0.ɵɵnextContext(2);
71
67
  i0.ɵɵadvance();
72
- i0.ɵɵconditional(ctx_r1.filteredRecordCount !== ctx_r1.totalRecordCount ? 1 : 2);
73
- } }
74
- function EntityViewerComponent_Conditional_1_Conditional_3_Conditional_5_Template(rf, ctx) { if (rf & 1) {
75
- const _r5 = i0.ɵɵgetCurrentView();
76
- i0.ɵɵelementStart(0, "button", 30);
77
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Conditional_5_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.setViewMode("timeline")); });
78
- i0.ɵɵelement(1, "i", 31);
79
- i0.ɵɵelementEnd();
80
- } if (rf & 2) {
81
- const ctx_r1 = i0.ɵɵnextContext(3);
82
- i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "timeline");
68
+ i0.ɵɵconditional(ctx_r1.FilteredRecordCount !== ctx_r1.TotalRecordCount ? 1 : 2);
83
69
  } }
84
70
  function EntityViewerComponent_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
85
71
  const _r4 = i0.ɵɵgetCurrentView();
86
- i0.ɵɵelementStart(0, "div", 16)(1, "button", 23);
87
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.setViewMode("grid")); });
88
- i0.ɵɵelement(2, "i", 24);
89
- i0.ɵɵelementEnd();
90
- i0.ɵɵelementStart(3, "button", 25);
91
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.setViewMode("cards")); });
92
- i0.ɵɵelement(4, "i", 26);
72
+ i0.ɵɵelementStart(0, "mj-view-type-switcher", 21);
73
+ i0.ɵɵlistener("ViewTypeSelected", function EntityViewerComponent_Conditional_1_Conditional_3_Template_mj_view_type_switcher_ViewTypeSelected_0_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.SelectViewTypeById($event.viewTypeId)); });
93
74
  i0.ɵɵelementEnd();
94
- i0.ɵɵconditionalCreate(5, EntityViewerComponent_Conditional_1_Conditional_3_Conditional_5_Template, 2, 2, "button", 27);
95
- i0.ɵɵelementStart(6, "button", 28);
96
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_3_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.setViewMode("map")); });
97
- i0.ɵɵelement(7, "i", 29);
98
- i0.ɵɵelementEnd()();
99
75
  } if (rf & 2) {
100
76
  const ctx_r1 = i0.ɵɵnextContext(2);
101
- i0.ɵɵadvance();
102
- i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "grid");
103
- i0.ɵɵadvance(2);
104
- i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "cards");
105
- i0.ɵɵadvance(2);
106
- i0.ɵɵconditional(ctx_r1.hasDateFields ? 5 : -1);
107
- i0.ɵɵadvance();
108
- i0.ɵɵclassProp("active", ctx_r1.effectiveViewMode === "map")("geo-hidden", !ctx_r1.HasGeoCoding);
77
+ i0.ɵɵproperty("Provider", ctx_r1.Provider)("Entity", ctx_r1.EffectiveEntity)("ActiveViewTypeID", ctx_r1.ActiveViewTypeId);
109
78
  } }
110
79
  function EntityViewerComponent_Conditional_1_Conditional_4_Template(rf, ctx) { if (rf & 1) {
111
- i0.ɵɵelement(0, "mj-recycle-bin-chip", 17);
80
+ i0.ɵɵelement(0, "mj-recycle-bin-chip", 15);
112
81
  } if (rf & 2) {
113
82
  const ctx_r1 = i0.ɵɵnextContext(2);
114
- i0.ɵɵproperty("EntityName", (ctx_r1.effectiveEntity == null ? null : ctx_r1.effectiveEntity.Name) ?? null);
115
- } }
116
- function EntityViewerComponent_Conditional_1_Conditional_5_Conditional_2_Template(rf, ctx) { if (rf & 1) {
117
- i0.ɵɵelementStart(0, "span", 34);
118
- i0.ɵɵtext(1);
119
- i0.ɵɵelementEnd();
120
- } if (rf & 2) {
121
- const ctx_r1 = i0.ɵɵnextContext(3);
122
- i0.ɵɵadvance();
123
- i0.ɵɵtextInterpolate(ctx_r1.selectedDateFieldDisplayName);
124
- } }
125
- function EntityViewerComponent_Conditional_1_Conditional_5_Conditional_3_For_2_Template(rf, ctx) { if (rf & 1) {
126
- i0.ɵɵelementStart(0, "option", 40);
127
- i0.ɵɵtext(1);
128
- i0.ɵɵelementEnd();
129
- } if (rf & 2) {
130
- const field_r8 = ctx.$implicit;
131
- i0.ɵɵproperty("value", field_r8.Name);
132
- i0.ɵɵadvance();
133
- i0.ɵɵtextInterpolate(field_r8.DisplayNameOrName);
134
- } }
135
- function EntityViewerComponent_Conditional_1_Conditional_5_Conditional_3_Template(rf, ctx) { if (rf & 1) {
136
- const _r7 = i0.ɵɵgetCurrentView();
137
- i0.ɵɵelementStart(0, "select", 39);
138
- i0.ɵɵlistener("change", function EntityViewerComponent_Conditional_1_Conditional_5_Conditional_3_Template_select_change_0_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.setTimelineDateField($event.target.value)); });
139
- i0.ɵɵrepeaterCreate(1, EntityViewerComponent_Conditional_1_Conditional_5_Conditional_3_For_2_Template, 2, 2, "option", 40, _forTrack0);
140
- i0.ɵɵelementEnd();
141
- } if (rf & 2) {
142
- const ctx_r1 = i0.ɵɵnextContext(3);
143
- i0.ɵɵproperty("value", ctx_r1.selectedTimelineDateField);
144
- i0.ɵɵadvance();
145
- i0.ɵɵrepeater(ctx_r1.availableDateFields);
146
- } }
147
- function EntityViewerComponent_Conditional_1_Conditional_5_Template(rf, ctx) { if (rf & 1) {
148
- const _r6 = i0.ɵɵgetCurrentView();
149
- i0.ɵɵelementStart(0, "div", 32);
150
- i0.ɵɵelement(1, "i", 33);
151
- i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_5_Conditional_2_Template, 2, 1, "span", 34)(3, EntityViewerComponent_Conditional_1_Conditional_5_Conditional_3_Template, 3, 1, "select", 35);
152
- i0.ɵɵelementEnd();
153
- i0.ɵɵelementStart(4, "div", 36)(5, "button", 37);
154
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_5_Template_button_click_5_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.toggleTimelineOrientation()); });
155
- i0.ɵɵelement(6, "i");
156
- i0.ɵɵelementEnd()();
157
- i0.ɵɵelementStart(7, "div", 38)(8, "button", 37);
158
- i0.ɵɵlistener("click", function EntityViewerComponent_Conditional_1_Conditional_5_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.toggleTimelineSortOrder()); });
159
- i0.ɵɵelement(9, "i");
160
- i0.ɵɵelementEnd()();
161
- } if (rf & 2) {
162
- const ctx_r1 = i0.ɵɵnextContext(2);
163
- i0.ɵɵadvance(2);
164
- i0.ɵɵconditional(ctx_r1.availableDateFields.length === 1 ? 2 : 3);
165
- i0.ɵɵadvance(3);
166
- i0.ɵɵproperty("title", ctx_r1.timelineOrientation === "vertical" ? "Switch to Horizontal" : "Switch to Vertical");
167
- i0.ɵɵadvance();
168
- i0.ɵɵclassMap(ctx_r1.timelineOrientation === "vertical" ? "fa-solid fa-ellipsis-vertical" : "fa-solid fa-ellipsis");
169
- i0.ɵɵadvance(2);
170
- i0.ɵɵproperty("title", ctx_r1.timelineSortOrder === "desc" ? "Showing Newest First (click for Oldest First)" : "Showing Oldest First (click for Newest First)");
171
- i0.ɵɵadvance();
172
- i0.ɵɵclassMap(ctx_r1.timelineSortOrder === "desc" ? "fa-solid fa-arrow-down-wide-short" : "fa-solid fa-arrow-up-wide-short");
83
+ i0.ɵɵproperty("EntityName", (ctx_r1.EffectiveEntity == null ? null : ctx_r1.EffectiveEntity.Name) ?? null);
173
84
  } }
174
85
  function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
175
- i0.ɵɵelementStart(0, "div", 1);
176
- i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div", 14);
177
- i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div", 15);
178
- i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_3_Template, 8, 9, "div", 16);
179
- i0.ɵɵconditionalCreate(4, EntityViewerComponent_Conditional_1_Conditional_4_Template, 1, 1, "mj-recycle-bin-chip", 17);
180
- i0.ɵɵconditionalCreate(5, EntityViewerComponent_Conditional_1_Conditional_5_Template, 10, 7);
86
+ i0.ɵɵelementStart(0, "div", 2);
87
+ i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div", 12);
88
+ i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div", 13);
89
+ i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_3_Template, 1, 3, "mj-view-type-switcher", 14);
90
+ i0.ɵɵconditionalCreate(4, EntityViewerComponent_Conditional_1_Conditional_4_Template, 1, 1, "mj-recycle-bin-chip", 15);
181
91
  i0.ɵɵelementEnd();
182
92
  } if (rf & 2) {
183
93
  const ctx_r1 = i0.ɵɵnextContext();
184
94
  i0.ɵɵadvance();
185
- i0.ɵɵconditional(ctx_r1.effectiveConfig.showFilter ? 1 : -1);
95
+ i0.ɵɵconditional(ctx_r1.EffectiveConfig.showFilter ? 1 : -1);
186
96
  i0.ɵɵadvance();
187
- i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.effectiveEntity ? 2 : -1);
97
+ i0.ɵɵconditional(ctx_r1.EffectiveConfig.showRecordCount && ctx_r1.EffectiveEntity ? 2 : -1);
188
98
  i0.ɵɵadvance();
189
- i0.ɵɵconditional(ctx_r1.effectiveConfig.showViewModeToggle ? 3 : -1);
99
+ i0.ɵɵconditional(ctx_r1.EffectiveConfig.showViewModeToggle ? 3 : -1);
190
100
  i0.ɵɵadvance();
191
101
  i0.ɵɵconditional(ctx_r1.ShowRecycleBin ? 4 : -1);
192
- i0.ɵɵadvance();
193
- i0.ɵɵconditional(ctx_r1.effectiveViewMode === "timeline" && ctx_r1.hasDateFields ? 5 : -1);
194
- } }
195
- function EntityViewerComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
196
- const _r9 = i0.ɵɵgetCurrentView();
197
- i0.ɵɵelementStart(0, "mj-map-view", 41);
198
- i0.ɵɵlistener("MarkerClick", function EntityViewerComponent_Conditional_18_Template_mj_map_view_MarkerClick_0_listener($event) { i0.ɵɵrestoreView(_r9); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMapMarkerClick($event)); })("RenderModeChange", function EntityViewerComponent_Conditional_18_Template_mj_map_view_RenderModeChange_0_listener($event) { i0.ɵɵrestoreView(_r9); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMapRenderModeChange($event)); })("DisplayStateChange", function EntityViewerComponent_Conditional_18_Template_mj_map_view_DisplayStateChange_0_listener($event) { i0.ɵɵrestoreView(_r9); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMapDisplayStateChange($event)); });
199
- i0.ɵɵelementEnd();
200
- } if (rf & 2) {
201
- const ctx_r1 = i0.ɵɵnextContext();
202
- i0.ɵɵproperty("hidden", ctx_r1.effectiveViewMode !== "map")("Entity", ctx_r1.effectiveEntity)("Records", ctx_r1.filteredRecords)("TotalRecordCount", ctx_r1.totalRecordCount)("RenderMode", ctx_r1.mapRenderMode)("DisplayState", ctx_r1.mapDisplayState);
203
102
  } }
103
+ function EntityViewerComponent_ng_template_16_Template(rf, ctx) { }
204
104
  /**
205
105
  * EntityViewerComponent - Full-featured composite component for viewing entity data
206
106
  *
@@ -217,163 +117,209 @@ function EntityViewerComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
217
117
  * ```html
218
118
  * <!-- Basic usage - loads data automatically -->
219
119
  * <mj-entity-viewer
220
- * [entity]="selectedEntity"
221
- * (recordSelected)="onRecordSelected($event)"
222
- * (recordOpened)="onRecordOpened($event)">
120
+ * [Entity]="selectedEntity"
121
+ * (RecordSelected)="onRecordSelected($event)"
122
+ * (RecordOpened)="onRecordOpened($event)">
223
123
  * </mj-entity-viewer>
224
124
  *
225
125
  * <!-- With external state control (like Data Explorer) -->
226
126
  * <mj-entity-viewer
227
- * [entity]="selectedEntity"
228
- * [(viewMode)]="state.viewMode"
229
- * [filterText]="state.filterText"
230
- * [selectedRecordId]="state.selectedRecordId"
231
- * (recordSelected)="onRecordSelected($event)"
232
- * (recordOpened)="onRecordOpened($event)"
233
- * (sortChanged)="onSortChanged($event)">
127
+ * [Entity]="selectedEntity"
128
+ * [ViewTypeID]="state.viewTypeId"
129
+ * [FilterText]="state.filterText"
130
+ * [SelectedRecordID]="state.selectedRecordId"
131
+ * (RecordSelected)="onRecordSelected($event)"
132
+ * (RecordOpened)="onRecordOpened($event)">
234
133
  * </mj-entity-viewer>
235
134
  * ```
236
135
  */
237
136
  export class EntityViewerComponent extends BaseAngularComponent {
238
137
  cdr;
239
138
  ngZone;
139
+ elementRef;
240
140
  /**
241
- * Maximum records to load in map mode. Map view needs all records for
242
- * geographic visualization paging doesn't make sense for maps. This cap
243
- * prevents unbounded queries on very large entities.
141
+ * Safety cap on the number of records loaded when a plug-in renderer asks the container to
142
+ * load the full set (via a {@link ViewDataRequest} with `loadAll: true`). Prevents unbounded
143
+ * queries on very large entities. Generic — not tied to any specific view type.
244
144
  */
245
- static MAP_MAX_RECORDS = 10000;
145
+ static LOAD_ALL_MAX_RECORDS = 10000;
246
146
  // ========================================
247
147
  // INPUTS (using getter/setter pattern)
248
148
  // ========================================
249
149
  _entity = null;
250
150
  _records = null;
251
151
  _config = {};
252
- _viewMode = null;
253
152
  _filterText = null;
254
153
  _sortState = null;
255
154
  _viewEntity = null;
256
- _timelineConfig = null;
257
155
  _initialized = false;
156
+ /**
157
+ * When true, the next {@link LoadData} loads the full record set (up to
158
+ * {@link LOAD_ALL_MAX_RECORDS}) instead of paginating. Set generically from a plug-in's
159
+ * {@link ViewDataRequest} (`loadAll`) — never from any view-type check.
160
+ */
161
+ _loadAllRecords = false;
258
162
  /** Whether a deferred reload has been queued via deferReload() */
259
163
  _reloadDeferred = false;
260
164
  /**
261
165
  * The entity to display records for
262
166
  */
263
- get entity() {
167
+ get Entity() {
264
168
  return this._entity;
265
169
  }
266
- set entity(value) {
170
+ set Entity(value) {
267
171
  const previousEntity = this._entity;
268
172
  this._entity = value;
269
- // Detect date fields for timeline support
270
- this.detectDateFields();
271
- // Detect geocoding support for map view
272
- this.updateGeoCodingSupport();
273
- if (this._initialized) {
274
- // If entity changed to a different entity, clear all stale state from the old entity
275
- if (value && previousEntity && !UUIDsEqual(value.ID, previousEntity.ID)) {
276
- if (this._viewEntity && !UUIDsEqual(this._viewEntity.EntityID, value.ID)) {
277
- this._viewEntity = null;
278
- }
279
- // Clear sort state it references fields from the old entity (e.g., FirstName)
280
- // and would produce invalid ORDER BY on the new entity
281
- this.internalSortState = null;
173
+ const entityChanged = !!(this._initialized && value && previousEntity && !UUIDsEqual(value.ID, previousEntity.ID));
174
+ // On a real entity change, drop per-entity state BEFORE we recompute view types + re-seed config,
175
+ // so the new entity starts clean:
176
+ // - The per-view-type config map (grid columnSettings, timeline date field, …) is per-ENTITY.
177
+ // Keeping the old entity's config applies its columnSettings to the new entity, so only fields
178
+ // common to both survive (e.g. just Name/Description) the "no/too-few columns" symptom.
179
+ // - The loaded UserView record belongs to the old entity; clear it so re-seeding reads the new
180
+ // entity's saved view / per-user default-view setting.
181
+ // - Sort state references old-entity fields and would produce an invalid ORDER BY.
182
+ if (entityChanged) {
183
+ if (this._viewEntity && value && !UUIDsEqual(this._viewEntity.EntityID, value.ID)) {
184
+ this._viewEntity = null;
282
185
  }
186
+ this.viewTypeConfigById.clear();
187
+ this.InternalSortState = null;
188
+ // Throw out the cached plug-in instances — they belong to the previous entity. The next
189
+ // selection rebuilds them fresh for the new entity (correct columns / date fields / geo).
190
+ this.clearDynamicRendererCache();
191
+ }
192
+ // Recompute available view types for the new entity from the registry (if loaded). This also
193
+ // re-seeds the per-view-type config from the NEW entity's saved view / default-view setting
194
+ // (now that the stale map was cleared above). Falls back silently when the registry has no data.
195
+ this.refreshAvailableViewTypes();
196
+ if (this._initialized) {
283
197
  if (value && !this._records) {
284
198
  // Reset state for new entity - synchronously clear all data and force change detection
285
199
  // before starting the async load to prevent stale data display
286
200
  this.resetPaginationState();
287
- this.internalRecords = [];
288
- this.totalRecordCount = 0;
289
- this.filteredRecordCount = 0;
201
+ this.InternalRecords = [];
202
+ this.TotalRecordCount = 0;
203
+ this.FilteredRecordCount = 0;
290
204
  this.cdr.detectChanges();
291
205
  // Defer the actual load so all input bindings (viewEntity, gridState, etc.)
292
206
  // complete before we fire the RunView — prevents duplicate loads with stale state
293
207
  this.deferReload();
294
208
  }
295
209
  else if (!value) {
296
- this.internalRecords = [];
297
- this.totalRecordCount = 0;
298
- this.filteredRecordCount = 0;
210
+ this.InternalRecords = [];
211
+ this.TotalRecordCount = 0;
212
+ this.FilteredRecordCount = 0;
299
213
  this.resetPaginationState();
300
214
  this.cdr.detectChanges();
301
215
  }
216
+ // The cache was thrown out above, so re-create the active view type FRESH for the new entity
217
+ // (unless refreshAvailableViewTypes already re-selected because the active type became
218
+ // unavailable). This rebuilds columns / date fields / geo cleanly; records flow in via the
219
+ // deferred reload. The container stays generic — it re-creates whatever plug-in is active
220
+ // without knowing what it is.
221
+ if (entityChanged && !this.dynamicRendererRef) {
222
+ const activeOption = this.AvailableViewTypes.find(o => o.key === this.ActiveViewTypeKey) ?? this.AvailableViewTypes[0];
223
+ if (activeOption) {
224
+ this.ActiveViewTypeKey = activeOption.key;
225
+ this.ActiveDynamicOption = activeOption;
226
+ this.selectDynamicRenderer(activeOption);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ /**
232
+ * Convenience input: the entity to display by **name**. Resolved internally to an `EntityInfo`
233
+ * via the active provider and applied through the {@link entity} setter. Use this OR `[entity]`
234
+ * OR `[EntityID]` — whichever is most convenient for the consumer.
235
+ */
236
+ set EntityName(value) {
237
+ if (!value) {
238
+ return;
239
+ }
240
+ const resolved = this.ProviderToUse?.EntityByName(value) ?? null;
241
+ if (resolved) {
242
+ this.Entity = resolved;
243
+ }
244
+ }
245
+ /**
246
+ * Convenience input: the entity to display by **ID**. Resolved internally to an `EntityInfo`
247
+ * and applied through the {@link entity} setter.
248
+ */
249
+ set EntityID(value) {
250
+ if (!value) {
251
+ return;
252
+ }
253
+ const resolved = this.ProviderToUse?.Entities.find(e => UUIDsEqual(e.ID, value)) ?? null;
254
+ if (resolved) {
255
+ this.Entity = resolved;
256
+ }
257
+ }
258
+ /**
259
+ * Convenience input: load and display a saved view by its `MJ: User Views` **ID**. The viewer
260
+ * loads the record via the active provider and applies it through the {@link viewEntity} setter
261
+ * (which also resolves the entity if `[entity]`/`[EntityName]`/`[EntityID]` weren't supplied).
262
+ */
263
+ set ViewID(value) {
264
+ if (value) {
265
+ void this.loadViewById(value);
302
266
  }
303
267
  }
304
268
  /**
305
269
  * Pre-loaded records (optional - if not provided, component loads data)
306
270
  */
307
- get records() {
271
+ get Records() {
308
272
  return this._records;
309
273
  }
310
- set records(value) {
274
+ set Records(value) {
311
275
  this._records = value;
312
276
  if (value) {
313
- this.internalRecords = value;
314
- this.totalRecordCount = value.length;
315
- this.filteredRecordCount = value.length;
316
- // Update timeline with new records
317
- this.updateTimelineGroups();
277
+ this.InternalRecords = value;
278
+ this.TotalRecordCount = value.length;
279
+ this.FilteredRecordCount = value.length;
280
+ this.pushDynamicRendererInputs();
318
281
  }
319
282
  }
320
283
  /**
321
284
  * Configuration options for the viewer
322
285
  */
323
- get config() {
286
+ get Config() {
324
287
  return this._config;
325
288
  }
326
- set config(value) {
289
+ set Config(value) {
327
290
  this._config = value;
328
291
  this.applyConfig();
329
292
  }
330
293
  /**
331
294
  * Currently selected record ID (primary key string)
332
295
  */
333
- selectedRecordId = null;
334
- /**
335
- * External view mode - allows parent to control view mode
336
- * Supports two-way binding: [(viewMode)]="state.viewMode"
337
- */
338
- get viewMode() {
339
- return this._viewMode;
296
+ get SelectedRecordID() {
297
+ return this._selectedRecordId;
340
298
  }
341
- set viewMode(value) {
342
- const previousEffective = this.effectiveViewMode;
343
- this._viewMode = value;
344
- if (value !== null) {
345
- this.internalViewMode = value;
346
- }
347
- // Map mode uses different RunView params (MaxRows = MAP_MAX_RECORDS, no pagination)
348
- // and different field set (includes BoundaryGeoJSON when entity.SupportsGeoCoding=1).
349
- // If the parent flips us into/out of map mode via two-way binding we need to reload —
350
- // otherwise the map gets the grid's paginated data without per-record geometry.
351
- const newEffective = this.effectiveViewMode;
352
- if (this._initialized && previousEffective !== newEffective &&
353
- (newEffective === 'map' || previousEffective === 'map')) {
354
- this.resetPaginationState();
355
- this.loadData();
356
- }
299
+ set SelectedRecordID(value) {
300
+ this._selectedRecordId = value;
301
+ this.pushDynamicRendererInputs();
357
302
  }
303
+ _selectedRecordId = null;
358
304
  /**
359
305
  * External filter text - allows parent to control filter
360
306
  * Supports two-way binding: [(filterText)]="state.filterText"
361
307
  */
362
- get filterText() {
308
+ get FilterText() {
363
309
  return this._filterText;
364
310
  }
365
- set filterText(value) {
366
- const oldFilter = this.debouncedFilterText;
311
+ set FilterText(value) {
312
+ const oldFilter = this.DebouncedFilterText;
367
313
  this._filterText = value;
368
314
  const newFilter = value ?? '';
369
- this.internalFilterText = newFilter;
370
- this.debouncedFilterText = newFilter;
315
+ this.InternalFilterText = newFilter;
316
+ this.DebouncedFilterText = newFilter;
371
317
  if (this._initialized) {
372
318
  // If server-side filtering and filter changed, reload from page 1
373
319
  // Keep existing records visible during refresh for better UX
374
- if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this._records) {
320
+ if (this.EffectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this._records) {
375
321
  this.resetPaginationState(false);
376
- this.loadData();
322
+ this.LoadData();
377
323
  }
378
324
  else {
379
325
  this.updateFilteredCount();
@@ -384,47 +330,61 @@ export class EntityViewerComponent extends BaseAngularComponent {
384
330
  /**
385
331
  * External sort state - allows parent to control sorting
386
332
  */
387
- get sortState() {
333
+ get SortState() {
388
334
  return this._sortState;
389
335
  }
390
- set sortState(value) {
391
- const oldSort = this.internalSortState;
336
+ set SortState(value) {
337
+ const oldSort = this.InternalSortState;
392
338
  this._sortState = value;
393
339
  if (value !== null) {
394
- this.internalSortState = value;
340
+ this.InternalSortState = value;
395
341
  if (this._initialized) {
396
342
  // If sort changed and using server-side sorting, reload
397
343
  // Keep existing records visible during refresh for better UX
398
- if (this.effectiveConfig.serverSideSorting && !this._records) {
344
+ if (this.EffectiveConfig.serverSideSorting && !this._records) {
399
345
  const sortChanged = !oldSort || !value ||
400
346
  oldSort.field !== value.field ||
401
347
  oldSort.direction !== value.direction;
402
348
  if (sortChanged) {
403
349
  this.resetPaginationState(false);
404
- this.loadData();
350
+ this.LoadData();
405
351
  }
406
352
  }
407
353
  }
408
354
  }
409
355
  }
410
- /**
411
- * Custom grid column definitions
412
- */
413
- gridColumns = [];
414
- /**
415
- * Custom card template
416
- */
417
- cardTemplate = null;
418
356
  /**
419
357
  * Optional User View entity that provides view configuration
420
358
  * When provided, the component will use the view's WhereClause, GridState, SortState, etc.
421
359
  * The view's filter is additive - UserSearchString is applied ON TOP of the view's WhereClause
422
360
  */
423
- get viewEntity() {
361
+ get ViewEntity() {
424
362
  return this._viewEntity;
425
363
  }
426
- set viewEntity(value) {
364
+ set ViewEntity(value) {
365
+ const previousViewId = this._viewEntity?.ID ?? null;
366
+ const nextViewId = value?.ID ?? null;
367
+ const viewChanged = this._initialized && previousViewId !== nextViewId;
427
368
  this._viewEntity = value;
369
+ // A changed view (including default↔saved and saved↔different) is a new data context: throw out
370
+ // the cached plug-in instances + per-view-type config so they rebuild/re-seed from the new view.
371
+ if (viewChanged) {
372
+ this.viewTypeConfigById.clear();
373
+ this.clearDynamicRendererCache();
374
+ }
375
+ // Re-resolve available view types + the initial view type/config from the new view record
376
+ // (self-contained: the viewer reads ViewTypeID + DisplayState.viewTypeConfigs off the record).
377
+ this.refreshAvailableViewTypes();
378
+ // If the cache was thrown out but refreshAvailableViewTypes didn't re-select (active type still
379
+ // valid), re-create the active view type fresh for the new view.
380
+ if (viewChanged && !this.dynamicRendererRef) {
381
+ const activeOption = this.AvailableViewTypes.find(o => o.key === this.ActiveViewTypeKey) ?? this.AvailableViewTypes[0];
382
+ if (activeOption) {
383
+ this.ActiveViewTypeKey = activeOption.key;
384
+ this.ActiveDynamicOption = activeOption;
385
+ this.selectDynamicRenderer(activeOption);
386
+ }
387
+ }
428
388
  if (this._initialized && this._entity && !this._records) {
429
389
  // Apply view's sort state if available, then defer the reload.
430
390
  // Deferring ensures all sibling input bindings (gridState, etc.) are
@@ -438,54 +398,7 @@ export class EntityViewerComponent extends BaseAngularComponent {
438
398
  * Grid state configuration from a User View
439
399
  * Controls column visibility, widths, order, and sort settings
440
400
  */
441
- gridState = null;
442
- /**
443
- * Timeline configuration state
444
- * Controls which date field is used and segment grouping
445
- */
446
- get timelineConfig() {
447
- return this._timelineConfig;
448
- }
449
- set timelineConfig(value) {
450
- const prev = this._timelineConfig;
451
- // Compare by value, not reference
452
- const isEqual = (prev === null && value === null) ||
453
- (prev !== null && value !== null &&
454
- prev.dateFieldName === value.dateFieldName &&
455
- prev.sortOrder === value.sortOrder &&
456
- prev.orientation === value.orientation &&
457
- prev.segmentGrouping === value.segmentGrouping);
458
- if (!isEqual) {
459
- this._timelineConfig = value;
460
- if (value && this._entity) {
461
- this.configureTimeline();
462
- this.cdr.markForCheck();
463
- }
464
- }
465
- }
466
- /**
467
- * Whether to show the grid toolbar.
468
- * When false, the grid is displayed without its own toolbar - useful when
469
- * entity-viewer provides its own filter/actions in the header.
470
- * @default false
471
- */
472
- showGridToolbar = false;
473
- /**
474
- * Grid toolbar configuration - controls which buttons are shown and their behavior
475
- * When not provided, uses sensible defaults
476
- */
477
- gridToolbarConfig = null;
478
- /**
479
- * Grid selection mode
480
- * @default 'single'
481
- */
482
- gridSelectionMode = 'single';
483
- /**
484
- * Show the "Add to List" button in the grid toolbar.
485
- * Requires gridSelectionMode to be 'multiple' for best UX.
486
- * @default false
487
- */
488
- showAddToListButton = false;
401
+ GridState = null;
489
402
  /**
490
403
  * Whether to render the Recycle Bin chip in the viewer header.
491
404
  * The chip auto-hides itself when the entity has no deleted records,
@@ -500,148 +413,190 @@ export class EntityViewerComponent extends BaseAngularComponent {
500
413
  /**
501
414
  * Emitted when a record is selected (single click)
502
415
  */
503
- recordSelected = new EventEmitter();
416
+ RecordSelected = new EventEmitter();
504
417
  /**
505
418
  * Emitted when a record should be opened (double-click or open button)
506
419
  */
507
- recordOpened = new EventEmitter();
420
+ RecordOpened = new EventEmitter();
508
421
  /**
509
422
  * Emitted when data is loaded
510
423
  */
511
- dataLoaded = new EventEmitter();
512
- /**
513
- * Emitted when the view mode changes (for two-way binding)
514
- */
515
- viewModeChange = new EventEmitter();
424
+ DataLoaded = new EventEmitter();
516
425
  /**
517
426
  * Emitted when filter text changes (for two-way binding)
518
427
  */
519
- filterTextChange = new EventEmitter();
428
+ FilterTextChange = new EventEmitter();
520
429
  /**
521
430
  * Emitted when filtered count changes
522
431
  */
523
- filteredCountChanged = new EventEmitter();
432
+ FilteredCountChanged = new EventEmitter();
433
+ /**
434
+ * NAVIGATION request bubbled up from a plug-in renderer to open a *related* record on a
435
+ * (possibly different) entity — e.g. a grid foreign-key drill-through. Routing lives in the
436
+ * outer app, so this is one of the few signals that legitimately bubbles up. The container
437
+ * forwards it untouched.
438
+ */
439
+ OpenRelatedRecordRequested = new EventEmitter();
440
+ /**
441
+ * NAVIGATION request bubbled up from a plug-in renderer to create a new record of the current
442
+ * entity (e.g. a grid's "New" button). Opening the create form is a routing concern owned by
443
+ * the outer app; the container forwards it without acting on it.
444
+ */
445
+ CreateRecordRequested = new EventEmitter();
446
+ /**
447
+ * The initial/active view type to open in, by `MJ: View Types` row ID. Hosts that persist
448
+ * the selection (e.g. Explorer's `UserView.ViewTypeID`) bind this so the viewer opens in the
449
+ * saved type — built-in OR plug-in. Applied once the registry resolves; later user switches
450
+ * emit {@link viewTypeChange} for the host to persist.
451
+ */
452
+ set ViewTypeID(value) {
453
+ this._initialViewTypeId = value;
454
+ // If options are already loaded, apply immediately; otherwise refreshAvailableViewTypes will.
455
+ if (value && this.AvailableViewTypes.length > 0) {
456
+ const opt = this.AvailableViewTypes.find(o => o.viewTypeId === value);
457
+ if (opt && opt.key !== this.ActiveViewTypeKey) {
458
+ this.applyViewTypeSelection(opt, false);
459
+ }
460
+ }
461
+ }
462
+ get ViewTypeID() {
463
+ return this._initialViewTypeId;
464
+ }
465
+ _initialViewTypeId = null;
466
+ // ========================================
467
+ // INTERNAL STATE
468
+ // ========================================
524
469
  /**
525
- * Emitted when sort state changes
470
+ * The view-type options shown in the switcher, sourced from {@link ViewTypeEngine}
471
+ * (the `MJ: View Types` registry), filtered by each descriptor's availability predicate.
472
+ * Every option is a dynamic-mounted plug-in.
526
473
  */
527
- sortChanged = new EventEmitter();
474
+ AvailableViewTypes = [];
475
+ /** Whether the registry (ViewTypeEngine) successfully sourced the available view types. */
476
+ ViewTypesFromRegistry = false;
528
477
  /**
529
- * Emitted when grid state changes (column resize, reorder, etc.)
478
+ * The currently-active view type's stable key (descriptor Name). Null until the registry resolves.
530
479
  */
531
- gridStateChanged = new EventEmitter();
480
+ ActiveViewTypeKey = null;
532
481
  /**
533
- * Emitted when timeline configuration changes (date field, grouping, etc.)
482
+ * The currently-active view type's option (the plug-in being mounted). Null until the
483
+ * registry resolves / a type is selected. Drives the dynamic-mount host in the template.
534
484
  */
535
- timelineConfigChange = new EventEmitter();
485
+ ActiveDynamicOption = null;
486
+ /** True once a plug-in view type is mounted (drives the dynamic host's visibility). */
487
+ get IsDynamicViewActive() {
488
+ return this.ActiveDynamicOption !== null;
489
+ }
490
+ /** Whether the view-type dropdown menu is currently open. */
491
+ ViewTypeDropdownOpen = false;
536
492
  /**
537
- * Emitted when the Add/New button is clicked in the grid toolbar
493
+ * Per-view-type configuration payloads, keyed by `MJ: View Types` row ID. Seeded from
494
+ * {@link viewTypeConfigsInput} and updated as plug-in renderers emit config changes;
495
+ * handed to each dynamic renderer on mount.
538
496
  */
539
- addRequested = new EventEmitter();
497
+ viewTypeConfigById = new Map();
540
498
  /**
541
- * Emitted when the Delete button is clicked in the grid toolbar
542
- * Includes the selected records to be deleted
499
+ * Per-view-type configuration provided by the host (e.g. Explorer reading
500
+ * `UserView.DisplayState.viewTypeConfigs`). The active type is controlled separately via
501
+ * the `viewMode` / ViewTypeID inputs; this carries only the config payloads.
543
502
  */
544
- deleteRequested = new EventEmitter();
503
+ set ViewTypeConfigs(value) {
504
+ this.viewTypeConfigById.clear();
505
+ for (const entry of value ?? []) {
506
+ if (entry?.viewTypeId) {
507
+ this.viewTypeConfigById.set(entry.viewTypeId, entry.config ?? {});
508
+ }
509
+ }
510
+ if (this.dynamicRendererRef && this.ActiveDynamicOption) {
511
+ this.pushDynamicRendererInputs();
512
+ }
513
+ }
545
514
  /**
546
- * Emitted when the Refresh button is clicked in the grid toolbar
515
+ * When true, the viewer persists view-type/config changes itself — to the loaded `UserView`
516
+ * record (when a `ViewEntity`/`ViewID` is present) or to per-user User Settings (the default-view
517
+ * case). When false (the default), the viewer only emits the `Before…`/`After…` events and the
518
+ * consumer is responsible for persistence. Persistence is provider-based (generic-safe) — never
519
+ * routing, which stays with the host app.
547
520
  */
548
- refreshRequested = new EventEmitter();
521
+ AutoSaveView = false;
549
522
  /**
550
- * Emitted when the Export button is clicked in the grid toolbar
523
+ * Emitted (cancelable) BEFORE the active view type changes. A handler may set
524
+ * `args.Cancel = true` to veto the switch. Fires for both built-in and plug-in types.
551
525
  */
552
- exportRequested = new EventEmitter();
526
+ BeforeViewTypeChange = new EventEmitter();
553
527
  /**
554
- * Emitted when the Add to List button is clicked in the grid toolbar.
555
- * Parent components should handle this to show the list management dialog.
528
+ * Emitted AFTER the active view type has changed (and, when {@link AutoSaveView} is on, after
529
+ * it has been persisted). Notification only.
556
530
  */
557
- addToListRequested = new EventEmitter();
531
+ AfterViewTypeChange = new EventEmitter();
558
532
  /**
559
- * Emitted when grid selection changes.
560
- * Parent components can use this to track selected records for their own toolbar buttons.
533
+ * Emitted (cancelable) BEFORE a plug-in renderer's configuration change is applied/persisted.
561
534
  */
562
- selectionChanged = new EventEmitter();
563
- // ========================================
564
- // INTERNAL STATE
565
- // ========================================
566
- internalViewMode = 'grid';
567
- internalFilterText = '';
568
- debouncedFilterText = '';
569
- isLoading = false;
570
- loadingMessage = 'Loading...';
571
- internalRecords = [];
572
- totalRecordCount = 0;
573
- filteredRecordCount = 0;
535
+ BeforeViewTypeConfigChange = new EventEmitter();
536
+ /**
537
+ * Emitted AFTER a plug-in renderer's configuration change has been applied (and persisted when
538
+ * {@link AutoSaveView} is on).
539
+ */
540
+ AfterViewTypeConfigChange = new EventEmitter();
541
+ /** Anchor for dynamically-mounted plug-in view renderers. */
542
+ dynamicViewHost;
543
+ /** The currently-ACTIVE (visible) plug-in renderer, if any. */
544
+ dynamicRendererRef = null;
545
+ /** Input names the active plug-in declares (from `reflectComponentType`), used to push only the
546
+ * generic inputs it accepts. Mirrors the active cache entry's input set. */
547
+ dynamicInputNames = new Set();
548
+ /**
549
+ * Cache of mounted plug-in renderer instances for the CURRENT data context (entity + view),
550
+ * keyed by `MJ: View Types` row ID. Switching view types within the same entity+view SHOWS/HIDES
551
+ * cached instances (preserving their state — e.g. a computed cluster scatter, grid scroll) instead
552
+ * of destroy/recreate. The whole cache is destroyed + rebuilt only when the data context changes:
553
+ * entity change, or view (record / ViewID, including default↔saved) change. See
554
+ * {@link clearDynamicRendererCache}.
555
+ */
556
+ dynamicRendererCache = new Map();
557
+ InternalFilterText = '';
558
+ DebouncedFilterText = '';
559
+ IsLoading = false;
560
+ LoadingMessage = 'Loading...';
561
+ InternalRecords = [];
562
+ TotalRecordCount = 0;
563
+ FilteredRecordCount = 0;
574
564
  /** Track which records matched on hidden (non-visible) fields */
575
- hiddenFieldMatches = new Map();
565
+ HiddenFieldMatches = new Map();
576
566
  /** Current sort state */
577
- internalSortState = null;
578
- /** Cached grid params to avoid recreating object on every change detection */
579
- _cachedGridParams = null;
580
- _lastGridParamsEntity = null;
581
- _lastGridParamsViewEntity = null;
567
+ InternalSortState = null;
582
568
  /** Pagination state */
583
- pagination = {
569
+ Pagination = {
584
570
  currentPage: 0,
585
571
  pageSize: 100,
586
572
  totalRecords: 0,
587
573
  hasMore: false,
588
574
  isLoading: false
589
575
  };
590
- // ========================================
591
- // TIMELINE STATE
592
- // ========================================
593
- /** Whether the current entity has date fields available for timeline view */
594
- hasDateFields = false;
595
- /** Whether the current entity supports geocoding (has SupportsGeoCoding = 1) */
596
- HasGeoCoding = false;
597
- /** Available date fields from the entity (sorted by priority) */
598
- availableDateFields = [];
599
- /** Timeline groups configuration for the timeline component */
600
- get timelineGroups() {
601
- return this._timelineGroups;
602
- }
603
- set timelineGroups(value) {
604
- const prev = this._timelineGroups;
605
- this._timelineGroups = value;
606
- // Detect meaningful changes to trigger refresh in child timeline component
607
- const hasChanged = prev !== value ||
608
- (prev.length > 0 && value.length > 0 &&
609
- (prev[0].EntityObjects !== value[0]?.EntityObjects ||
610
- prev[0].DateFieldName !== value[0]?.DateFieldName));
611
- if (hasChanged) {
612
- // Force change detection to propagate to child timeline component
613
- this.cdr.markForCheck();
614
- }
615
- }
616
- _timelineGroups = [];
617
- /** Timeline sort order */
618
- timelineSortOrder = 'desc';
619
- /** Timeline segment grouping */
620
- timelineSegmentGrouping = 'month';
621
- /** Timeline orientation (vertical or horizontal) */
622
- timelineOrientation = 'vertical';
623
- /** Currently selected date field for timeline */
624
- selectedTimelineDateField = null;
625
576
  destroy$ = new Subject();
626
577
  filterInput$ = new Subject();
627
578
  /** Track if this is the first load (vs. load more) */
628
579
  isInitialLoad = true;
629
- /** Reference to the data grid component for flushing pending changes */
630
- dataGridRef;
631
- constructor(cdr, ngZone) {
580
+ constructor(cdr, ngZone, elementRef) {
632
581
  super();
633
582
  this.cdr = cdr;
634
583
  this.ngZone = ngZone;
584
+ this.elementRef = elementRef;
635
585
  }
586
+ /** IntersectionObserver used to detect when this viewer is re-attached/re-shown (Explorer caches +
587
+ * reattaches resource components without firing Angular lifecycle hooks). See {@link ngAfterViewInit}. */
588
+ _visibilityObserver = null;
589
+ _wasVisible = false;
636
590
  // ========================================
637
591
  // PUBLIC METHODS
638
592
  // ========================================
639
593
  /**
640
- * Ensures any pending grid state changes are saved immediately without waiting for debounce.
641
- * Call this before switching views or entities to ensure changes are saved.
594
+ * Hook retained for hosts (e.g. the workspace) that call this before switching views/entities.
595
+ * View-type-specific persistence (e.g. a grid's live column/sort state) is now owned by the
596
+ * mounted plug-in renderer, so the container has nothing to flush — this is a no-op.
642
597
  */
643
598
  EnsurePendingChangesSaved() {
644
- this.dataGridRef?.EnsurePendingChangesSaved();
599
+ // no-op: plug-in renderers own their own pending-state persistence
645
600
  }
646
601
  // ========================================
647
602
  // COMPUTED PROPERTIES
@@ -651,13 +606,13 @@ export class EntityViewerComponent extends BaseAngularComponent {
651
606
  * This allows callers to provide just a viewEntity without explicitly setting the entity input.
652
607
  * Uses fallback resolution when ViewEntityInfo is not available.
653
608
  */
654
- get effectiveEntity() {
655
- if (this.entity) {
656
- return this.entity;
609
+ get EffectiveEntity() {
610
+ if (this.Entity) {
611
+ return this.Entity;
657
612
  }
658
613
  // Auto-derive from viewEntity if available
659
- if (this.viewEntity) {
660
- return this.getEntityInfoFromViewEntity(this.viewEntity);
614
+ if (this.ViewEntity) {
615
+ return this.getEntityInfoFromViewEntity(this.ViewEntity);
661
616
  }
662
617
  return null;
663
618
  }
@@ -691,129 +646,52 @@ export class EntityViewerComponent extends BaseAngularComponent {
691
646
  console.warn(`[EntityViewer] Could not determine entity for view "${viewEntity.Name}" (ID: ${viewEntity.ID})`);
692
647
  return null;
693
648
  }
694
- /**
695
- * Get the effective view mode (external or internal)
696
- */
697
- get effectiveViewMode() {
698
- return this.viewMode ?? this.internalViewMode;
699
- }
700
649
  /**
701
650
  * Get the effective filter text (external or internal)
702
651
  */
703
- get effectiveFilterText() {
704
- return this.filterText ?? this.internalFilterText;
705
- }
706
- /**
707
- * Get the raw ID value from selectedRecordId for timeline selection.
708
- * The selectedRecordId is in composite key format (e.g., "ID|abc-123" or "ID=abc-123"),
709
- * but the timeline stores just the raw ID value.
710
- */
711
- get timelineSelectedEventId() {
712
- if (!this.selectedRecordId)
713
- return null;
714
- // Handle "ID|value" format (pipe separator)
715
- if (this.selectedRecordId.includes('|')) {
716
- const parts = this.selectedRecordId.split('|');
717
- return parts.length > 1 ? parts[1] : this.selectedRecordId;
718
- }
719
- // Handle "ID=value" format (equals separator)
720
- if (this.selectedRecordId.includes('=')) {
721
- const parts = this.selectedRecordId.split('=');
722
- return parts.length > 1 ? parts[1] : this.selectedRecordId;
723
- }
724
- // Return as-is if no separator found
725
- return this.selectedRecordId;
652
+ get EffectiveFilterText() {
653
+ return this.FilterText ?? this.InternalFilterText;
726
654
  }
727
655
  /**
728
656
  * Get the effective sort state (external or internal)
729
657
  */
730
- get effectiveSortState() {
731
- return this.sortState ?? this.internalSortState;
732
- }
733
- /**
734
- * Get the OrderBy string for mj-entity-data-grid from the effective sort state
735
- */
736
- get effectiveSortOrderBy() {
737
- const sortState = this.effectiveSortState;
738
- if (!sortState?.field || !sortState.direction) {
739
- return '';
740
- }
741
- return `${sortState.field} ${sortState.direction.toUpperCase()}`;
658
+ get EffectiveSortState() {
659
+ return this.SortState ?? this.InternalSortState;
742
660
  }
743
661
  /**
744
662
  * Get merged configuration with defaults
745
663
  */
746
- get effectiveConfig() {
747
- return { ...DEFAULT_VIEWER_CONFIG, ...this.config };
748
- }
749
- /**
750
- * Get cached grid params - only recreates object when entity or viewEntity changes
751
- * This prevents Angular from seeing a new object reference on every change detection
752
- * which would cause the grid to reinitialize
753
- */
754
- get gridParams() {
755
- const entity = this.effectiveEntity;
756
- if (!entity) {
757
- return null;
758
- }
759
- // Check if we need to recreate the params object
760
- const entityChanged = this._lastGridParamsEntity !== entity.Name;
761
- const viewEntityChanged = this._lastGridParamsViewEntity !== this.viewEntity;
762
- if (entityChanged || viewEntityChanged || !this._cachedGridParams) {
763
- this._lastGridParamsEntity = entity.Name;
764
- this._lastGridParamsViewEntity = this.viewEntity ?? null;
765
- this._cachedGridParams = {
766
- EntityName: entity.Name,
767
- ViewEntity: this.viewEntity || undefined
768
- };
769
- }
770
- return this._cachedGridParams;
771
- }
772
- /**
773
- * Get the effective grid toolbar configuration
774
- * Merges user-provided config with defaults appropriate for entity-viewer context
775
- */
776
- get effectiveGridToolbarConfig() {
777
- const defaults = {
778
- showSearch: false, // Entity-viewer has its own filter
779
- showRefresh: true,
780
- showAdd: true,
781
- showDelete: true,
782
- showExport: true,
783
- showColumnChooser: true,
784
- showRowCount: true,
785
- showSelectionCount: true
786
- };
787
- return { ...defaults, ...this.gridToolbarConfig };
664
+ get EffectiveConfig() {
665
+ return { ...DEFAULT_VIEWER_CONFIG, ...this.Config };
788
666
  }
789
667
  /**
790
668
  * Get the records to display (external or internal)
791
669
  */
792
- get displayRecords() {
793
- return this.records ?? this.internalRecords;
670
+ get DisplayRecords() {
671
+ return this.Records ?? this.InternalRecords;
794
672
  }
795
673
  /**
796
674
  * Get filtered records - when using server-side filtering, records are already filtered
797
675
  * When using client-side filtering, apply filter locally
798
676
  */
799
- get filteredRecords() {
800
- const records = this.displayRecords;
677
+ get FilteredRecords() {
678
+ const records = this.DisplayRecords;
801
679
  // If server-side filtering is enabled, records are already filtered
802
- if (this.effectiveConfig.serverSideFiltering) {
680
+ if (this.EffectiveConfig.serverSideFiltering) {
803
681
  return records;
804
682
  }
805
683
  // Client-side filtering fallback
806
- const filterText = this.debouncedFilterText?.trim().toLowerCase();
807
- this.hiddenFieldMatches.clear();
808
- if (!filterText || !this.entity) {
684
+ const filterText = this.DebouncedFilterText?.trim().toLowerCase();
685
+ this.HiddenFieldMatches.clear();
686
+ if (!filterText || !this.Entity) {
809
687
  return records;
810
688
  }
811
689
  const visibleFields = this.getVisibleFieldNames();
812
690
  return records.filter(record => {
813
691
  const matchResult = this.recordMatchesFilter(record, filterText, visibleFields);
814
692
  if (matchResult.matches && matchResult.matchedField && !matchResult.matchedInVisibleField) {
815
- const recordKey = buildPkString(record, this.entity);
816
- this.hiddenFieldMatches.set(recordKey, matchResult.matchedField);
693
+ const recordKey = buildPkString(record, this.Entity);
694
+ this.HiddenFieldMatches.set(recordKey, matchResult.matchedField);
817
695
  }
818
696
  return matchResult.matches;
819
697
  });
@@ -822,11 +700,11 @@ export class EntityViewerComponent extends BaseAngularComponent {
822
700
  * Check if a record matches the filter text (client-side)
823
701
  */
824
702
  recordMatchesFilter(record, filterText, visibleFields) {
825
- if (!this.entity)
703
+ if (!this.Entity)
826
704
  return { matches: true, matchedField: null, matchedInVisibleField: false };
827
705
  let matchedField = null;
828
706
  let matchedInVisibleField = false;
829
- for (const field of this.entity.Fields) {
707
+ for (const field of this.Entity.Fields) {
830
708
  if (!this.shouldSearchField(field))
831
709
  continue;
832
710
  const value = record[field.Name];
@@ -883,36 +761,36 @@ export class EntityViewerComponent extends BaseAngularComponent {
883
761
  */
884
762
  getVisibleFieldNames() {
885
763
  const visible = new Set();
886
- if (!this.entity)
764
+ if (!this.Entity)
887
765
  return visible;
888
- for (const field of this.entity.Fields) {
766
+ for (const field of this.Entity.Fields) {
889
767
  if (field.DefaultInView === true) {
890
768
  visible.add(field.Name);
891
769
  }
892
770
  }
893
- if (this.entity.NameField) {
894
- visible.add(this.entity.NameField.Name);
771
+ if (this.Entity.NameField) {
772
+ visible.add(this.Entity.NameField.Name);
895
773
  }
896
774
  return visible;
897
775
  }
898
776
  /**
899
777
  * Check if a record matched on a hidden field
900
778
  */
901
- hasHiddenFieldMatch(record) {
902
- if (!this.debouncedFilterText || !this.entity)
779
+ HasHiddenFieldMatch(record) {
780
+ if (!this.DebouncedFilterText || !this.Entity)
903
781
  return false;
904
- return this.hiddenFieldMatches.has(buildPkString(record, this.entity));
782
+ return this.HiddenFieldMatches.has(buildPkString(record, this.Entity));
905
783
  }
906
784
  /**
907
785
  * Get the name of the hidden field that matched for display
908
786
  */
909
- getHiddenMatchFieldName(record) {
910
- if (!this.entity)
787
+ GetHiddenMatchFieldName(record) {
788
+ if (!this.Entity)
911
789
  return '';
912
- const fieldName = this.hiddenFieldMatches.get(buildPkString(record, this.entity));
913
- if (!fieldName || !this.entity)
790
+ const fieldName = this.HiddenFieldMatches.get(buildPkString(record, this.Entity));
791
+ if (!fieldName || !this.Entity)
914
792
  return '';
915
- const field = this.entity.Fields.find(f => f.Name === fieldName);
793
+ const field = this.Entity.Fields.find(f => f.Name === fieldName);
916
794
  return field ? field.DisplayNameOrName : fieldName;
917
795
  }
918
796
  // ========================================
@@ -922,18 +800,22 @@ export class EntityViewerComponent extends BaseAngularComponent {
922
800
  this.applyConfig();
923
801
  this.setupFilterDebounce();
924
802
  // Initialize debounced filter from external filter text if provided
925
- if (this.filterText !== null) {
926
- this.debouncedFilterText = this.filterText;
803
+ if (this.FilterText !== null) {
804
+ this.DebouncedFilterText = this.FilterText;
927
805
  }
928
806
  // Initialize sort state from config
929
- if (this.effectiveConfig.defaultSortField) {
930
- this.internalSortState = {
931
- field: this.effectiveConfig.defaultSortField,
932
- direction: this.effectiveConfig.defaultSortDirection ?? 'asc'
807
+ if (this.EffectiveConfig.defaultSortField) {
808
+ this.InternalSortState = {
809
+ field: this.EffectiveConfig.defaultSortField,
810
+ direction: this.EffectiveConfig.defaultSortDirection ?? 'asc'
933
811
  };
934
812
  }
935
813
  // Mark as initialized - setters will now trigger data loading
936
814
  this._initialized = true;
815
+ // Load the view-type registry and source the available-modes list from it.
816
+ // Fire-and-forget: until it resolves (or if it fails), the template uses the
817
+ // hardcoded fallback switcher, so behavior is unchanged on un-seeded systems.
818
+ void this.ensureViewTypesLoaded();
937
819
  // If viewEntity was set before initialization, extract its sort state now.
938
820
  // The viewEntity setter skips this when _initialized is false.
939
821
  if (this._viewEntity) {
@@ -958,7 +840,7 @@ export class EntityViewerComponent extends BaseAngularComponent {
958
840
  Promise.resolve().then(async () => {
959
841
  try {
960
842
  if (this._initialized && this._entity && !this._records) {
961
- await this.loadData();
843
+ await this.LoadData();
962
844
  }
963
845
  }
964
846
  finally {
@@ -968,7 +850,60 @@ export class EntityViewerComponent extends BaseAngularComponent {
968
850
  }
969
851
  });
970
852
  }
853
+ ngAfterViewInit() {
854
+ // CRITICAL: the active view type is selected during ngOnInit (Entity setter → refreshAvailableViewTypes
855
+ // → applyViewTypeSelection → selectDynamicRenderer), but `dynamicViewHost` is a NON-static @ViewChild
856
+ // that only resolves now (ngAfterViewInit). So that earlier selectDynamicRenderer() returned early
857
+ // (host undefined) and never created the renderer — leaving a stale ActiveDynamicOption with no
858
+ // mounted ref (symptom: "171 records" header but an empty grid, pushInputs SKIPPED ref=null). Now
859
+ // that the host exists, mount the active renderer if it hasn't been.
860
+ if (this.ActiveDynamicOption && !this.dynamicRendererRef && this.dynamicViewHost) {
861
+ this.selectDynamicRenderer(this.ActiveDynamicOption);
862
+ }
863
+ // Detect re-attach: Explorer caches resource components and detaches/re-attaches their VIEW
864
+ // without firing ngOnInit/ngOnDestroy. When the dashboard tab is re-focused, this viewer's host
865
+ // re-enters the DOM/viewport — but the mounted plug-in (e.g. AG Grid) was rendered while detached
866
+ // and may show an empty body even though the data is still loaded. An IntersectionObserver gives
867
+ // us a reliable "became visible again" signal with no lifecycle hook.
868
+ if (typeof IntersectionObserver !== 'undefined' && this.elementRef?.nativeElement) {
869
+ this.ngZone.runOutsideAngular(() => {
870
+ this._visibilityObserver = new IntersectionObserver((entries) => {
871
+ const visible = entries.some(e => e.isIntersecting);
872
+ if (visible && !this._wasVisible) {
873
+ this._wasVisible = true;
874
+ this.ngZone.run(() => this.onViewerReattached());
875
+ }
876
+ else if (!visible) {
877
+ this._wasVisible = false;
878
+ }
879
+ });
880
+ this._visibilityObserver.observe(this.elementRef.nativeElement);
881
+ });
882
+ }
883
+ }
884
+ /**
885
+ * Called when the viewer becomes visible again after being detached (cached-tab reattach). Mounts
886
+ * the active plug-in if it was never created (host wasn't ready at selection time), or re-pushes the
887
+ * current inputs so a host-fed grid re-renders its rows (the "N records in header but empty grid on
888
+ * return" symptom).
889
+ */
890
+ onViewerReattached() {
891
+ const active = this.ActiveDynamicOption;
892
+ if (active && !this.dynamicRendererRef && this.dynamicViewHost) {
893
+ // Active view type selected but never mounted (host wasn't ready at selection time) — mount now.
894
+ this.selectDynamicRenderer(active);
895
+ }
896
+ else if (active && this.dynamicRendererRef) {
897
+ // Re-assert visibility (force display:block) + re-push inputs so the mounted renderer re-renders.
898
+ this.setRendererVisible(this.dynamicRendererRef, true);
899
+ this.pushDynamicRendererInputs();
900
+ this.dynamicRendererRef.changeDetectorRef.detectChanges();
901
+ }
902
+ }
971
903
  ngOnDestroy() {
904
+ this._visibilityObserver?.disconnect();
905
+ this._visibilityObserver = null;
906
+ this.clearDynamicRendererCache();
972
907
  this.destroy$.next();
973
908
  this.destroy$.complete();
974
909
  }
@@ -982,13 +917,13 @@ export class EntityViewerComponent extends BaseAngularComponent {
982
917
  */
983
918
  applySortStateFromView(view) {
984
919
  if (!view) {
985
- this.internalSortState = null;
920
+ this.InternalSortState = null;
986
921
  return;
987
922
  }
988
923
  // Priority 1: SortState column (via ViewSortInfo)
989
924
  const viewSortInfo = view.ViewSortInfo;
990
925
  if (viewSortInfo && viewSortInfo.length > 0) {
991
- this.internalSortState = {
926
+ this.InternalSortState = {
992
927
  field: viewSortInfo[0].field,
993
928
  direction: viewSortInfo[0].direction?.toLowerCase() === 'desc' ? 'desc' : 'asc'
994
929
  };
@@ -999,7 +934,7 @@ export class EntityViewerComponent extends BaseAngularComponent {
999
934
  const gridState = view.GridStateObject;
1000
935
  if (gridState?.sortSettings && gridState.sortSettings.length > 0) {
1001
936
  const firstSort = gridState.sortSettings[0];
1002
- this.internalSortState = {
937
+ this.InternalSortState = {
1003
938
  field: firstSort.field,
1004
939
  direction: firstSort.dir === 'desc' ? 'desc' : 'asc'
1005
940
  };
@@ -1007,31 +942,29 @@ export class EntityViewerComponent extends BaseAngularComponent {
1007
942
  }
1008
943
  }
1009
944
  // No sort defined — reset to prevent stale sort from previous view
1010
- this.internalSortState = null;
945
+ this.InternalSortState = null;
1011
946
  }
1012
947
  applyConfig() {
1013
- const config = this.effectiveConfig;
1014
- this.pagination.pageSize = config.pageSize;
1015
- if (this.viewMode === null) {
1016
- this.internalViewMode = config.defaultViewMode;
1017
- }
948
+ const config = this.EffectiveConfig;
949
+ this.Pagination.pageSize = config.pageSize;
1018
950
  }
1019
951
  setupFilterDebounce() {
1020
952
  this.filterInput$
1021
- .pipe(debounceTime(this.effectiveConfig.filterDebounceMs), distinctUntilChanged(), takeUntil(this.destroy$))
953
+ .pipe(debounceTime(this.EffectiveConfig.filterDebounceMs), distinctUntilChanged(), takeUntil(this.destroy$))
1022
954
  .subscribe(filterText => {
1023
- const oldFilter = this.debouncedFilterText;
1024
- this.debouncedFilterText = filterText;
1025
- this.filterTextChange.emit(filterText);
955
+ const oldFilter = this.DebouncedFilterText;
956
+ this.DebouncedFilterText = filterText;
957
+ this.FilterTextChange.emit(filterText);
1026
958
  // If server-side filtering and filter changed, reload from page 1
1027
959
  // Keep existing records visible during refresh for better UX
1028
- if (this.effectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.records) {
960
+ if (this.EffectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.Records) {
1029
961
  this.resetPaginationState(false);
1030
- this.loadData();
962
+ this.LoadData();
1031
963
  }
1032
964
  else {
1033
965
  this.updateFilteredCount();
1034
966
  }
967
+ this.pushDynamicRendererInputs(); // client-side filter changed — refresh a mounted plug-in
1035
968
  this.cdr.detectChanges();
1036
969
  });
1037
970
  }
@@ -1039,12 +972,12 @@ export class EntityViewerComponent extends BaseAngularComponent {
1039
972
  * Update the filtered record count and emit event
1040
973
  */
1041
974
  updateFilteredCount() {
1042
- const newCount = this.filteredRecords.length;
1043
- if (this.filteredRecordCount !== newCount) {
1044
- this.filteredRecordCount = newCount;
1045
- this.filteredCountChanged.emit({
975
+ const newCount = this.FilteredRecords.length;
976
+ if (this.FilteredRecordCount !== newCount) {
977
+ this.FilteredRecordCount = newCount;
978
+ this.FilteredCountChanged.emit({
1046
979
  filteredCount: newCount,
1047
- totalCount: this.totalRecordCount
980
+ totalCount: this.TotalRecordCount
1048
981
  });
1049
982
  }
1050
983
  }
@@ -1054,17 +987,17 @@ export class EntityViewerComponent extends BaseAngularComponent {
1054
987
  * When clearRecords is false, keeps existing records visible during refresh - use for sort/filter changes.
1055
988
  */
1056
989
  resetPaginationState(clearRecords = true) {
1057
- this.pagination = {
990
+ this.Pagination = {
1058
991
  currentPage: 0,
1059
- pageSize: this.effectiveConfig.pageSize,
1060
- totalRecords: clearRecords ? 0 : this.pagination.totalRecords,
992
+ pageSize: this.EffectiveConfig.pageSize,
993
+ totalRecords: clearRecords ? 0 : this.Pagination.totalRecords,
1061
994
  hasMore: false,
1062
995
  isLoading: false
1063
996
  };
1064
997
  if (clearRecords) {
1065
- this.internalRecords = [];
1066
- this.totalRecordCount = 0;
1067
- this.filteredRecordCount = 0;
998
+ this.InternalRecords = [];
999
+ this.TotalRecordCount = 0;
1000
+ this.FilteredRecordCount = 0;
1068
1001
  }
1069
1002
  this.isInitialLoad = true;
1070
1003
  }
@@ -1078,12 +1011,12 @@ export class EntityViewerComponent extends BaseAngularComponent {
1078
1011
  /**
1079
1012
  * Load data for the current entity with server-side filtering/sorting/pagination
1080
1013
  */
1081
- async loadData() {
1082
- const entity = this.effectiveEntity;
1014
+ async LoadData() {
1015
+ const entity = this.EffectiveEntity;
1083
1016
  if (!entity) {
1084
- this.internalRecords = [];
1085
- this.totalRecordCount = 0;
1086
- this.filteredRecordCount = 0;
1017
+ this.InternalRecords = [];
1018
+ this.TotalRecordCount = 0;
1019
+ this.FilteredRecordCount = 0;
1087
1020
  return;
1088
1021
  }
1089
1022
  // Increment sequence to track this load request
@@ -1091,55 +1024,60 @@ export class EntityViewerComponent extends BaseAngularComponent {
1091
1024
  // If a load is already in progress, set a flag so we reload once the current
1092
1025
  // load completes. We can't use deferReload() here because the microtask would
1093
1026
  // fire while isLoading is still true, causing an infinite loop.
1094
- if (this.isLoading) {
1027
+ if (this.IsLoading) {
1095
1028
  this._pendingReload = true;
1096
1029
  return;
1097
1030
  }
1098
- this.isLoading = true;
1099
- this.pagination.isLoading = true;
1100
- this.loadingMessage = `Loading ${entity.Name}...`;
1031
+ this.IsLoading = true;
1032
+ this.Pagination.isLoading = true;
1033
+ this.LoadingMessage = `Loading ${entity.Name}...`;
1101
1034
  this.cdr.detectChanges();
1102
1035
  const startTime = Date.now();
1103
- const config = this.effectiveConfig;
1036
+ const config = this.EffectiveConfig;
1104
1037
  try {
1105
1038
  const rv = RunView.FromMetadataProvider(this.ProviderToUse);
1106
1039
  // Build OrderBy clause
1107
1040
  // Priority: 1) External/internal sort state 2) View's OrderByClause
1108
1041
  // 3) GridState.sortSettings (saved user defaults) 4) undefined
1109
1042
  let orderBy;
1110
- const sortState = this.effectiveSortState;
1043
+ const sortState = this.EffectiveSortState;
1111
1044
  if (config.serverSideSorting && sortState?.field && sortState.direction) {
1112
1045
  orderBy = `${sortState.field} ${sortState.direction.toUpperCase()}`;
1113
1046
  }
1114
- else if (this.viewEntity?.OrderByClause) {
1115
- orderBy = this.viewEntity.OrderByClause;
1047
+ else if (this.ViewEntity?.OrderByClause) {
1048
+ orderBy = this.ViewEntity.OrderByClause;
1116
1049
  }
1117
- else if (this.gridState?.sortSettings?.length) {
1118
- orderBy = this.gridState.sortSettings
1050
+ else if (this.GridState?.sortSettings?.length) {
1051
+ orderBy = this.GridState.sortSettings
1119
1052
  .map(s => `${s.field} ${(s.dir || 'asc').toUpperCase()}`)
1120
1053
  .join(', ');
1121
1054
  }
1122
- // Map mode loads all records (up to MAP_MAX_RECORDS) since paging
1123
- // doesn't make sense for geographic visualization. Other modes use
1124
- // standard page-based pagination.
1125
- const isMapMode = this.effectiveViewMode === 'map';
1126
- const maxRows = isMapMode ? EntityViewerComponent.MAP_MAX_RECORDS : config.pageSize;
1127
- const startRow = isMapMode ? 0 : this.pagination.currentPage * config.pageSize;
1055
+ // When a plug-in renderer has asked the container to load the full set (via a generic
1056
+ // ViewDataRequest with loadAll:true), load all records up to the safety cap and skip
1057
+ // pagination. Otherwise use standard page-based pagination. This is fully view-type-agnostic
1058
+ // the container never inspects which plug-in is mounted.
1059
+ const maxRows = this._loadAllRecords ? EntityViewerComponent.LOAD_ALL_MAX_RECORDS : config.pageSize;
1060
+ const startRow = this._loadAllRecords ? 0 : this.Pagination.currentPage * config.pageSize;
1128
1061
  // Build ExtraFilter from view's WhereClause if available
1129
1062
  // The view's WhereClause is the "business filter" - UserSearchString is additive
1130
- const extraFilter = this.viewEntity?.WhereClause || undefined;
1063
+ const extraFilter = this.ViewEntity?.WhereClause || undefined;
1131
1064
  const result = await rv.RunView({
1132
1065
  EntityName: entity.Name,
1133
1066
  ResultType: 'simple',
1134
- Fields: computeFieldsList(entity, this.gridState),
1067
+ // Load the FULL field set. The container is a generic plug-in host: different view types need
1068
+ // different fields (the grid shows its columns, but Timeline needs the date field, Map needs
1069
+ // lat/long, etc.). It cannot restrict to any one plug-in's columns, so it fetches all fields and
1070
+ // lets each plug-in pick what it needs. (Omitting `Fields` on a 'simple' RunView returns all
1071
+ // entity fields.) Previously this used `computeFieldsList(entity, GridState)` — correct when the
1072
+ // host WAS the grid, but it dropped `__mj_*` date fields, leaving Timeline with "no events".
1135
1073
  MaxRows: maxRows,
1136
1074
  StartRow: startRow,
1137
1075
  OrderBy: orderBy,
1138
1076
  ExtraFilter: extraFilter,
1139
1077
  // Only use UserSearchString for regular text search, NOT for smart filters
1140
1078
  // Smart filters generate WhereClause via AI on the server, so the prompt text should not be passed as UserSearchString
1141
- UserSearchString: config.serverSideFiltering && !this.viewEntity?.SmartFilterEnabled
1142
- ? this.debouncedFilterText || undefined
1079
+ UserSearchString: config.serverSideFiltering && !this.ViewEntity?.SmartFilterEnabled
1080
+ ? this.DebouncedFilterText || undefined
1143
1081
  : undefined
1144
1082
  });
1145
1083
  // Check if this load is still the current one (detect stale responses)
@@ -1148,50 +1086,47 @@ export class EntityViewerComponent extends BaseAngularComponent {
1148
1086
  }
1149
1087
  if (result.Success) {
1150
1088
  // Always replace records (page-based navigation, not accumulation)
1151
- this.internalRecords = result.Results;
1152
- this.totalRecordCount = result.TotalRowCount;
1153
- this.filteredRecordCount = this.internalRecords.length;
1089
+ this.InternalRecords = result.Results;
1090
+ this.TotalRecordCount = result.TotalRowCount;
1091
+ this.FilteredRecordCount = this.InternalRecords.length;
1154
1092
  // Update pagination state
1155
- this.pagination.totalRecords = result.TotalRowCount;
1156
- this.pagination.hasMore = false; // No longer used with page-based paging
1157
- // Re-check geo support after data loads (effectiveEntity may have resolved via viewEntity)
1158
- this.updateGeoCodingSupport();
1159
- this.dataLoaded.emit({
1093
+ this.Pagination.totalRecords = result.TotalRowCount;
1094
+ this.Pagination.hasMore = false; // No longer used with page-based paging
1095
+ this.DataLoaded.emit({
1160
1096
  totalRowCount: result.TotalRowCount,
1161
- loadedRowCount: this.internalRecords.length,
1097
+ loadedRowCount: this.InternalRecords.length,
1162
1098
  loadTime: Date.now() - startTime,
1163
- records: this.internalRecords
1099
+ records: this.InternalRecords
1164
1100
  });
1165
- this.filteredCountChanged.emit({
1166
- filteredCount: this.internalRecords.length,
1101
+ this.FilteredCountChanged.emit({
1102
+ filteredCount: this.InternalRecords.length,
1167
1103
  totalCount: result.TotalRowCount
1168
1104
  });
1169
- // Update timeline groups with new data
1170
- this.updateTimelineGroups();
1171
1105
  }
1172
1106
  else {
1173
1107
  if (this.isInitialLoad) {
1174
- this.internalRecords = [];
1108
+ this.InternalRecords = [];
1175
1109
  }
1176
- this.totalRecordCount = 0;
1177
- this.filteredRecordCount = 0;
1110
+ this.TotalRecordCount = 0;
1111
+ this.FilteredRecordCount = 0;
1178
1112
  }
1179
1113
  }
1180
1114
  catch (error) {
1181
1115
  if (this.isInitialLoad) {
1182
- this.internalRecords = [];
1116
+ this.InternalRecords = [];
1183
1117
  }
1184
- this.totalRecordCount = 0;
1185
- this.filteredRecordCount = 0;
1118
+ this.TotalRecordCount = 0;
1119
+ this.FilteredRecordCount = 0;
1186
1120
  }
1187
1121
  finally {
1188
1122
  // Use ngZone.run() to ensure state changes trigger change detection.
1189
1123
  // With es2022 native async/await + zone.js 0.16, the await resumes
1190
1124
  // outside Angular's zone, so detectChanges() alone may not flush properly.
1191
1125
  this.ngZone.run(() => {
1192
- this.isLoading = false;
1193
- this.pagination.isLoading = false;
1126
+ this.IsLoading = false;
1127
+ this.Pagination.isLoading = false;
1194
1128
  this.isInitialLoad = false;
1129
+ this.pushDynamicRendererInputs(); // keep a mounted plug-in renderer in sync with new data
1195
1130
  this.cdr.detectChanges();
1196
1131
  });
1197
1132
  // If a reload was requested while we were loading, trigger it now.
@@ -1199,699 +1134,660 @@ export class EntityViewerComponent extends BaseAngularComponent {
1199
1134
  if (this._pendingReload) {
1200
1135
  this._pendingReload = false;
1201
1136
  this.resetPaginationState();
1202
- this.loadData();
1137
+ this.LoadData();
1203
1138
  }
1204
1139
  }
1205
1140
  }
1206
1141
  /**
1207
1142
  * Handle page change from PaginationComponent
1208
1143
  */
1209
- onPageChange(event) {
1210
- this.pagination.currentPage = event.PageNumber - 1; // Convert 1-based to 0-based
1144
+ OnPageChange(event) {
1145
+ this.Pagination.currentPage = event.PageNumber - 1; // Convert 1-based to 0-based
1211
1146
  this.isInitialLoad = true; // Treat page navigation as a fresh load for loading state
1212
- this.loadData();
1147
+ this.LoadData();
1213
1148
  }
1214
1149
  /**
1215
1150
  * Refresh data (re-load from server, starting at page 1)
1216
1151
  * Keeps existing records visible during refresh for better UX
1217
1152
  */
1218
- refresh() {
1219
- if (!this.records) {
1153
+ Refresh() {
1154
+ if (!this.Records) {
1220
1155
  this.resetPaginationState(false);
1221
- this.loadData();
1156
+ this.LoadData();
1222
1157
  }
1223
1158
  }
1224
1159
  // ========================================
1225
1160
  // VIEW MODE
1226
1161
  // ========================================
1227
1162
  /**
1228
- * Set the view mode and emit change event
1229
- */
1230
- setViewMode(mode) {
1231
- const previousMode = this.effectiveViewMode;
1232
- if (previousMode !== mode) {
1233
- this.internalViewMode = mode;
1234
- this.viewModeChange.emit(mode);
1235
- // Reload data when switching to/from map mode because map loads all
1236
- // records (up to MAP_MAX_RECORDS) while other modes use page-based pagination.
1237
- const switchingToMap = mode === 'map' && previousMode !== 'map';
1238
- const switchingFromMap = mode !== 'map' && previousMode === 'map';
1239
- if (switchingToMap || switchingFromMap) {
1240
- this.resetPaginationState();
1241
- this.loadData();
1242
- }
1243
- else {
1244
- this.cdr.detectChanges();
1245
- }
1246
- }
1247
- }
1248
- // ========================================
1249
- // FILTERING
1250
- // ========================================
1251
- /**
1252
- * Handle filter input change
1253
- */
1254
- onFilterChange(value) {
1255
- this.internalFilterText = value;
1256
- this.filterInput$.next(value);
1257
- }
1258
- /**
1259
- * Clear the filter
1163
+ * Loads the ViewTypeEngine once, then recomputes the available view types for the
1164
+ * current entity. Fire-and-forget from lifecycle/setters — failures simply leave the
1165
+ * switcher empty until the registry is available.
1260
1166
  */
1261
- clearFilter() {
1262
- this.internalFilterText = '';
1263
- this.filterInput$.next('');
1264
- this.cdr.detectChanges();
1265
- }
1266
- // ========================================
1267
- // SORTING
1268
- // ========================================
1269
- /**
1270
- * Handle sort change from grid component
1271
- */
1272
- onSortChanged(event) {
1273
- const oldSort = this.internalSortState;
1274
- this.internalSortState = event.sort;
1275
- this.sortChanged.emit(event);
1276
- // If server-side sorting, reload from page 1
1277
- // Keep existing records visible during refresh for better UX
1278
- if (this.effectiveConfig.serverSideSorting && !this.records) {
1279
- const sortChanged = !oldSort || !event.sort ||
1280
- oldSort.field !== event.sort?.field ||
1281
- oldSort.direction !== event.sort?.direction;
1282
- if (sortChanged) {
1283
- this.resetPaginationState(false);
1284
- this.loadData();
1285
- }
1167
+ async ensureViewTypesLoaded() {
1168
+ try {
1169
+ const provider = this.ProviderToUse;
1170
+ await ViewTypeEngine.Instance.Config(false, provider?.CurrentUser, provider ?? undefined);
1171
+ // Let plug-in descriptors (e.g. Cluster) preload availability data before predicates run.
1172
+ await ViewTypeEngine.Instance.EnsureAvailabilityData(provider ?? undefined);
1173
+ this.refreshAvailableViewTypes();
1174
+ }
1175
+ catch {
1176
+ // Engine unavailable / not seeded — leave the switcher empty.
1177
+ this.ViewTypesFromRegistry = false;
1286
1178
  }
1287
1179
  }
1288
- // ========================================
1289
- // EVENT HANDLERS
1290
- // ========================================
1291
1180
  /**
1292
- * Handle record selection from child components (grid or cards)
1181
+ * Recomputes {@link AvailableViewTypes} from the registry for the current entity. Every
1182
+ * available view type is a dynamic-mounted plug-in. Reconciles the active selection when the
1183
+ * previously-active type is no longer available (e.g. the entity changed).
1293
1184
  */
1294
- onRecordSelected(event) {
1295
- this.recordSelected.emit(event);
1185
+ refreshAvailableViewTypes() {
1186
+ const entity = this.EffectiveEntity;
1187
+ if (!entity) {
1188
+ this.AvailableViewTypes = [];
1189
+ this.ViewTypesFromRegistry = false;
1190
+ return;
1191
+ }
1192
+ // Self-contained config seeding: pull per-view-type config from the loaded view record or
1193
+ // the per-user default-view setting when the consumer didn't supply it via [ViewTypeConfigs].
1194
+ this.seedViewTypeConfigsIfEmpty();
1195
+ let rows = [];
1196
+ try {
1197
+ rows = ViewTypeEngine.Instance.GetAvailableViewTypeRows(entity, this.ProviderToUse ?? undefined);
1198
+ }
1199
+ catch {
1200
+ rows = [];
1201
+ }
1202
+ const options = [];
1203
+ for (const { ViewType, Descriptor } of rows) {
1204
+ options.push({
1205
+ key: Descriptor.Name,
1206
+ mode: null, // every view type is dynamic-mounted
1207
+ label: Descriptor.DisplayName,
1208
+ icon: Descriptor.Icon ?? '',
1209
+ isDynamic: true,
1210
+ descriptor: Descriptor,
1211
+ viewTypeId: ViewType.ID,
1212
+ });
1213
+ }
1214
+ this.AvailableViewTypes = options;
1215
+ this.ViewTypesFromRegistry = options.length > 0;
1216
+ // Keep the active key valid: if the previously-active type is no longer available
1217
+ // (entity changed), select — in priority order — the resolved initial ViewTypeID
1218
+ // (explicit input → loaded view record → per-user default-view setting), then the first
1219
+ // available type.
1220
+ if (options.length > 0 && !options.some(o => o.key === this.ActiveViewTypeKey)) {
1221
+ const desiredId = this.resolveInitialViewTypeId();
1222
+ const desired = desiredId ? options.find(o => o.viewTypeId === desiredId) : undefined;
1223
+ const fallback = desired ?? options[0];
1224
+ this.applyViewTypeSelection(fallback, false);
1225
+ }
1226
+ this.cdr.detectChanges();
1296
1227
  }
1297
1228
  /**
1298
- * Handle record opened from child components (grid or cards)
1229
+ * Returns the switcher option for the active view type (for the current-type chip in the
1230
+ * switcher), or null before the registry resolves.
1299
1231
  */
1300
- onRecordOpened(event) {
1301
- this.recordOpened.emit(event);
1232
+ get ActiveViewTypeOption() {
1233
+ return this.AvailableViewTypes.find(o => o.key === this.ActiveViewTypeKey) ?? null;
1302
1234
  }
1303
1235
  /**
1304
- * Handle grid state changes (column resize, reorder, etc.)
1236
+ * Selects a view type from the switcher. Built-in types route to {@link setViewMode}
1237
+ * (preserving the rich grid/cards/timeline/map integration); plug-in types are
1238
+ * dynamic-mounted via the descriptor's RendererComponent. Emits {@link viewTypeChange}
1239
+ * so hosts can persist `UserView.ViewTypeID`.
1305
1240
  */
1306
- onGridStateChanged(event) {
1307
- this.gridStateChanged.emit(event);
1241
+ SelectViewType(option) {
1242
+ this.ViewTypeDropdownOpen = false;
1243
+ this.applyViewTypeSelection(option, true);
1308
1244
  }
1309
1245
  /**
1310
- * Handle page change from the data grid's pager
1246
+ * Selects a view type by its `MJ: View Types` row ID. This is the entry point used by external
1247
+ * chrome (e.g. the workspace toolbar's {@link ViewTypeSwitcherComponent}) that surfaces the
1248
+ * switcher outside the viewer's own header. Resolves the matching available option and applies
1249
+ * it through the same lifecycle as a header-driven switch (cancelable events + persistence).
1250
+ * No-op when the ID isn't an available view type or is already active.
1251
+ *
1252
+ * @param viewTypeId the `MJ: View Types` row ID to switch to.
1311
1253
  */
1312
- onGridPageChange(event) {
1313
- this.onPageChange(event);
1314
- }
1315
- // ========================================
1316
- // DATA GRID EVENT HANDLERS
1317
- // ========================================
1318
- /**
1319
- * Handle row click from mj-entity-data-grid
1320
- * Maps to recordSelected event for parent components
1321
- */
1322
- onDataGridRowClick(event) {
1323
- const entity = this.effectiveEntity;
1324
- if (!entity || !event.row)
1254
+ SelectViewTypeById(viewTypeId) {
1255
+ if (!viewTypeId) {
1325
1256
  return;
1326
- this.recordSelected.emit({
1327
- record: event.row,
1328
- entity: entity,
1329
- compositeKey: buildCompositeKey(event.row, entity)
1330
- });
1257
+ }
1258
+ const option = this.AvailableViewTypes.find(o => o.viewTypeId === viewTypeId);
1259
+ if (option && option.key !== this.ActiveViewTypeKey) {
1260
+ this.applyViewTypeSelection(option, true);
1261
+ }
1331
1262
  }
1332
1263
  /**
1333
- * Handle row double-click from mj-entity-data-grid
1334
- * Maps to recordOpened event for parent components
1264
+ * The `MJ: View Types` row ID of the currently-active view type, or null before the registry
1265
+ * resolves. Lets external chrome (the workspace toolbar switcher) reflect the viewer's active
1266
+ * type without reaching into the viewer's internal {@link ViewModeOption}s.
1335
1267
  */
1336
- onDataGridRowDoubleClick(event) {
1337
- const entity = this.effectiveEntity;
1338
- if (!entity || !event.row)
1339
- return;
1340
- this.recordOpened.emit({
1341
- record: event.row,
1342
- entity: entity,
1343
- compositeKey: buildCompositeKey(event.row, entity)
1344
- });
1268
+ get ActiveViewTypeId() {
1269
+ return this.ActiveViewTypeOption?.viewTypeId ?? null;
1345
1270
  }
1346
1271
  /**
1347
- * Handle sort changed from mj-entity-data-grid
1348
- * Maps to sortChanged event for parent components
1272
+ * Applies a view-type selection. `emit` is true for user-initiated switches (fires the
1273
+ * cancelable {@link BeforeViewTypeChange}, persists when {@link AutoSaveView} is on, then fires
1274
+ * {@link AfterViewTypeChange}) and false for internal reconciliation (no events, no persist).
1275
+ *
1276
+ * Every view type is a dynamic-mounted plug-in: this tears down the previous renderer and mounts
1277
+ * the selected descriptor's `RendererComponent`. The container resets its generic load mode (a
1278
+ * fresh plug-in that needs the full set will re-request it via `dataRequest({loadAll:true})`) and
1279
+ * reloads page-based data when the active type changes.
1349
1280
  */
1350
- onDataGridSortChanged(event) {
1351
- // Convert the data grid's sort state to our SortState format
1352
- const newSort = event.newSortState && event.newSortState.length > 0
1353
- ? {
1354
- field: event.newSortState[0].field,
1355
- direction: event.newSortState[0].direction
1281
+ applyViewTypeSelection(option, emit) {
1282
+ const previousViewTypeId = this.ActiveViewTypeOption?.viewTypeId ?? null;
1283
+ if (emit) {
1284
+ const before = {
1285
+ ViewTypeID: option.viewTypeId,
1286
+ DriverClass: option.key,
1287
+ PreviousViewTypeID: previousViewTypeId,
1288
+ Cancel: false,
1289
+ };
1290
+ this.BeforeViewTypeChange.emit(before);
1291
+ if (before.Cancel) {
1292
+ return; // a handler vetoed the switch — leave the switcher unchanged
1356
1293
  }
1357
- : null;
1358
- this.internalSortState = newSort;
1359
- this.sortChanged.emit({ sort: newSort });
1360
- // If server-side sorting, reload from page 1.
1361
- // Use deferReload() so that if a view-switch reload is already in-flight
1362
- // (e.g., AG Grid fired an async sortChanged from applySortStateToGrid),
1363
- // we don't trigger a redundant second RunView.
1364
- // For normal user-initiated column-header clicks, no deferred reload is
1365
- // pending so deferReload() fires immediately no UX difference.
1366
- if (this.effectiveConfig.serverSideSorting && !this.records) {
1294
+ }
1295
+ const typeChanged = this.ActiveViewTypeKey !== option.key;
1296
+ this.ActiveViewTypeKey = option.key;
1297
+ this.ActiveDynamicOption = option;
1298
+ this.selectDynamicRenderer(option);
1299
+ // On a real type switch, drop any prior load-all mode and reload page-based data so the newly
1300
+ // mounted plug-in starts from the standard paginated set. A plug-in needing everything (e.g. a
1301
+ // map) re-asks via dataRequest on its own init. Skip the reload for the initial reconciliation
1302
+ // (no prior type) the regular entity/view data-load path already handles the first load.
1303
+ if (typeChanged && previousViewTypeId !== null && this._loadAllRecords && !this._records) {
1304
+ this._loadAllRecords = false;
1367
1305
  this.resetPaginationState(false);
1368
- this.deferReload();
1306
+ this.LoadData();
1369
1307
  }
1370
- }
1371
- /**
1372
- * Handle foreign key link click from mj-entity-data-grid
1373
- * Bubbles the event up for parent components to handle navigation
1374
- */
1375
- /**
1376
- * Handle foreign key link click from mj-entity-data-grid
1377
- * Converts to recordOpened event for seamless navigation integration
1378
- */
1379
- onForeignKeyClick(event) {
1380
- // Look up the related entity by name
1381
- const md = this.ProviderToUse;
1382
- const relatedEntity = event.relatedEntityName
1383
- ? md.Entities.find(e => e.Name === event.relatedEntityName)
1384
- : md.Entities.find(e => UUIDsEqual(e.ID, event.relatedEntityId));
1385
- if (!relatedEntity) {
1386
- return;
1308
+ if (emit) {
1309
+ if (this.AutoSaveView) {
1310
+ void this.persistActiveViewType(option);
1311
+ }
1312
+ this.AfterViewTypeChange.emit({
1313
+ ViewTypeID: option.viewTypeId,
1314
+ DriverClass: option.key,
1315
+ PreviousViewTypeID: previousViewTypeId,
1316
+ Cancel: false,
1317
+ });
1387
1318
  }
1388
- // Create composite key using the target entity's actual primary key field name
1389
- const pkFieldName = relatedEntity.FirstPrimaryKey?.Name || 'ID';
1390
- const compositeKey = new CompositeKey([{ FieldName: pkFieldName, Value: event.recordId }]);
1391
- // Emit recordOpened for the related entity (record is undefined since it's not loaded)
1392
- this.recordOpened.emit({
1393
- entity: relatedEntity,
1394
- compositeKey
1395
- });
1396
- }
1397
- /**
1398
- * Handle Add/New button click from data grid toolbar
1399
- */
1400
- onGridAddRequested() {
1401
- this.addRequested.emit();
1402
- }
1403
- /**
1404
- * Handle Refresh button click from data grid toolbar
1405
- */
1406
- onGridRefreshRequested() {
1407
- this.refreshRequested.emit();
1408
- // Also trigger an internal refresh
1409
- this.refresh();
1410
- }
1411
- /**
1412
- * Handle Delete button click from data grid toolbar
1413
- */
1414
- onGridDeleteRequested(records) {
1415
- this.deleteRequested.emit({ records });
1416
- }
1417
- /**
1418
- * Handle Export button click from data grid toolbar
1419
- */
1420
- onGridExportRequested() {
1421
- this.exportRequested.emit({ format: 'excel' });
1422
- }
1423
- /**
1424
- * Handle Add to List button click from data grid toolbar.
1425
- * Forwards the event to parent components for list management.
1426
- */
1427
- onGridAddToListRequested(event) {
1428
- this.addToListRequested.emit(event);
1429
- }
1430
- /**
1431
- * Handle selection change from data grid.
1432
- * Converts selected keys to records and forwards to parent components.
1433
- */
1434
- onGridSelectionChange(selectedKeys) {
1435
- const entity = this.effectiveEntity;
1436
- if (!entity)
1437
- return;
1438
- // Find the actual records from our filtered records
1439
- const records = this.filteredRecords.filter(record => {
1440
- const key = buildPkString(record, entity);
1441
- return selectedKeys.includes(key);
1442
- });
1443
- // Get the raw primary key values for list management
1444
- const recordIds = records.map(record => String(record[entity.PrimaryKeys[0].Name]));
1445
- this.selectionChanged.emit({ records, recordIds });
1319
+ this.cdr.detectChanges();
1446
1320
  }
1447
1321
  // ========================================
1448
- // TIMELINE METHODS
1322
+ // VIEW-TYPE PERSISTENCE (provider-based; never routing)
1449
1323
  // ========================================
1450
1324
  /**
1451
- * Handle timeline event click - emit as record selection
1325
+ * Where view-type/config persists when {@link AutoSaveView} is on: a loaded `UserView` record,
1326
+ * per-user User Settings (the default-view case), or nowhere.
1452
1327
  */
1453
- onTimelineEventClick(event) {
1454
- const record = event.event.entity;
1455
- const entity = this.effectiveEntity;
1456
- if (record && entity) {
1457
- this.recordSelected.emit({
1458
- record,
1459
- entity: entity,
1460
- compositeKey: buildCompositeKey(record, entity)
1461
- });
1328
+ persistenceTarget() {
1329
+ if (this._viewEntity?.ID) {
1330
+ return 'record';
1462
1331
  }
1332
+ if (this.EffectiveEntity) {
1333
+ return 'user-settings';
1334
+ }
1335
+ return 'none';
1463
1336
  }
1464
- /**
1465
- * Update HasGeoCoding based on the current effectiveEntity.
1466
- * Called from entity setter and after data loads (when effectiveEntity may resolve via viewEntity).
1467
- */
1468
- updateGeoCodingSupport() {
1469
- const entity = this.effectiveEntity;
1470
- const newValue = !!(entity && entity.SupportsGeoCoding);
1471
- if (newValue !== this.HasGeoCoding) {
1472
- this.HasGeoCoding = newValue;
1473
- this.fallbackFromMapIfNeeded();
1474
- this.cdr.detectChanges();
1337
+ /** Persist the active view-type selection to the resolved target. */
1338
+ async persistActiveViewType(option) {
1339
+ const target = this.persistenceTarget();
1340
+ if (target === 'record') {
1341
+ const ve = this._viewEntity;
1342
+ ve.ViewTypeID = option.viewTypeId;
1343
+ await this.saveViewEntity(ve);
1344
+ }
1345
+ else if (target === 'user-settings') {
1346
+ this.saveDefaultViewSetting();
1475
1347
  }
1476
1348
  }
1477
- /**
1478
- * Handle map marker click — emit the record for the parent to handle (open record, etc.)
1479
- */
1480
- onMapMarkerClick(event) {
1481
- const entity = this.effectiveEntity;
1482
- if (event.Record && entity) {
1483
- const compositeKey = buildCompositeKey(event.Record, entity);
1484
- // Emit both recordSelected (for detail panels) and recordOpened (for navigation)
1485
- this.recordSelected.emit({
1486
- record: event.Record,
1487
- entity: entity,
1488
- compositeKey
1489
- });
1490
- this.recordOpened.emit({
1491
- record: event.Record,
1492
- entity: entity,
1493
- compositeKey
1494
- });
1349
+ /** Persist a per-view-type config change to the resolved target. */
1350
+ async persistViewTypeConfig(viewTypeId) {
1351
+ const target = this.persistenceTarget();
1352
+ if (target === 'record') {
1353
+ const ve = this._viewEntity;
1354
+ const displayState = ve.DisplayStateObject ?? { defaultMode: 'grid' };
1355
+ const configs = (displayState.viewTypeConfigs ?? []).filter(c => c.viewTypeId !== viewTypeId);
1356
+ configs.push({ viewTypeId, config: this.viewTypeConfigById.get(viewTypeId) ?? {} });
1357
+ displayState.viewTypeConfigs = configs;
1358
+ ve.DisplayStateObject = displayState;
1359
+ await this.saveViewEntity(ve);
1360
+ }
1361
+ else if (target === 'user-settings') {
1362
+ this.saveDefaultViewSetting();
1495
1363
  }
1496
1364
  }
1497
- /** Map display state (zoom, center) passed from parent for persistence across reloads. */
1498
- mapDisplayState = null;
1499
- /** Map render mode — separate from DisplayState for clear single-source-of-truth. */
1500
- mapRenderMode = 'point';
1501
- /** Emitted when the map's display state changes (zoom, center). */
1502
- mapDisplayStateChange = new EventEmitter();
1503
- /** Emitted when the map's render mode changes (user clicks mode buttons). */
1504
- mapRenderModeChange = new EventEmitter();
1505
- /**
1506
- * Handle map display state changes — bubble up to parent for persistence.
1507
- */
1508
- onMapDisplayStateChange(state) {
1509
- this.mapDisplayStateChange.emit(state);
1365
+ /** Save the loaded view record, logging (not throwing) on failure. */
1366
+ async saveViewEntity(viewEntity) {
1367
+ const saved = await viewEntity.Save();
1368
+ if (!saved) {
1369
+ LogError(`EntityViewer: failed to persist view: ${viewEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1370
+ }
1510
1371
  }
1511
- onMapRenderModeChange(mode) {
1512
- this.mapRenderModeChange.emit(mode);
1372
+ /** User Settings key for this entity's per-user default-view state. */
1373
+ defaultViewSettingKey() {
1374
+ const entity = this.EffectiveEntity;
1375
+ return entity ? `mj.entityViewer.${entity.ID.toLowerCase()}.view` : null;
1513
1376
  }
1514
- /**
1515
- * Toggle timeline orientation between vertical and horizontal
1516
- */
1517
- toggleTimelineOrientation() {
1518
- this.timelineOrientation = this.timelineOrientation === 'vertical' ? 'horizontal' : 'vertical';
1519
- // Emit config change so parent can persist the preference
1520
- this.emitTimelineConfigChange();
1521
- this.cdr.detectChanges();
1377
+ /** Persist the per-user default-view state (active view type + per-type configs) to User Settings. */
1378
+ saveDefaultViewSetting() {
1379
+ const key = this.defaultViewSettingKey();
1380
+ if (!key) {
1381
+ return;
1382
+ }
1383
+ const payload = {
1384
+ viewTypeId: this.ActiveViewTypeOption?.viewTypeId ?? null,
1385
+ viewTypeConfigs: this.serializeViewTypeConfigs(),
1386
+ };
1387
+ UserInfoEngine.Instance.SetSettingDebounced(key, JSON.stringify(payload));
1522
1388
  }
1523
- /**
1524
- * Toggle timeline sort order between newest first (desc) and oldest first (asc)
1525
- */
1526
- toggleTimelineSortOrder() {
1527
- this.timelineSortOrder = this.timelineSortOrder === 'desc' ? 'asc' : 'desc';
1528
- // Emit config change so parent can persist the preference
1529
- this.emitTimelineConfigChange();
1530
- this.cdr.detectChanges();
1389
+ /** Convert the in-memory per-view-type config map to the persisted array shape. */
1390
+ serializeViewTypeConfigs() {
1391
+ return Array.from(this.viewTypeConfigById.entries()).map(([viewTypeId, config]) => ({ viewTypeId, config }));
1531
1392
  }
1532
1393
  /**
1533
- * Change the date field used for the timeline
1394
+ * Resolve which view type the viewer should open in, in priority order:
1395
+ * explicit `ViewTypeID` input → the loaded `UserView` record's `ViewTypeID` → the per-user
1396
+ * default-view setting (User Settings). Returns null to let the caller fall back to the legacy
1397
+ * mode / first available type. This is how the viewer is self-contained for both the saved-view
1398
+ * and default-view cases without the consumer wiring anything.
1534
1399
  */
1535
- setTimelineDateField(fieldName) {
1536
- if (this.availableDateFields.some(f => f.Name === fieldName)) {
1537
- this.selectedTimelineDateField = fieldName;
1538
- this.updateTimelineGroups();
1539
- this.emitTimelineConfigChange();
1540
- this.cdr.detectChanges();
1400
+ resolveInitialViewTypeId() {
1401
+ if (this._initialViewTypeId) {
1402
+ return this._initialViewTypeId;
1403
+ }
1404
+ if (this._viewEntity?.ViewTypeID) {
1405
+ return this._viewEntity.ViewTypeID;
1541
1406
  }
1407
+ return this.readDefaultViewSetting()?.viewTypeId ?? null;
1542
1408
  }
1543
1409
  /**
1544
- * Get the display name of the currently selected timeline date field
1410
+ * Load a saved view by ID via the active provider and apply it through the {@link viewEntity}
1411
+ * setter. Backs the {@link ViewID} convenience input. Logs (does not throw) on failure.
1545
1412
  */
1546
- get selectedDateFieldDisplayName() {
1547
- if (!this.selectedTimelineDateField)
1548
- return '';
1549
- const field = this.availableDateFields.find(f => f.Name === this.selectedTimelineDateField);
1550
- return field?.DisplayNameOrName || this.selectedTimelineDateField;
1413
+ async loadViewById(viewId) {
1414
+ const provider = this.ProviderToUse;
1415
+ if (!provider) {
1416
+ return;
1417
+ }
1418
+ const view = await provider.GetEntityObject('MJ: User Views', provider.CurrentUser);
1419
+ const loaded = await view.Load(viewId);
1420
+ if (loaded) {
1421
+ this.ViewEntity = view;
1422
+ }
1423
+ else {
1424
+ LogError(`EntityViewer: failed to load view ${viewId}: ${view.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1425
+ }
1551
1426
  }
1552
- /**
1553
- * Emit the current timeline configuration for persistence
1554
- */
1555
- emitTimelineConfigChange() {
1556
- if (this.selectedTimelineDateField) {
1557
- this.timelineConfigChange.emit({
1558
- dateFieldName: this.selectedTimelineDateField,
1559
- sortOrder: this.timelineSortOrder,
1560
- segmentGrouping: this.timelineSegmentGrouping,
1561
- orientation: this.timelineOrientation
1562
- });
1427
+ /** Read the per-user default-view setting for the current entity (or null). */
1428
+ readDefaultViewSetting() {
1429
+ const key = this.defaultViewSettingKey();
1430
+ if (!key) {
1431
+ return null;
1432
+ }
1433
+ const raw = UserInfoEngine.Instance.GetSetting(key);
1434
+ if (!raw) {
1435
+ return null;
1436
+ }
1437
+ try {
1438
+ return JSON.parse(raw);
1439
+ }
1440
+ catch {
1441
+ return null;
1563
1442
  }
1564
1443
  }
1565
1444
  /**
1566
- * Detect and configure timeline based on entity's date fields
1567
- * Called when entity changes
1445
+ * Seed the in-memory per-view-type config map from the available sources when the consumer
1446
+ * hasn't supplied them via the {@link ViewTypeConfigs} input: the loaded `UserView` record's
1447
+ * `DisplayState.viewTypeConfigs` (saved-view case) or the per-user default-view setting.
1448
+ * Only seeds when the map is empty, so an explicit input always wins.
1568
1449
  */
1569
- detectDateFields() {
1570
- if (!this.entity) {
1571
- this.hasDateFields = false;
1572
- this.availableDateFields = [];
1573
- this.timelineGroups = [];
1574
- this.fallbackFromTimelineIfNeeded();
1450
+ seedViewTypeConfigsIfEmpty() {
1451
+ if (this.viewTypeConfigById.size > 0) {
1575
1452
  return;
1576
1453
  }
1577
- // Find all date fields - include __mj_CreatedAt and __mj_UpdatedAt as they're useful for timelines
1578
- const dateFields = this.entity.Fields.filter(f => f.TSType === EntityFieldTSType.Date);
1579
- if (dateFields.length === 0) {
1580
- this.hasDateFields = false;
1581
- this.availableDateFields = [];
1582
- this.timelineGroups = [];
1583
- this.fallbackFromTimelineIfNeeded();
1584
- return;
1454
+ const fromRecord = this._viewEntity?.DisplayStateObject?.viewTypeConfigs;
1455
+ const source = fromRecord ?? this.readDefaultViewSetting()?.viewTypeConfigs ?? [];
1456
+ for (const entry of source) {
1457
+ if (entry?.viewTypeId) {
1458
+ this.viewTypeConfigById.set(entry.viewTypeId, entry.config ?? {});
1459
+ }
1585
1460
  }
1586
- // Sort by priority: DefaultInView date fields first (by Sequence), then others (by Sequence)
1587
- this.availableDateFields = this.sortDateFieldsByPriority(dateFields);
1588
- this.hasDateFields = true;
1589
- // Configure timeline with the best date field
1590
- this.configureTimeline();
1591
1461
  }
1462
+ // ========================================
1463
+ // DYNAMIC (PLUG-IN) VIEW RENDERER MOUNTING
1464
+ // ========================================
1592
1465
  /**
1593
- * If currently on timeline view but timeline is no longer available,
1594
- * fall back to grid view
1466
+ * Makes `option`'s plug-in the ACTIVE view: reuses its cached instance when present (preserving
1467
+ * state), otherwise creates it. All other cached instances are hidden (kept mounted). The container
1468
+ * has zero knowledge of which plug-in this is — it only knows the {@link IViewRenderer} contract.
1595
1469
  */
1596
- fallbackFromTimelineIfNeeded() {
1597
- if (this.effectiveViewMode === 'timeline' && !this.hasDateFields) {
1598
- this.setViewMode('grid');
1470
+ selectDynamicRenderer(option) {
1471
+ const host = this.dynamicViewHost;
1472
+ if (!host || !option.descriptor.RendererComponent) {
1473
+ return;
1474
+ }
1475
+ // Hide every cached instance; the active one is shown below. Hidden instances stay mounted so
1476
+ // their state survives a round-trip (e.g. switch Grid → Cluster → Grid without re-clustering).
1477
+ for (const entry of this.dynamicRendererCache.values()) {
1478
+ this.setRendererVisible(entry.ref, false);
1479
+ }
1480
+ let entry = this.dynamicRendererCache.get(option.viewTypeId);
1481
+ if (!entry) {
1482
+ entry = this.createDynamicRenderer(option);
1483
+ }
1484
+ this.dynamicRendererRef = entry.ref;
1485
+ this.dynamicInputNames = entry.inputs;
1486
+ this.setRendererVisible(entry.ref, true);
1487
+ this.pushDynamicRendererInputs();
1488
+ entry.ref.changeDetectorRef.detectChanges();
1489
+ }
1490
+ /**
1491
+ * Creates a plug-in renderer instance, wires its generic outputs, captures its declared inputs,
1492
+ * and caches it by `MJ: View Types` row ID. (Does not show/activate it — {@link selectDynamicRenderer}
1493
+ * does that.)
1494
+ */
1495
+ createDynamicRenderer(option) {
1496
+ const host = this.dynamicViewHost;
1497
+ const componentType = option.descriptor.RendererComponent;
1498
+ const ref = host.createComponent(componentType);
1499
+ // Capture the set of input names this plug-in actually declares, so we only push the generic
1500
+ // inputs it accepts. A lean plug-in (e.g. Timeline/Cards) won't declare grid-only inputs like
1501
+ // `totalRecordCount`/`page` — calling `setInput` for those logs NG0303 in dev (the log fires
1502
+ // before the throw, so a try/catch can't suppress it). Pre-checking with the reflected metadata
1503
+ // keeps the console clean and the container plug-in-agnostic.
1504
+ const inputs = new Set(reflectComponentType(componentType)?.inputs.map(i => i.templateName) ?? []);
1505
+ // Wire the generic IViewRenderer outputs. All are optional on lean plug-ins, so guard each
1506
+ // with `?.`. Only navigation (record open / related-record / create) bubbles to the outer app;
1507
+ // config and dataRequest are generic plug-in↔container coordination.
1508
+ const inst = ref.instance;
1509
+ inst.recordSelected?.pipe(takeUntil(this.destroy$)).subscribe((r) => this.onDynamicRecordSelected(r));
1510
+ inst.recordOpened?.pipe(takeUntil(this.destroy$)).subscribe((r) => this.onDynamicRecordOpened(r));
1511
+ inst.configChanged
1512
+ ?.pipe(takeUntil(this.destroy$))
1513
+ .subscribe((cfg) => this.onDynamicConfigChanged(option.viewTypeId, (cfg ?? {})));
1514
+ inst.openRelatedRecordRequested
1515
+ ?.pipe(takeUntil(this.destroy$))
1516
+ .subscribe((nav) => this.OpenRelatedRecordRequested.emit(nav));
1517
+ inst.createRecordRequested
1518
+ ?.pipe(takeUntil(this.destroy$))
1519
+ .subscribe(() => this.CreateRecordRequested.emit());
1520
+ inst.dataRequest
1521
+ ?.pipe(takeUntil(this.destroy$))
1522
+ .subscribe((req) => this.onDynamicDataRequest(req));
1523
+ const created = { ref, inputs };
1524
+ this.dynamicRendererCache.set(option.viewTypeId, created);
1525
+ return created;
1526
+ }
1527
+ /**
1528
+ * Toggle a cached renderer's host element visibility without destroying it (preserves state).
1529
+ * When visible, force `display: block` + `height: 100%` rather than clearing the style: the wrapper
1530
+ * components use `ViewEncapsulation.None`, under which their `:host { display:block; height:100% }`
1531
+ * rule is a no-op, so a custom element defaults to `display: inline` — which gives AG Grid a
1532
+ * zero-width viewport (no rows/columns render), especially after a hide → show cycle. Forcing block
1533
+ * here guarantees every view-type plug-in gets a full-size block container.
1534
+ */
1535
+ setRendererVisible(ref, visible) {
1536
+ const el = ref.location.nativeElement;
1537
+ if (el) {
1538
+ el.style.display = visible ? 'block' : 'none';
1539
+ if (visible) {
1540
+ el.style.height = '100%';
1541
+ }
1599
1542
  }
1600
1543
  }
1601
1544
  /**
1602
- * If currently on map view but geocoding is no longer available,
1603
- * fall back to grid view
1545
+ * Destroys ALL cached plug-in instances and clears the host. Called when the data context changes
1546
+ * (entity change / view (record / ViewID) change) so the next selection rebuilds fresh. NOT called
1547
+ * on a plain view-type switch within the same context — that path reuses cached instances.
1604
1548
  */
1605
- fallbackFromMapIfNeeded() {
1606
- if (this.effectiveViewMode === 'map' && !this.HasGeoCoding) {
1607
- this.setViewMode('grid');
1549
+ clearDynamicRendererCache() {
1550
+ for (const entry of this.dynamicRendererCache.values()) {
1551
+ entry.ref.destroy();
1608
1552
  }
1553
+ this.dynamicRendererCache.clear();
1554
+ this.dynamicRendererRef = null;
1555
+ this.dynamicInputNames = new Set();
1556
+ this.dynamicViewHost?.clear();
1609
1557
  }
1610
1558
  /**
1611
- * Sort date fields by priority:
1612
- * 1. DefaultInView=true fields, sorted by Sequence (lowest first)
1613
- * 2. Other date fields, sorted by Sequence (lowest first)
1559
+ * Pushes the current generic data-context into the mounted plug-in renderer via `setInput`.
1560
+ * Every input is part of the generic {@link IViewRenderer} contract; lean plug-ins may not declare
1561
+ * all of them, so each `setInput` is wrapped in try/catch (Angular throws on undeclared inputs).
1614
1562
  */
1615
- sortDateFieldsByPriority(dateFields) {
1616
- const defaultInView = dateFields.filter(f => f.DefaultInView).sort((a, b) => a.Sequence - b.Sequence);
1617
- const others = dateFields.filter(f => !f.DefaultInView).sort((a, b) => a.Sequence - b.Sequence);
1618
- return [...defaultInView, ...others];
1563
+ pushDynamicRendererInputs() {
1564
+ const ref = this.dynamicRendererRef;
1565
+ const option = this.ActiveDynamicOption;
1566
+ if (!ref || !option) {
1567
+ return;
1568
+ }
1569
+ this.setDynamicInput(ref, 'entity', this.EffectiveEntity);
1570
+ this.setDynamicInput(ref, 'provider', this.ProviderToUse);
1571
+ this.setDynamicInput(ref, 'Provider', this.Provider);
1572
+ this.setDynamicInput(ref, 'records', this.FilteredRecords);
1573
+ this.setDynamicInput(ref, 'selectedRecordId', this.SelectedRecordID);
1574
+ this.setDynamicInput(ref, 'filterText', this.DebouncedFilterText);
1575
+ this.setDynamicInput(ref, 'config', this.viewTypeConfigById.get(option.viewTypeId) ?? {});
1576
+ this.setDynamicInput(ref, 'totalRecordCount', this.TotalRecordCount);
1577
+ this.setDynamicInput(ref, 'page', this.Pagination.currentPage + 1);
1578
+ this.setDynamicInput(ref, 'pageSize', this.Pagination.pageSize);
1579
+ this.setDynamicInput(ref, 'isLoading', this.IsLoading);
1619
1580
  }
1620
1581
  /**
1621
- * Configure the timeline with the current date field and records
1582
+ * Guarded `setInput`: lean plug-ins implement only the core contract, so we only set an input the
1583
+ * mounted plug-in actually declares (per {@link dynamicInputNames}, captured at mount via
1584
+ * `reflectComponentType`). This avoids NG0303 dev errors for optional inputs the plug-in omits and
1585
+ * keeps the container plug-in-agnostic.
1622
1586
  */
1623
- configureTimeline() {
1624
- if (!this.entity || !this.hasDateFields || this.availableDateFields.length === 0) {
1625
- this.timelineGroups = [];
1626
- return;
1587
+ setDynamicInput(ref, name, value) {
1588
+ if (this.dynamicInputNames.has(name)) {
1589
+ ref.setInput(name, value);
1627
1590
  }
1628
- // Determine which date field to use
1629
- const dateFieldName = this.getEffectiveTimelineDateField();
1630
- this.selectedTimelineDateField = dateFieldName;
1631
- // Apply timeline config if provided
1632
- if (this.timelineConfig) {
1633
- this.timelineSortOrder = (this.timelineConfig.sortOrder || 'desc');
1634
- this.timelineSegmentGrouping = (this.timelineConfig.segmentGrouping || 'month');
1635
- this.timelineOrientation = this.timelineConfig.orientation || 'vertical';
1591
+ }
1592
+ onDynamicRecordSelected(record) {
1593
+ const entity = this.EffectiveEntity;
1594
+ if (entity && record) {
1595
+ const row = record;
1596
+ this.RecordSelected.emit({ record: row, entity, compositeKey: buildCompositeKey(row, entity) });
1636
1597
  }
1637
- // Create a timeline group for the current entity's data
1638
- this.updateTimelineGroups();
1639
1598
  }
1640
- /**
1641
- * Get the effective date field to use for timeline
1642
- * Priority: timelineConfig > first available date field
1643
- */
1644
- getEffectiveTimelineDateField() {
1645
- // If we have a config with a specific date field, use it if valid
1646
- if (this.timelineConfig?.dateFieldName) {
1647
- const configField = this.availableDateFields.find(f => f.Name === this.timelineConfig.dateFieldName);
1648
- if (configField) {
1649
- return configField.Name;
1650
- }
1599
+ onDynamicRecordOpened(record) {
1600
+ const entity = this.EffectiveEntity;
1601
+ if (entity && record) {
1602
+ const row = record;
1603
+ this.RecordOpened.emit({ record: row, entity, compositeKey: buildCompositeKey(row, entity) });
1651
1604
  }
1652
- // Otherwise use the first available date field (already sorted by priority)
1653
- return this.availableDateFields[0].Name;
1654
1605
  }
1655
- /**
1656
- * Update timeline groups with current records
1657
- * Called when records change
1658
- */
1659
- updateTimelineGroups() {
1660
- if (!this.entity || !this.selectedTimelineDateField) {
1661
- this.timelineGroups = [];
1606
+ onDynamicConfigChanged(viewTypeId, config) {
1607
+ const before = { ViewTypeID: viewTypeId, Config: config, Cancel: false };
1608
+ this.BeforeViewTypeConfigChange.emit(before);
1609
+ if (before.Cancel) {
1662
1610
  return;
1663
1611
  }
1664
- // Find title field - prefer NameField, then first string field with DefaultInView
1665
- const titleField = this.findTitleField();
1666
- // Create a single group for the current data
1667
- const group = new TimelineGroup();
1668
- group.DataSourceType = 'array';
1669
- group.EntityObjects = this.filteredRecords;
1670
- group.TitleFieldName = titleField;
1671
- group.DateFieldName = this.selectedTimelineDateField;
1672
- group.IdFieldName = 'ID';
1673
- group.GroupLabel = this.entity.Name;
1674
- // Find a suitable description field
1675
- const descField = this.findDescriptionField();
1676
- if (descField) {
1677
- group.DescriptionFieldName = descField;
1678
- }
1679
- // Find a suitable subtitle field
1680
- const subtitleField = this.findSubtitleField(titleField);
1681
- if (subtitleField) {
1682
- group.SubtitleFieldName = subtitleField;
1683
- }
1684
- // Configure card display
1685
- group.CardConfig = {
1686
- collapsible: true,
1687
- defaultExpanded: false,
1688
- showDate: true,
1689
- dateFormat: 'MMM d, yyyy h:mm a'
1690
- };
1691
- this.timelineGroups = [group];
1612
+ this.viewTypeConfigById.set(viewTypeId, config);
1613
+ if (this.AutoSaveView) {
1614
+ void this.persistViewTypeConfig(viewTypeId);
1615
+ }
1616
+ this.AfterViewTypeConfigChange.emit({ ViewTypeID: viewTypeId, Config: config, Cancel: false });
1692
1617
  }
1693
1618
  /**
1694
- * Find the best field to use as the title
1619
+ * Honors a generic {@link ViewDataRequest} from the mounted plug-in. Fully view-type-agnostic —
1620
+ * the container applies whatever is present (sort / page / pageSize / loadAll) against its
1621
+ * existing generic data-loading path. No per-view-type branching.
1695
1622
  */
1696
- findTitleField() {
1697
- if (!this.entity)
1698
- return 'ID';
1699
- // Prefer the entity's NameField
1700
- if (this.entity.NameField) {
1701
- return this.entity.NameField.Name;
1623
+ onDynamicDataRequest(req) {
1624
+ if (!req || this._records) {
1625
+ // Nothing to do when records are externally supplied (no internal RunView to retrigger).
1626
+ return;
1627
+ }
1628
+ // loadAll → switch the data loader into full-set mode (replaces the old map-specific branch).
1629
+ if (req.loadAll === true && !this._loadAllRecords) {
1630
+ this._loadAllRecords = true;
1631
+ this.resetPaginationState(false);
1632
+ this.LoadData();
1633
+ return;
1634
+ }
1635
+ if (req.loadAll === false && this._loadAllRecords) {
1636
+ this._loadAllRecords = false;
1637
+ this.resetPaginationState(false);
1638
+ this.LoadData();
1639
+ return;
1640
+ }
1641
+ // sort → set the host's internal sort state and reload via the server-side-sort path.
1642
+ if (req.sort) {
1643
+ const first = req.sort.length > 0 ? req.sort[0] : null;
1644
+ const newSort = first ? { field: first.field, direction: first.direction } : null;
1645
+ const oldSort = this.InternalSortState;
1646
+ this.InternalSortState = newSort;
1647
+ const sortChanged = !oldSort || !newSort ||
1648
+ oldSort.field !== newSort.field || oldSort.direction !== newSort.direction;
1649
+ if (this.EffectiveConfig.serverSideSorting && sortChanged) {
1650
+ this.resetPaginationState(false);
1651
+ this.LoadData();
1652
+ }
1653
+ return;
1654
+ }
1655
+ // page / pageSize → load that page via the existing pager path.
1656
+ if (req.pageSize != null && req.pageSize !== this.Pagination.pageSize) {
1657
+ this.Pagination.pageSize = req.pageSize;
1702
1658
  }
1703
- // Look for common name patterns in DefaultInView string fields
1704
- const stringFields = this.entity.Fields.filter(f => f.TSType === EntityFieldTSType.String && f.DefaultInView && !f.Name.startsWith('__mj_')).sort((a, b) => a.Sequence - b.Sequence);
1705
- const namePatterns = ['name', 'title', 'subject', 'label'];
1706
- for (const pattern of namePatterns) {
1707
- const match = stringFields.find(f => f.Name.toLowerCase().includes(pattern));
1708
- if (match)
1709
- return match.Name;
1659
+ if (req.page != null) {
1660
+ this.OnPageChange({ PageNumber: req.page, PageSize: req.pageSize ?? this.Pagination.pageSize });
1710
1661
  }
1711
- // Fall back to first string field
1712
- return stringFields.length > 0 ? stringFields[0].Name : 'ID';
1713
1662
  }
1663
+ // ========================================
1664
+ // FILTERING
1665
+ // ========================================
1714
1666
  /**
1715
- * Find a suitable description field
1667
+ * Handle filter input change
1716
1668
  */
1717
- findDescriptionField() {
1718
- if (!this.entity)
1719
- return null;
1720
- // Look for common description patterns
1721
- const descPatterns = ['description', 'notes', 'summary', 'content', 'body', 'details'];
1722
- const textFields = this.entity.Fields.filter(f => (f.TSType === EntityFieldTSType.String) && !f.Name.startsWith('__mj_'));
1723
- for (const pattern of descPatterns) {
1724
- const match = textFields.find(f => f.Name.toLowerCase().includes(pattern));
1725
- if (match)
1726
- return match.Name;
1727
- }
1728
- return null;
1669
+ OnFilterChange(value) {
1670
+ this.InternalFilterText = value;
1671
+ this.filterInput$.next(value);
1729
1672
  }
1730
1673
  /**
1731
- * Find a suitable subtitle field (different from title)
1674
+ * Clear the filter
1732
1675
  */
1733
- findSubtitleField(excludeField) {
1734
- if (!this.entity)
1735
- return null;
1736
- // Look for status, type, category, or other short classification fields
1737
- const patterns = ['status', 'type', 'category', 'state', 'priority'];
1738
- const fields = this.entity.Fields.filter(f => f.TSType === EntityFieldTSType.String &&
1739
- f.DefaultInView &&
1740
- f.Name !== excludeField &&
1741
- !f.Name.startsWith('__mj_')).sort((a, b) => a.Sequence - b.Sequence);
1742
- for (const pattern of patterns) {
1743
- const match = fields.find(f => f.Name.toLowerCase().includes(pattern));
1744
- if (match)
1745
- return match.Name;
1746
- }
1747
- // Use the first string field that's not the title field
1748
- const firstOther = fields.find(f => f.Name !== excludeField);
1749
- return firstOther?.Name || null;
1750
- }
1751
- static ɵfac = function EntityViewerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone)); };
1676
+ ClearFilter() {
1677
+ this.InternalFilterText = '';
1678
+ this.filterInput$.next('');
1679
+ this.cdr.detectChanges();
1680
+ }
1681
+ static ɵfac = function EntityViewerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone), i0.ɵɵdirectiveInject(i0.ElementRef)); };
1752
1682
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityViewerComponent, selectors: [["mj-entity-viewer"]], viewQuery: function EntityViewerComponent_Query(rf, ctx) { if (rf & 1) {
1753
- i0.ɵɵviewQuery(EntityDataGridComponent, 5);
1683
+ i0.ɵɵviewQuery(_c0, 5, ViewContainerRef);
1754
1684
  } if (rf & 2) {
1755
1685
  let _t;
1756
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dataGridRef = _t.first);
1757
- } }, 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", ShowRecycleBin: "ShowRecycleBin", mapDisplayState: "mapDisplayState", mapRenderMode: "mapRenderMode" }, 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", mapDisplayStateChange: "mapDisplayStateChange", mapRenderModeChange: "mapRenderModeChange" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 19, vars: 42, 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", "ForeignKeyClick", "PageChange", "hidden", "Data", "Params", "FilterText", "GridState", "Height", "ShowToolbar", "ToolbarConfig", "SelectionMode", "ShowAddToListButton", "AllowLoad", "ShowPager", "PageSize", "TotalRowCount", "PagerPageNumber"], [3, "recordSelected", "recordOpened", "hidden", "entity", "records", "selectedRecordId", "cardTemplate", "hiddenFieldMatches", "filterText"], [3, "afterEventClick", "hidden", "groups", "orientation", "layout", "sortOrder", "segmentGrouping", "segmentsCollapsible", "segmentsDefaultExpanded", "selectedEventId"], [3, "hidden", "Entity", "Records", "TotalRecordCount", "RenderMode", "DisplayState"], [1, "filter-container"], [1, "record-count"], [1, "view-mode-toggle"], [3, "EntityName"], [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", "Map View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-map-location-dot"], ["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, "MarkerClick", "RenderModeChange", "DisplayStateChange", "hidden", "Entity", "Records", "TotalRecordCount", "RenderMode", "DisplayState"]], template: function EntityViewerComponent_Template(rf, ctx) { if (rf & 1) {
1758
- i0.ɵɵelementStart(0, "div", 0);
1759
- i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Template, 6, 5, "div", 1);
1760
- i0.ɵɵelementStart(2, "div", 2)(3, "div", 3);
1761
- i0.ɵɵelement(4, "mj-loading", 4);
1686
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dynamicViewHost = _t.first);
1687
+ } }, hostAttrs: [2, "display", "block", "height", "100%"], inputs: { Entity: "Entity", EntityName: "EntityName", EntityID: "EntityID", ViewID: "ViewID", Records: "Records", Config: "Config", SelectedRecordID: "SelectedRecordID", FilterText: "FilterText", SortState: "SortState", ViewEntity: "ViewEntity", GridState: "GridState", ShowRecycleBin: "ShowRecycleBin", ViewTypeID: "ViewTypeID", ViewTypeConfigs: "ViewTypeConfigs", AutoSaveView: "AutoSaveView" }, outputs: { RecordSelected: "RecordSelected", RecordOpened: "RecordOpened", DataLoaded: "DataLoaded", FilterTextChange: "FilterTextChange", FilteredCountChanged: "FilteredCountChanged", OpenRelatedRecordRequested: "OpenRelatedRecordRequested", CreateRecordRequested: "CreateRecordRequested", BeforeViewTypeChange: "BeforeViewTypeChange", AfterViewTypeChange: "AfterViewTypeChange", BeforeViewTypeConfigChange: "BeforeViewTypeConfigChange", AfterViewTypeConfigChange: "AfterViewTypeConfigChange" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 18, vars: 11, consts: [["dynamicViewHost", ""], [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"], [1, "dynamic-view-host", 3, "hidden"], [1, "filter-container"], [1, "record-count"], [3, "Provider", "Entity", "ActiveViewTypeID"], [3, "EntityName"], [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"], [3, "ViewTypeSelected", "Provider", "Entity", "ActiveViewTypeID"]], template: function EntityViewerComponent_Template(rf, ctx) { if (rf & 1) {
1688
+ i0.ɵɵelementStart(0, "div", 1);
1689
+ i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Template, 5, 4, "div", 2);
1690
+ i0.ɵɵelementStart(2, "div", 3)(3, "div", 4);
1691
+ i0.ɵɵelement(4, "mj-loading", 5);
1762
1692
  i0.ɵɵelementEnd();
1763
- i0.ɵɵelementStart(5, "div", 5);
1764
- i0.ɵɵelement(6, "mj-loading", 6);
1693
+ i0.ɵɵelementStart(5, "div", 6);
1694
+ i0.ɵɵelement(6, "mj-loading", 7);
1765
1695
  i0.ɵɵelementEnd();
1766
- i0.ɵɵelementStart(7, "div", 7);
1767
- i0.ɵɵelement(8, "i", 8);
1696
+ i0.ɵɵelementStart(7, "div", 8);
1697
+ i0.ɵɵelement(8, "i", 9);
1768
1698
  i0.ɵɵelementStart(9, "p");
1769
1699
  i0.ɵɵtext(10, "Select an entity to view records");
1770
1700
  i0.ɵɵelementEnd()();
1771
- i0.ɵɵelementStart(11, "div", 7);
1772
- i0.ɵɵelement(12, "i", 9);
1701
+ i0.ɵɵelementStart(11, "div", 8);
1702
+ i0.ɵɵelement(12, "i", 10);
1773
1703
  i0.ɵɵelementStart(13, "p");
1774
1704
  i0.ɵɵtext(14);
1775
1705
  i0.ɵɵelementEnd()();
1776
- i0.ɵɵelementStart(15, "mj-entity-data-grid", 10);
1777
- 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); })("ForeignKeyClick", function EntityViewerComponent_Template_mj_entity_data_grid_ForeignKeyClick_15_listener($event) { return ctx.onForeignKeyClick($event); })("PageChange", function EntityViewerComponent_Template_mj_entity_data_grid_PageChange_15_listener($event) { return ctx.onGridPageChange($event); });
1778
- i0.ɵɵelementEnd();
1779
- i0.ɵɵelementStart(16, "mj-entity-cards", 11);
1780
- 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); });
1781
- i0.ɵɵelementEnd();
1782
- i0.ɵɵelementStart(17, "mj-timeline", 12);
1783
- i0.ɵɵlistener("afterEventClick", function EntityViewerComponent_Template_mj_timeline_afterEventClick_17_listener($event) { return ctx.onTimelineEventClick($event); });
1784
- i0.ɵɵelementEnd();
1785
- i0.ɵɵconditionalCreate(18, EntityViewerComponent_Conditional_18_Template, 1, 6, "mj-map-view", 13);
1786
- i0.ɵɵelementEnd()();
1706
+ i0.ɵɵelementStart(15, "div", 11);
1707
+ i0.ɵɵtemplate(16, EntityViewerComponent_ng_template_16_Template, 0, 0, "ng-template", null, 0, i0.ɵɵtemplateRefExtractor);
1708
+ i0.ɵɵelementEnd()()();
1787
1709
  } if (rf & 2) {
1788
- i0.ɵɵstyleProp("height", ctx.effectiveConfig.height);
1710
+ i0.ɵɵstyleProp("height", ctx.EffectiveConfig.height);
1789
1711
  i0.ɵɵadvance();
1790
- i0.ɵɵconditional(ctx.effectiveConfig.showFilter || ctx.effectiveConfig.showViewModeToggle || ctx.effectiveConfig.showRecordCount ? 1 : -1);
1712
+ i0.ɵɵconditional(ctx.EffectiveConfig.showFilter || ctx.EffectiveConfig.showViewModeToggle || ctx.EffectiveConfig.showRecordCount ? 1 : -1);
1791
1713
  i0.ɵɵadvance(2);
1792
- i0.ɵɵproperty("hidden", !(ctx.isLoading && ctx.filteredRecords.length === 0));
1714
+ i0.ɵɵproperty("hidden", !(ctx.IsLoading && ctx.FilteredRecords.length === 0));
1793
1715
  i0.ɵɵadvance();
1794
- i0.ɵɵproperty("text", ctx.loadingMessage);
1716
+ i0.ɵɵproperty("text", ctx.LoadingMessage);
1795
1717
  i0.ɵɵadvance();
1796
- i0.ɵɵproperty("hidden", !(ctx.isLoading && ctx.filteredRecords.length > 0));
1718
+ i0.ɵɵproperty("hidden", !(ctx.IsLoading && ctx.FilteredRecords.length > 0));
1797
1719
  i0.ɵɵadvance();
1798
- i0.ɵɵproperty("text", ctx.loadingMessage);
1720
+ i0.ɵɵproperty("text", ctx.LoadingMessage);
1799
1721
  i0.ɵɵadvance();
1800
- i0.ɵɵproperty("hidden", !!ctx.effectiveEntity);
1722
+ i0.ɵɵproperty("hidden", !!ctx.EffectiveEntity);
1801
1723
  i0.ɵɵadvance(4);
1802
- i0.ɵɵproperty("hidden", !ctx.effectiveEntity || ctx.filteredRecords.length > 0 || ctx.isLoading);
1724
+ i0.ɵɵproperty("hidden", !ctx.EffectiveEntity || ctx.FilteredRecords.length > 0 || ctx.IsLoading || ctx.IsDynamicViewActive);
1803
1725
  i0.ɵɵadvance(3);
1804
- i0.ɵɵtextInterpolate(ctx.debouncedFilterText ? "No matching records" : "No records found");
1805
- i0.ɵɵadvance();
1806
- 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)("ShowPager", ctx.effectiveConfig.showPagination)("PageSize", ctx.effectiveConfig.pageSize)("TotalRowCount", ctx.pagination.totalRecords)("PagerPageNumber", ctx.pagination.currentPage + 1);
1726
+ i0.ɵɵtextInterpolate(ctx.DebouncedFilterText ? "No matching records" : "No records found");
1807
1727
  i0.ɵɵadvance();
1808
- 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);
1809
- i0.ɵɵadvance();
1810
- 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);
1811
- i0.ɵɵadvance();
1812
- i0.ɵɵconditional(ctx.HasGeoCoding ? 18 : -1);
1813
- } }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i2.LoadingComponent, i3.TimelineComponent, i4.MapViewComponent, i5.EntityCardsComponent, i6.EntityDataGridComponent, i7.RecycleBinChipComponent, i8.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface-card);\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: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\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: var(--mj-text-disabled);\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 var(--mj-border-default);\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: var(--mj-brand-primary);\n}\n\n.filter-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-disabled);\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: var(--mj-text-disabled);\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n\n\n.record-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n\n\n.view-mode-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-text-muted);\n transition: all 0.15s ease;\n}\n\n.toggle-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-secondary);\n}\n\n.toggle-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\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: var(--mj-text-secondary);\n}\n\n.timeline-date-selector[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-disabled);\n}\n\n.date-field-label[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n font-weight: 500;\n}\n\n.date-field-select[_ngcontent-%COMP%] {\n padding: 4px 8px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 13px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\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: var(--mj-border-strong);\n}\n\n.date-field-select[_ngcontent-%COMP%]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n\n\n.timeline-orientation-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-bg-surface);\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: color-mix(in srgb, var(--mj-bg-surface) 80%, transparent);\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: var(--mj-text-disabled);\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%], \nmj-map-view[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\nmj-entity-cards[_ngcontent-%COMP%]:not([hidden]), \nmj-timeline[_ngcontent-%COMP%]:not([hidden]), \nmj-map-view[_ngcontent-%COMP%]:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n\n\n.toggle-btn.geo-hidden[_ngcontent-%COMP%] {\n display: none !important;\n}"] });
1728
+ i0.ɵɵproperty("hidden", !ctx.EffectiveEntity);
1729
+ } }, dependencies: [i1.LoadingComponent, i2.RecycleBinChipComponent, i3.ViewTypeSwitcherComponent, i4.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface-card);\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: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\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: var(--mj-text-disabled);\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 var(--mj-border-default);\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: var(--mj-brand-primary);\n}\n\n.filter-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-disabled);\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: var(--mj-text-disabled);\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n\n\n.record-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n\n\n.view-mode-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-text-muted);\n transition: all 0.15s ease;\n}\n\n.toggle-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-secondary);\n}\n\n.toggle-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n\n\n.view-type-dropdown[_ngcontent-%COMP%] {\n position: relative;\n display: inline-flex;\n}\n\n.view-type-dropdown-trigger[_ngcontent-%COMP%] {\n width: auto;\n gap: 6px;\n padding: 0 10px;\n}\n\n.view-type-dropdown-label[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 500;\n}\n\n.view-type-dropdown-caret[_ngcontent-%COMP%] {\n font-size: 10px;\n opacity: 0.7;\n}\n\n.view-type-dropdown-menu[_ngcontent-%COMP%] {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 20;\n min-width: 180px;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: 0 8px 24px color-mix(in srgb, var(--mj-text-primary) 14%, transparent);\n padding: 4px;\n display: flex;\n flex-direction: column;\n}\n\n.view-type-dropdown-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 8px 10px;\n border: none;\n background: transparent;\n border-radius: 6px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 13px;\n text-align: left;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.view-type-dropdown-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.view-type-dropdown-item.active[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.view-type-dropdown-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n width: 16px;\n text-align: center;\n}\n\n\n\n.dynamic-view-host[_ngcontent-%COMP%] {\n height: 100%;\n width: 100%;\n overflow: auto;\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: var(--mj-bg-surface);\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: color-mix(in srgb, var(--mj-bg-surface) 80%, transparent);\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: var(--mj-text-disabled);\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\n.dynamic-view-host[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}"] });
1814
1730
  }
1815
1731
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
1816
1732
  type: Component,
1817
1733
  args: [{ standalone: false, selector: 'mj-entity-viewer', host: {
1818
1734
  'style': 'display: block; height: 100%;'
1819
- }, 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 <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'map'\"\n [class.geo-hidden]=\"!HasGeoCoding\"\n (click)=\"setViewMode('map')\"\n title=\"Map View\">\n <i class=\"fa-solid fa-map-location-dot\"></i>\n </button>\n </div>\n }\n\n <!-- Recycle Bin chip (auto-hides when entity has no deleted records, no tracking,\n or user lacks Delete permission) -->\n @if (ShowRecycleBin) {\n <mj-recycle-bin-chip [EntityName]=\"effectiveEntity?.Name ?? null\"></mj-recycle-bin-chip>\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 [ShowPager]=\"effectiveConfig.showPagination\"\n [PageSize]=\"effectiveConfig.pageSize\"\n [TotalRowCount]=\"pagination.totalRecords\"\n [PagerPageNumber]=\"pagination.currentPage + 1\"\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 (ForeignKeyClick)=\"onForeignKeyClick($event)\"\n (PageChange)=\"onGridPageChange($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\n <!-- Map View - rendered when geocoding supported, visibility controlled by hidden -->\n @if (HasGeoCoding) {\n <mj-map-view\n [hidden]=\"effectiveViewMode !== 'map'\"\n [Entity]=\"effectiveEntity!\"\n [Records]=\"filteredRecords\"\n [TotalRecordCount]=\"totalRecordCount\"\n [RenderMode]=\"mapRenderMode\"\n [DisplayState]=\"mapDisplayState\"\n (MarkerClick)=\"onMapMarkerClick($event)\"\n (RenderModeChange)=\"onMapRenderModeChange($event)\"\n (DisplayStateChange)=\"onMapDisplayStateChange($event)\">\n </mj-map-view>\n }\n </div>\n\n</div>\n", styles: [".entity-viewer-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface-card);\n}\n\n/* Header */\n.viewer-header {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\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: var(--mj-text-disabled);\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 var(--mj-border-default);\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: var(--mj-brand-primary);\n}\n\n.filter-input::placeholder {\n color: var(--mj-text-disabled);\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: var(--mj-text-disabled);\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n/* Record Count */\n.record-count {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-text-muted);\n transition: all 0.15s ease;\n}\n\n.toggle-btn:hover {\n color: var(--mj-text-secondary);\n}\n\n.toggle-btn.active {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\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: var(--mj-text-secondary);\n}\n\n.timeline-date-selector i {\n color: var(--mj-text-disabled);\n}\n\n.date-field-label {\n color: var(--mj-text-secondary);\n font-weight: 500;\n}\n\n.date-field-select {\n padding: 4px 8px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 13px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select:hover {\n border-color: var(--mj-border-strong);\n}\n\n.date-field-select:focus {\n border-color: var(--mj-brand-primary);\n}\n\n/* Timeline Orientation Toggle */\n.timeline-orientation-toggle {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-bg-surface);\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: color-mix(in srgb, var(--mj-bg-surface) 80%, transparent);\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: var(--mj-text-disabled);\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],\nmj-map-view[hidden] {\n display: none !important;\n}\n\n/* Visible view components should fill available space */\nmj-entity-cards:not([hidden]),\nmj-timeline:not([hidden]),\nmj-map-view:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n/* Hide map toggle when entity does not support geocoding */\n.toggle-btn.geo-hidden {\n display: none !important;\n}\n"] }]
1820
- }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], { entity: [{
1821
- type: Input
1822
- }], records: [{
1735
+ }, 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 Type Toggle \u2014 the reusable switcher, driven entirely by the MJ: View Types\n registry (ViewTypeEngine). The available types and their icons come from metadata\n (ViewType.Icon), filtered by each descriptor's availability predicate. Selecting routes\n back through SelectViewTypeById, which mounts the chosen descriptor's RendererComponent\n into #dynamicViewHost. The container has zero knowledge of any view type's chrome. -->\n @if (EffectiveConfig.showViewModeToggle) {\n <mj-view-type-switcher\n [Provider]=\"Provider\"\n [Entity]=\"EffectiveEntity\"\n [ActiveViewTypeID]=\"ActiveViewTypeId\"\n (ViewTypeSelected)=\"SelectViewTypeById($event.viewTypeId)\">\n </mj-view-type-switcher>\n }\n\n <!-- Recycle Bin chip (auto-hides when entity has no deleted records, no tracking,\n or user lacks Delete permission) -->\n @if (ShowRecycleBin) {\n <mj-recycle-bin-chip [EntityName]=\"EffectiveEntity?.Name ?? null\"></mj-recycle-bin-chip>\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. Suppressed while a plug-in is active \u2014 every plug-in renders\n its own empty/loading state, so the container never shows a generic \"no records\" over it. -->\n <div class=\"empty-state\" [hidden]=\"!EffectiveEntity || FilteredRecords.length > 0 || IsLoading || IsDynamicViewActive\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ DebouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n\n <!-- Plug-in (dynamic) view renderer host \u2014 the ONLY render path. The active descriptor's\n RendererComponent is created into #dynamicViewHost via ViewContainerRef and fed the generic\n IViewRenderer contract. Shown whenever there's an entity; the plug-in owns all its chrome. -->\n <div class=\"dynamic-view-host\" [hidden]=\"!EffectiveEntity\">\n <ng-template #dynamicViewHost></ng-template>\n </div>\n </div>\n\n</div>\n", styles: [".entity-viewer-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface-card);\n}\n\n/* Header */\n.viewer-header {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\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: var(--mj-text-disabled);\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 var(--mj-border-default);\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: var(--mj-brand-primary);\n}\n\n.filter-input::placeholder {\n color: var(--mj-text-disabled);\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: var(--mj-text-disabled);\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n/* Record Count */\n.record-count {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background: var(--mj-bg-surface-card);\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: var(--mj-text-muted);\n transition: all 0.15s ease;\n}\n\n.toggle-btn:hover {\n color: var(--mj-text-secondary);\n}\n\n.toggle-btn.active {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n/* Compact view-type dropdown \u2014 used when there are more view types than the strip threshold */\n.view-type-dropdown {\n position: relative;\n display: inline-flex;\n}\n\n.view-type-dropdown-trigger {\n width: auto;\n gap: 6px;\n padding: 0 10px;\n}\n\n.view-type-dropdown-label {\n font-size: 13px;\n font-weight: 500;\n}\n\n.view-type-dropdown-caret {\n font-size: 10px;\n opacity: 0.7;\n}\n\n.view-type-dropdown-menu {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 20;\n min-width: 180px;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: 0 8px 24px color-mix(in srgb, var(--mj-text-primary) 14%, transparent);\n padding: 4px;\n display: flex;\n flex-direction: column;\n}\n\n.view-type-dropdown-item {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 8px 10px;\n border: none;\n background: transparent;\n border-radius: 6px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 13px;\n text-align: left;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.view-type-dropdown-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.view-type-dropdown-item.active {\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.view-type-dropdown-item i {\n width: 16px;\n text-align: center;\n}\n\n/* Host for dynamically-mounted plug-in view renderers (Cluster, third-party) */\n.dynamic-view-host {\n height: 100%;\n width: 100%;\n overflow: auto;\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: var(--mj-bg-surface);\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: color-mix(in srgb, var(--mj-bg-surface) 80%, transparent);\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: var(--mj-text-disabled);\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/* Hide the dynamic host when no plug-in is mounted (no entity) */\n.dynamic-view-host[hidden] {\n display: none !important;\n}\n"] }]
1736
+ }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }], { Entity: [{
1823
1737
  type: Input
1824
- }], config: [{
1738
+ }], EntityName: [{
1825
1739
  type: Input
1826
- }], selectedRecordId: [{
1740
+ }], EntityID: [{
1827
1741
  type: Input
1828
- }], viewMode: [{
1742
+ }], ViewID: [{
1829
1743
  type: Input
1830
- }], filterText: [{
1744
+ }], Records: [{
1831
1745
  type: Input
1832
- }], sortState: [{
1746
+ }], Config: [{
1833
1747
  type: Input
1834
- }], gridColumns: [{
1748
+ }], SelectedRecordID: [{
1835
1749
  type: Input
1836
- }], cardTemplate: [{
1750
+ }], FilterText: [{
1837
1751
  type: Input
1838
- }], viewEntity: [{
1752
+ }], SortState: [{
1839
1753
  type: Input
1840
- }], gridState: [{
1754
+ }], ViewEntity: [{
1841
1755
  type: Input
1842
- }], timelineConfig: [{
1843
- type: Input
1844
- }], showGridToolbar: [{
1845
- type: Input
1846
- }], gridToolbarConfig: [{
1847
- type: Input
1848
- }], gridSelectionMode: [{
1849
- type: Input
1850
- }], showAddToListButton: [{
1756
+ }], GridState: [{
1851
1757
  type: Input
1852
1758
  }], ShowRecycleBin: [{
1853
1759
  type: Input
1854
- }], recordSelected: [{
1855
- type: Output
1856
- }], recordOpened: [{
1760
+ }], RecordSelected: [{
1857
1761
  type: Output
1858
- }], dataLoaded: [{
1762
+ }], RecordOpened: [{
1859
1763
  type: Output
1860
- }], viewModeChange: [{
1764
+ }], DataLoaded: [{
1861
1765
  type: Output
1862
- }], filterTextChange: [{
1766
+ }], FilterTextChange: [{
1863
1767
  type: Output
1864
- }], filteredCountChanged: [{
1768
+ }], FilteredCountChanged: [{
1865
1769
  type: Output
1866
- }], sortChanged: [{
1770
+ }], OpenRelatedRecordRequested: [{
1867
1771
  type: Output
1868
- }], gridStateChanged: [{
1772
+ }], CreateRecordRequested: [{
1869
1773
  type: Output
1870
- }], timelineConfigChange: [{
1871
- type: Output
1872
- }], addRequested: [{
1873
- type: Output
1874
- }], deleteRequested: [{
1875
- type: Output
1876
- }], refreshRequested: [{
1774
+ }], ViewTypeID: [{
1775
+ type: Input
1776
+ }], ViewTypeConfigs: [{
1777
+ type: Input
1778
+ }], AutoSaveView: [{
1779
+ type: Input
1780
+ }], BeforeViewTypeChange: [{
1877
1781
  type: Output
1878
- }], exportRequested: [{
1782
+ }], AfterViewTypeChange: [{
1879
1783
  type: Output
1880
- }], addToListRequested: [{
1784
+ }], BeforeViewTypeConfigChange: [{
1881
1785
  type: Output
1882
- }], selectionChanged: [{
1786
+ }], AfterViewTypeConfigChange: [{
1883
1787
  type: Output
1884
- }], dataGridRef: [{
1788
+ }], dynamicViewHost: [{
1885
1789
  type: ViewChild,
1886
- args: [EntityDataGridComponent]
1887
- }], mapDisplayState: [{
1888
- type: Input
1889
- }], mapRenderMode: [{
1890
- type: Input
1891
- }], mapDisplayStateChange: [{
1892
- type: Output
1893
- }], mapRenderModeChange: [{
1894
- type: Output
1790
+ args: ['dynamicViewHost', { read: ViewContainerRef }]
1895
1791
  }] }); })();
1896
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 81 }); })();
1792
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 133 }); })();
1897
1793
  //# sourceMappingURL=entity-viewer.component.js.map