@memberjunction/ng-entity-viewer 5.39.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.
- package/dist/__tests__/view-types.test.d.ts +2 -0
- package/dist/__tests__/view-types.test.d.ts.map +1 -0
- package/dist/__tests__/view-types.test.js +102 -0
- package/dist/__tests__/view-types.test.js.map +1 -0
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.js +8 -0
- package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts +356 -341
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.js +993 -1097
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.d.ts +126 -126
- package/dist/lib/view-config-panel/view-config-panel.component.js +635 -635
- package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
- package/dist/lib/view-selector/view-selector.component.d.ts +226 -0
- package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -0
- package/dist/lib/view-selector/view-selector.component.js +861 -0
- package/dist/lib/view-selector/view-selector.component.js.map +1 -0
- package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts +114 -0
- package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts.map +1 -0
- package/dist/lib/view-type-switcher/view-type-switcher.component.js +209 -0
- package/dist/lib/view-type-switcher/view-type-switcher.component.js.map +1 -0
- package/dist/lib/view-types/descriptors/cards-view-type.d.ts +18 -0
- package/dist/lib/view-types/descriptors/cards-view-type.d.ts.map +1 -0
- package/dist/lib/view-types/descriptors/cards-view-type.js +31 -0
- package/dist/lib/view-types/descriptors/cards-view-type.js.map +1 -0
- package/dist/lib/view-types/descriptors/grid-view-type.d.ts +17 -0
- package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -0
- package/dist/lib/view-types/descriptors/grid-view-type.js +30 -0
- package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -0
- package/dist/lib/view-types/descriptors/map-view-type.d.ts +21 -0
- package/dist/lib/view-types/descriptors/map-view-type.d.ts.map +1 -0
- package/dist/lib/view-types/descriptors/map-view-type.js +35 -0
- package/dist/lib/view-types/descriptors/map-view-type.js.map +1 -0
- package/dist/lib/view-types/descriptors/timeline-view-type.d.ts +22 -0
- package/dist/lib/view-types/descriptors/timeline-view-type.d.ts.map +1 -0
- package/dist/lib/view-types/descriptors/timeline-view-type.js +40 -0
- package/dist/lib/view-types/descriptors/timeline-view-type.js.map +1 -0
- package/dist/lib/view-types/index.d.ts +20 -0
- package/dist/lib/view-types/index.d.ts.map +1 -0
- package/dist/lib/view-types/index.js +29 -0
- package/dist/lib/view-types/index.js.map +1 -0
- package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts +93 -0
- package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts.map +1 -0
- package/dist/lib/view-types/renderers/cards-view-renderer.component.js +144 -0
- package/dist/lib/view-types/renderers/cards-view-renderer.component.js.map +1 -0
- package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +273 -0
- package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -0
- package/dist/lib/view-types/renderers/grid-view-renderer.component.js +558 -0
- package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -0
- package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts +135 -0
- package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -0
- package/dist/lib/view-types/renderers/map-view-renderer.component.js +216 -0
- package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -0
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts +176 -0
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -0
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +535 -0
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -0
- package/dist/lib/view-types/view-type.contracts.d.ts +235 -0
- package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -0
- package/dist/lib/view-types/view-type.contracts.js +51 -0
- package/dist/lib/view-types/view-type.contracts.js.map +1 -0
- package/dist/lib/view-types/view-type.engine.d.ts +76 -0
- package/dist/lib/view-types/view-type.engine.d.ts.map +1 -0
- package/dist/lib/view-types/view-type.engine.js +138 -0
- package/dist/lib/view-types/view-type.engine.js.map +1 -0
- package/dist/lib/view-workspace/view-workspace.component.d.ts +451 -0
- package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -0
- package/dist/lib/view-workspace/view-workspace.component.js +1212 -0
- package/dist/lib/view-workspace/view-workspace.component.js.map +1 -0
- package/dist/module.d.ts +20 -11
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +50 -8
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +8 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +14 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +16 -15
|
@@ -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 {
|
|
5
|
+
import { RunView, LogError } from '@memberjunction/core';
|
|
6
6
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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 {
|
|
10
|
+
import { ViewTypeEngine } from '../view-types';
|
|
11
11
|
import * as i0 from "@angular/core";
|
|
12
|
-
import * as i1 from "@
|
|
13
|
-
import * as i2 from "
|
|
14
|
-
import * as i3 from "
|
|
15
|
-
import * as i4 from "@
|
|
16
|
-
|
|
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",
|
|
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.
|
|
25
|
-
i0.ɵɵelement(1, "i",
|
|
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",
|
|
31
|
-
i0.ɵɵelement(1, "i",
|
|
32
|
-
i0.ɵɵelementStart(2, "input",
|
|
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.
|
|
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",
|
|
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.
|
|
36
|
+
i0.ɵɵproperty("placeholder", ctx_r1.EffectiveConfig.filterPlaceholder)("value", ctx_r1.EffectiveFilterText);
|
|
41
37
|
i0.ɵɵadvance();
|
|
42
|
-
i0.ɵɵconditional(ctx_r1.
|
|
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.
|
|
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.
|
|
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",
|
|
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.
|
|
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, "
|
|
87
|
-
i0.ɵɵlistener("
|
|
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.ɵɵ
|
|
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",
|
|
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.
|
|
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",
|
|
176
|
-
i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div",
|
|
177
|
-
i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div",
|
|
178
|
-
i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_3_Template,
|
|
179
|
-
i0.ɵɵconditionalCreate(4, EntityViewerComponent_Conditional_1_Conditional_4_Template, 1, 1, "mj-recycle-bin-chip",
|
|
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.
|
|
95
|
+
i0.ɵɵconditional(ctx_r1.EffectiveConfig.showFilter ? 1 : -1);
|
|
186
96
|
i0.ɵɵadvance();
|
|
187
|
-
i0.ɵɵconditional(ctx_r1.
|
|
97
|
+
i0.ɵɵconditional(ctx_r1.EffectiveConfig.showRecordCount && ctx_r1.EffectiveEntity ? 2 : -1);
|
|
188
98
|
i0.ɵɵadvance();
|
|
189
|
-
i0.ɵɵconditional(ctx_r1.
|
|
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
|
-
* [
|
|
221
|
-
* (
|
|
222
|
-
* (
|
|
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
|
-
* [
|
|
228
|
-
* [
|
|
229
|
-
* [
|
|
230
|
-
* [
|
|
231
|
-
* (
|
|
232
|
-
* (
|
|
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
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
|
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
|
|
167
|
+
get Entity() {
|
|
264
168
|
return this._entity;
|
|
265
169
|
}
|
|
266
|
-
set
|
|
170
|
+
set Entity(value) {
|
|
267
171
|
const previousEntity = this._entity;
|
|
268
172
|
this._entity = value;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
288
|
-
this.
|
|
289
|
-
this.
|
|
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.
|
|
297
|
-
this.
|
|
298
|
-
this.
|
|
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
|
|
271
|
+
get Records() {
|
|
308
272
|
return this._records;
|
|
309
273
|
}
|
|
310
|
-
set
|
|
274
|
+
set Records(value) {
|
|
311
275
|
this._records = value;
|
|
312
276
|
if (value) {
|
|
313
|
-
this.
|
|
314
|
-
this.
|
|
315
|
-
this.
|
|
316
|
-
|
|
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
|
|
286
|
+
get Config() {
|
|
324
287
|
return this._config;
|
|
325
288
|
}
|
|
326
|
-
set
|
|
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
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
this.
|
|
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
|
|
308
|
+
get FilterText() {
|
|
363
309
|
return this._filterText;
|
|
364
310
|
}
|
|
365
|
-
set
|
|
366
|
-
const oldFilter = this.
|
|
311
|
+
set FilterText(value) {
|
|
312
|
+
const oldFilter = this.DebouncedFilterText;
|
|
367
313
|
this._filterText = value;
|
|
368
314
|
const newFilter = value ?? '';
|
|
369
|
-
this.
|
|
370
|
-
this.
|
|
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.
|
|
320
|
+
if (this.EffectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this._records) {
|
|
375
321
|
this.resetPaginationState(false);
|
|
376
|
-
this.
|
|
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
|
|
333
|
+
get SortState() {
|
|
388
334
|
return this._sortState;
|
|
389
335
|
}
|
|
390
|
-
set
|
|
391
|
-
const oldSort = this.
|
|
336
|
+
set SortState(value) {
|
|
337
|
+
const oldSort = this.InternalSortState;
|
|
392
338
|
this._sortState = value;
|
|
393
339
|
if (value !== null) {
|
|
394
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
361
|
+
get ViewEntity() {
|
|
424
362
|
return this._viewEntity;
|
|
425
363
|
}
|
|
426
|
-
set
|
|
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
|
-
|
|
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
|
-
|
|
416
|
+
RecordSelected = new EventEmitter();
|
|
504
417
|
/**
|
|
505
418
|
* Emitted when a record should be opened (double-click or open button)
|
|
506
419
|
*/
|
|
507
|
-
|
|
420
|
+
RecordOpened = new EventEmitter();
|
|
508
421
|
/**
|
|
509
422
|
* Emitted when data is loaded
|
|
510
423
|
*/
|
|
511
|
-
|
|
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
|
-
|
|
428
|
+
FilterTextChange = new EventEmitter();
|
|
520
429
|
/**
|
|
521
430
|
* Emitted when filtered count changes
|
|
522
431
|
*/
|
|
523
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
474
|
+
AvailableViewTypes = [];
|
|
475
|
+
/** Whether the registry (ViewTypeEngine) successfully sourced the available view types. */
|
|
476
|
+
ViewTypesFromRegistry = false;
|
|
528
477
|
/**
|
|
529
|
-
*
|
|
478
|
+
* The currently-active view type's stable key (descriptor Name). Null until the registry resolves.
|
|
530
479
|
*/
|
|
531
|
-
|
|
480
|
+
ActiveViewTypeKey = null;
|
|
532
481
|
/**
|
|
533
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
497
|
+
viewTypeConfigById = new Map();
|
|
540
498
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
521
|
+
AutoSaveView = false;
|
|
549
522
|
/**
|
|
550
|
-
* Emitted
|
|
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
|
-
|
|
526
|
+
BeforeViewTypeChange = new EventEmitter();
|
|
553
527
|
/**
|
|
554
|
-
* Emitted
|
|
555
|
-
*
|
|
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
|
-
|
|
531
|
+
AfterViewTypeChange = new EventEmitter();
|
|
558
532
|
/**
|
|
559
|
-
* Emitted
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
565
|
+
HiddenFieldMatches = new Map();
|
|
576
566
|
/** Current sort state */
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
641
|
-
*
|
|
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
|
-
|
|
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
|
|
655
|
-
if (this.
|
|
656
|
-
return this.
|
|
609
|
+
get EffectiveEntity() {
|
|
610
|
+
if (this.Entity) {
|
|
611
|
+
return this.Entity;
|
|
657
612
|
}
|
|
658
613
|
// Auto-derive from viewEntity if available
|
|
659
|
-
if (this.
|
|
660
|
-
return this.getEntityInfoFromViewEntity(this.
|
|
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
|
|
704
|
-
return this.
|
|
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
|
|
731
|
-
return this.
|
|
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
|
|
747
|
-
return { ...DEFAULT_VIEWER_CONFIG, ...this.
|
|
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
|
|
793
|
-
return this.
|
|
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
|
|
800
|
-
const records = this.
|
|
677
|
+
get FilteredRecords() {
|
|
678
|
+
const records = this.DisplayRecords;
|
|
801
679
|
// If server-side filtering is enabled, records are already filtered
|
|
802
|
-
if (this.
|
|
680
|
+
if (this.EffectiveConfig.serverSideFiltering) {
|
|
803
681
|
return records;
|
|
804
682
|
}
|
|
805
683
|
// Client-side filtering fallback
|
|
806
|
-
const filterText = this.
|
|
807
|
-
this.
|
|
808
|
-
if (!filterText || !this.
|
|
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.
|
|
816
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
764
|
+
if (!this.Entity)
|
|
887
765
|
return visible;
|
|
888
|
-
for (const field of this.
|
|
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.
|
|
894
|
-
visible.add(this.
|
|
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
|
-
|
|
902
|
-
if (!this.
|
|
779
|
+
HasHiddenFieldMatch(record) {
|
|
780
|
+
if (!this.DebouncedFilterText || !this.Entity)
|
|
903
781
|
return false;
|
|
904
|
-
return this.
|
|
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
|
-
|
|
910
|
-
if (!this.
|
|
787
|
+
GetHiddenMatchFieldName(record) {
|
|
788
|
+
if (!this.Entity)
|
|
911
789
|
return '';
|
|
912
|
-
const fieldName = this.
|
|
913
|
-
if (!fieldName || !this.
|
|
790
|
+
const fieldName = this.HiddenFieldMatches.get(buildPkString(record, this.Entity));
|
|
791
|
+
if (!fieldName || !this.Entity)
|
|
914
792
|
return '';
|
|
915
|
-
const field = this.
|
|
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.
|
|
926
|
-
this.
|
|
803
|
+
if (this.FilterText !== null) {
|
|
804
|
+
this.DebouncedFilterText = this.FilterText;
|
|
927
805
|
}
|
|
928
806
|
// Initialize sort state from config
|
|
929
|
-
if (this.
|
|
930
|
-
this.
|
|
931
|
-
field: this.
|
|
932
|
-
direction: this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
945
|
+
this.InternalSortState = null;
|
|
1011
946
|
}
|
|
1012
947
|
applyConfig() {
|
|
1013
|
-
const config = this.
|
|
1014
|
-
this.
|
|
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.
|
|
953
|
+
.pipe(debounceTime(this.EffectiveConfig.filterDebounceMs), distinctUntilChanged(), takeUntil(this.destroy$))
|
|
1022
954
|
.subscribe(filterText => {
|
|
1023
|
-
const oldFilter = this.
|
|
1024
|
-
this.
|
|
1025
|
-
this.
|
|
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.
|
|
960
|
+
if (this.EffectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.Records) {
|
|
1029
961
|
this.resetPaginationState(false);
|
|
1030
|
-
this.
|
|
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.
|
|
1043
|
-
if (this.
|
|
1044
|
-
this.
|
|
1045
|
-
this.
|
|
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.
|
|
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.
|
|
990
|
+
this.Pagination = {
|
|
1058
991
|
currentPage: 0,
|
|
1059
|
-
pageSize: this.
|
|
1060
|
-
totalRecords: clearRecords ? 0 : this.
|
|
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.
|
|
1066
|
-
this.
|
|
1067
|
-
this.
|
|
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
|
|
1082
|
-
const entity = this.
|
|
1014
|
+
async LoadData() {
|
|
1015
|
+
const entity = this.EffectiveEntity;
|
|
1083
1016
|
if (!entity) {
|
|
1084
|
-
this.
|
|
1085
|
-
this.
|
|
1086
|
-
this.
|
|
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.
|
|
1027
|
+
if (this.IsLoading) {
|
|
1095
1028
|
this._pendingReload = true;
|
|
1096
1029
|
return;
|
|
1097
1030
|
}
|
|
1098
|
-
this.
|
|
1099
|
-
this.
|
|
1100
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
1115
|
-
orderBy = this.
|
|
1047
|
+
else if (this.ViewEntity?.OrderByClause) {
|
|
1048
|
+
orderBy = this.ViewEntity.OrderByClause;
|
|
1116
1049
|
}
|
|
1117
|
-
else if (this.
|
|
1118
|
-
orderBy = this.
|
|
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
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
// standard page-based pagination.
|
|
1125
|
-
|
|
1126
|
-
const maxRows =
|
|
1127
|
-
const startRow =
|
|
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.
|
|
1063
|
+
const extraFilter = this.ViewEntity?.WhereClause || undefined;
|
|
1131
1064
|
const result = await rv.RunView({
|
|
1132
1065
|
EntityName: entity.Name,
|
|
1133
1066
|
ResultType: 'simple',
|
|
1134
|
-
|
|
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.
|
|
1142
|
-
? this.
|
|
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.
|
|
1152
|
-
this.
|
|
1153
|
-
this.
|
|
1089
|
+
this.InternalRecords = result.Results;
|
|
1090
|
+
this.TotalRecordCount = result.TotalRowCount;
|
|
1091
|
+
this.FilteredRecordCount = this.InternalRecords.length;
|
|
1154
1092
|
// Update pagination state
|
|
1155
|
-
this.
|
|
1156
|
-
this.
|
|
1157
|
-
|
|
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.
|
|
1097
|
+
loadedRowCount: this.InternalRecords.length,
|
|
1162
1098
|
loadTime: Date.now() - startTime,
|
|
1163
|
-
records: this.
|
|
1099
|
+
records: this.InternalRecords
|
|
1164
1100
|
});
|
|
1165
|
-
this.
|
|
1166
|
-
filteredCount: this.
|
|
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.
|
|
1108
|
+
this.InternalRecords = [];
|
|
1175
1109
|
}
|
|
1176
|
-
this.
|
|
1177
|
-
this.
|
|
1110
|
+
this.TotalRecordCount = 0;
|
|
1111
|
+
this.FilteredRecordCount = 0;
|
|
1178
1112
|
}
|
|
1179
1113
|
}
|
|
1180
1114
|
catch (error) {
|
|
1181
1115
|
if (this.isInitialLoad) {
|
|
1182
|
-
this.
|
|
1116
|
+
this.InternalRecords = [];
|
|
1183
1117
|
}
|
|
1184
|
-
this.
|
|
1185
|
-
this.
|
|
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.
|
|
1193
|
-
this.
|
|
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.
|
|
1137
|
+
this.LoadData();
|
|
1203
1138
|
}
|
|
1204
1139
|
}
|
|
1205
1140
|
}
|
|
1206
1141
|
/**
|
|
1207
1142
|
* Handle page change from PaginationComponent
|
|
1208
1143
|
*/
|
|
1209
|
-
|
|
1210
|
-
this.
|
|
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.
|
|
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
|
-
|
|
1219
|
-
if (!this.
|
|
1153
|
+
Refresh() {
|
|
1154
|
+
if (!this.Records) {
|
|
1220
1155
|
this.resetPaginationState(false);
|
|
1221
|
-
this.
|
|
1156
|
+
this.LoadData();
|
|
1222
1157
|
}
|
|
1223
1158
|
}
|
|
1224
1159
|
// ========================================
|
|
1225
1160
|
// VIEW MODE
|
|
1226
1161
|
// ========================================
|
|
1227
1162
|
/**
|
|
1228
|
-
*
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1295
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
1301
|
-
this.
|
|
1232
|
+
get ActiveViewTypeOption() {
|
|
1233
|
+
return this.AvailableViewTypes.find(o => o.key === this.ActiveViewTypeKey) ?? null;
|
|
1302
1234
|
}
|
|
1303
1235
|
/**
|
|
1304
|
-
*
|
|
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
|
-
|
|
1307
|
-
this.
|
|
1241
|
+
SelectViewType(option) {
|
|
1242
|
+
this.ViewTypeDropdownOpen = false;
|
|
1243
|
+
this.applyViewTypeSelection(option, true);
|
|
1308
1244
|
}
|
|
1309
1245
|
/**
|
|
1310
|
-
*
|
|
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
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
*
|
|
1334
|
-
*
|
|
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
|
-
|
|
1337
|
-
|
|
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
|
-
*
|
|
1348
|
-
*
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
1358
|
-
this.
|
|
1359
|
-
this.
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
if (this.
|
|
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.
|
|
1306
|
+
this.LoadData();
|
|
1369
1307
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1322
|
+
// VIEW-TYPE PERSISTENCE (provider-based; never routing)
|
|
1449
1323
|
// ========================================
|
|
1450
1324
|
/**
|
|
1451
|
-
*
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
this.
|
|
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
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
this.
|
|
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
|
-
/**
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1536
|
-
if (this.
|
|
1537
|
-
this.
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
*
|
|
1567
|
-
*
|
|
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
|
-
|
|
1570
|
-
if (
|
|
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
|
-
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
-
*
|
|
1594
|
-
*
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
*
|
|
1603
|
-
*
|
|
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
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
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
|
-
|
|
1616
|
-
const
|
|
1617
|
-
const
|
|
1618
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1624
|
-
if (
|
|
1625
|
-
|
|
1626
|
-
return;
|
|
1587
|
+
setDynamicInput(ref, name, value) {
|
|
1588
|
+
if (this.dynamicInputNames.has(name)) {
|
|
1589
|
+
ref.setInput(name, value);
|
|
1627
1590
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
this.
|
|
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
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1697
|
-
if (!this.
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
1704
|
-
|
|
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
|
-
*
|
|
1667
|
+
* Handle filter input change
|
|
1716
1668
|
*/
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
*
|
|
1674
|
+
* Clear the filter
|
|
1732
1675
|
*/
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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(
|
|
1683
|
+
i0.ɵɵviewQuery(_c0, 5, ViewContainerRef);
|
|
1754
1684
|
} if (rf & 2) {
|
|
1755
1685
|
let _t;
|
|
1756
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
1757
|
-
} }, hostAttrs: [2, "display", "block", "height", "100%"], inputs: {
|
|
1758
|
-
i0.ɵɵelementStart(0, "div",
|
|
1759
|
-
i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Template,
|
|
1760
|
-
i0.ɵɵelementStart(2, "div",
|
|
1761
|
-
i0.ɵɵelement(4, "mj-loading",
|
|
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",
|
|
1764
|
-
i0.ɵɵelement(6, "mj-loading",
|
|
1693
|
+
i0.ɵɵelementStart(5, "div", 6);
|
|
1694
|
+
i0.ɵɵelement(6, "mj-loading", 7);
|
|
1765
1695
|
i0.ɵɵelementEnd();
|
|
1766
|
-
i0.ɵɵelementStart(7, "div",
|
|
1767
|
-
i0.ɵɵelement(8, "i",
|
|
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",
|
|
1772
|
-
i0.ɵɵelement(12, "i",
|
|
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, "
|
|
1777
|
-
i0.ɵɵ
|
|
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.
|
|
1710
|
+
i0.ɵɵstyleProp("height", ctx.EffectiveConfig.height);
|
|
1789
1711
|
i0.ɵɵadvance();
|
|
1790
|
-
i0.ɵɵconditional(ctx.
|
|
1712
|
+
i0.ɵɵconditional(ctx.EffectiveConfig.showFilter || ctx.EffectiveConfig.showViewModeToggle || ctx.EffectiveConfig.showRecordCount ? 1 : -1);
|
|
1791
1713
|
i0.ɵɵadvance(2);
|
|
1792
|
-
i0.ɵɵproperty("hidden", !(ctx.
|
|
1714
|
+
i0.ɵɵproperty("hidden", !(ctx.IsLoading && ctx.FilteredRecords.length === 0));
|
|
1793
1715
|
i0.ɵɵadvance();
|
|
1794
|
-
i0.ɵɵproperty("text", ctx.
|
|
1716
|
+
i0.ɵɵproperty("text", ctx.LoadingMessage);
|
|
1795
1717
|
i0.ɵɵadvance();
|
|
1796
|
-
i0.ɵɵproperty("hidden", !(ctx.
|
|
1718
|
+
i0.ɵɵproperty("hidden", !(ctx.IsLoading && ctx.FilteredRecords.length > 0));
|
|
1797
1719
|
i0.ɵɵadvance();
|
|
1798
|
-
i0.ɵɵproperty("text", ctx.
|
|
1720
|
+
i0.ɵɵproperty("text", ctx.LoadingMessage);
|
|
1799
1721
|
i0.ɵɵadvance();
|
|
1800
|
-
i0.ɵɵproperty("hidden", !!ctx.
|
|
1722
|
+
i0.ɵɵproperty("hidden", !!ctx.EffectiveEntity);
|
|
1801
1723
|
i0.ɵɵadvance(4);
|
|
1802
|
-
i0.ɵɵproperty("hidden", !ctx.
|
|
1724
|
+
i0.ɵɵproperty("hidden", !ctx.EffectiveEntity || ctx.FilteredRecords.length > 0 || ctx.IsLoading || ctx.IsDynamicViewActive);
|
|
1803
1725
|
i0.ɵɵadvance(3);
|
|
1804
|
-
i0.ɵɵtextInterpolate(ctx.
|
|
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",
|
|
1809
|
-
|
|
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 }], {
|
|
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
|
-
}],
|
|
1738
|
+
}], EntityName: [{
|
|
1825
1739
|
type: Input
|
|
1826
|
-
}],
|
|
1740
|
+
}], EntityID: [{
|
|
1827
1741
|
type: Input
|
|
1828
|
-
}],
|
|
1742
|
+
}], ViewID: [{
|
|
1829
1743
|
type: Input
|
|
1830
|
-
}],
|
|
1744
|
+
}], Records: [{
|
|
1831
1745
|
type: Input
|
|
1832
|
-
}],
|
|
1746
|
+
}], Config: [{
|
|
1833
1747
|
type: Input
|
|
1834
|
-
}],
|
|
1748
|
+
}], SelectedRecordID: [{
|
|
1835
1749
|
type: Input
|
|
1836
|
-
}],
|
|
1750
|
+
}], FilterText: [{
|
|
1837
1751
|
type: Input
|
|
1838
|
-
}],
|
|
1752
|
+
}], SortState: [{
|
|
1839
1753
|
type: Input
|
|
1840
|
-
}],
|
|
1754
|
+
}], ViewEntity: [{
|
|
1841
1755
|
type: Input
|
|
1842
|
-
}],
|
|
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
|
-
}],
|
|
1855
|
-
type: Output
|
|
1856
|
-
}], recordOpened: [{
|
|
1760
|
+
}], RecordSelected: [{
|
|
1857
1761
|
type: Output
|
|
1858
|
-
}],
|
|
1762
|
+
}], RecordOpened: [{
|
|
1859
1763
|
type: Output
|
|
1860
|
-
}],
|
|
1764
|
+
}], DataLoaded: [{
|
|
1861
1765
|
type: Output
|
|
1862
|
-
}],
|
|
1766
|
+
}], FilterTextChange: [{
|
|
1863
1767
|
type: Output
|
|
1864
|
-
}],
|
|
1768
|
+
}], FilteredCountChanged: [{
|
|
1865
1769
|
type: Output
|
|
1866
|
-
}],
|
|
1770
|
+
}], OpenRelatedRecordRequested: [{
|
|
1867
1771
|
type: Output
|
|
1868
|
-
}],
|
|
1772
|
+
}], CreateRecordRequested: [{
|
|
1869
1773
|
type: Output
|
|
1870
|
-
}],
|
|
1871
|
-
type:
|
|
1872
|
-
}],
|
|
1873
|
-
type:
|
|
1874
|
-
}],
|
|
1875
|
-
type:
|
|
1876
|
-
}],
|
|
1774
|
+
}], ViewTypeID: [{
|
|
1775
|
+
type: Input
|
|
1776
|
+
}], ViewTypeConfigs: [{
|
|
1777
|
+
type: Input
|
|
1778
|
+
}], AutoSaveView: [{
|
|
1779
|
+
type: Input
|
|
1780
|
+
}], BeforeViewTypeChange: [{
|
|
1877
1781
|
type: Output
|
|
1878
|
-
}],
|
|
1782
|
+
}], AfterViewTypeChange: [{
|
|
1879
1783
|
type: Output
|
|
1880
|
-
}],
|
|
1784
|
+
}], BeforeViewTypeConfigChange: [{
|
|
1881
1785
|
type: Output
|
|
1882
|
-
}],
|
|
1786
|
+
}], AfterViewTypeConfigChange: [{
|
|
1883
1787
|
type: Output
|
|
1884
|
-
}],
|
|
1788
|
+
}], dynamicViewHost: [{
|
|
1885
1789
|
type: ViewChild,
|
|
1886
|
-
args: [
|
|
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:
|
|
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
|