@memberjunction/ng-entity-viewer 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts +5 -0
- package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts.map +1 -1
- package/dist/lib/aggregate-panel/aggregate-panel.component.js +45 -47
- package/dist/lib/aggregate-panel/aggregate-panel.component.js.map +1 -1
- package/dist/lib/entity-cards/entity-cards.component.js +7 -10
- package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +10 -1
- 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 +35 -34
- package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +0 -4
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -1
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +166 -207
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts +109 -5
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.js +287 -42
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
- package/dist/lib/recycle-bin/recycle-bin.component.js +21 -44
- package/dist/lib/recycle-bin/recycle-bin.component.js.map +1 -1
- package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js +19 -20
- package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.js +268 -301
- package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
- package/dist/lib/view-selector/view-selector.component.d.ts +26 -1
- package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -1
- package/dist/lib/view-selector/view-selector.component.js +118 -76
- package/dist/lib/view-selector/view-selector.component.js.map +1 -1
- package/dist/lib/view-types/descriptors/grid-view-type.d.ts +7 -0
- package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -1
- package/dist/lib/view-types/descriptors/grid-view-type.js +7 -0
- package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -1
- package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +18 -1
- package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -1
- package/dist/lib/view-types/renderers/grid-view-renderer.component.js +29 -3
- package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -1
- package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -1
- package/dist/lib/view-types/renderers/map-view-renderer.component.js +11 -14
- package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -1
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -1
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +12 -14
- package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -1
- package/dist/lib/view-types/view-type.contracts.d.ts +44 -0
- package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -1
- package/dist/lib/view-types/view-type.contracts.js +6 -0
- package/dist/lib/view-types/view-type.contracts.js.map +1 -1
- package/dist/lib/view-workspace/view-workspace.component.d.ts +32 -1
- package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -1
- package/dist/lib/view-workspace/view-workspace.component.js +68 -7
- package/dist/lib/view-workspace/view-workspace.component.js.map +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +10 -1
- package/dist/module.js.map +1 -1
- package/package.json +18 -17
- package/dist/__tests__/entity-cards-selection.test.d.ts +0 -2
- package/dist/__tests__/entity-cards-selection.test.d.ts.map +0 -1
- package/dist/__tests__/entity-cards-selection.test.js +0 -149
- package/dist/__tests__/entity-cards-selection.test.js.map +0 -1
- package/dist/__tests__/entity-cards-viewmodel.test.d.ts +0 -2
- package/dist/__tests__/entity-cards-viewmodel.test.d.ts.map +0 -1
- package/dist/__tests__/entity-cards-viewmodel.test.js +0 -208
- package/dist/__tests__/entity-cards-viewmodel.test.js.map +0 -1
- package/dist/__tests__/highlight.test.d.ts +0 -2
- package/dist/__tests__/highlight.test.d.ts.map +0 -1
- package/dist/__tests__/highlight.test.js +0 -67
- package/dist/__tests__/highlight.test.js.map +0 -1
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -7
- package/dist/__tests__/index.test.js.map +0 -1
- package/dist/__tests__/view-types.test.d.ts +0 -2
- package/dist/__tests__/view-types.test.d.ts.map +0 -1
- package/dist/__tests__/view-types.test.js +0 -102
- package/dist/__tests__/view-types.test.js.map +0 -1
|
@@ -10,25 +10,26 @@ import { DEFAULT_VIEWER_CONFIG } from '../types';
|
|
|
10
10
|
import { ViewTypeEngine } from '../view-types';
|
|
11
11
|
import * as i0 from "@angular/core";
|
|
12
12
|
import * as i1 from "@memberjunction/ng-shared-generic";
|
|
13
|
-
import * as i2 from "
|
|
14
|
-
import * as i3 from "../
|
|
15
|
-
import * as i4 from "
|
|
13
|
+
import * as i2 from "@memberjunction/ng-ui-components";
|
|
14
|
+
import * as i3 from "../recycle-bin/recycle-bin-chip.component";
|
|
15
|
+
import * as i4 from "../view-type-switcher/view-type-switcher.component";
|
|
16
|
+
import * as i5 from "@angular/common";
|
|
16
17
|
const _c0 = ["dynamicViewHost"];
|
|
17
18
|
function EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
18
19
|
const _r3 = i0.ɵɵgetCurrentView();
|
|
19
|
-
i0.ɵɵelementStart(0, "button",
|
|
20
|
+
i0.ɵɵelementStart(0, "button", 18);
|
|
20
21
|
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",
|
|
22
|
+
i0.ɵɵelement(1, "i", 19);
|
|
22
23
|
i0.ɵɵelementEnd();
|
|
23
24
|
} }
|
|
24
25
|
function EntityViewerComponent_Conditional_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
25
26
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
26
|
-
i0.ɵɵelementStart(0, "div",
|
|
27
|
-
i0.ɵɵelement(1, "i",
|
|
28
|
-
i0.ɵɵelementStart(2, "input",
|
|
27
|
+
i0.ɵɵelementStart(0, "div", 11);
|
|
28
|
+
i0.ɵɵelement(1, "i", 15);
|
|
29
|
+
i0.ɵɵelementStart(2, "input", 16);
|
|
29
30
|
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)); });
|
|
30
31
|
i0.ɵɵelementEnd();
|
|
31
|
-
i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button",
|
|
32
|
+
i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button", 17);
|
|
32
33
|
i0.ɵɵelementEnd();
|
|
33
34
|
} if (rf & 2) {
|
|
34
35
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
@@ -59,7 +60,7 @@ function EntityViewerComponent_Conditional_1_Conditional_2_Conditional_2_Templat
|
|
|
59
60
|
i0.ɵɵtextInterpolate1("", i0.ɵɵpipeBind1(2, 1, ctx_r1.TotalRecordCount), " records");
|
|
60
61
|
} }
|
|
61
62
|
function EntityViewerComponent_Conditional_1_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
62
|
-
i0.ɵɵelementStart(0, "div",
|
|
63
|
+
i0.ɵɵelementStart(0, "div", 12);
|
|
63
64
|
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");
|
|
64
65
|
i0.ɵɵelementEnd();
|
|
65
66
|
} if (rf & 2) {
|
|
@@ -69,7 +70,7 @@ function EntityViewerComponent_Conditional_1_Conditional_2_Template(rf, ctx) { i
|
|
|
69
70
|
} }
|
|
70
71
|
function EntityViewerComponent_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
71
72
|
const _r4 = i0.ɵɵgetCurrentView();
|
|
72
|
-
i0.ɵɵelementStart(0, "mj-view-type-switcher",
|
|
73
|
+
i0.ɵɵelementStart(0, "mj-view-type-switcher", 20);
|
|
73
74
|
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)); });
|
|
74
75
|
i0.ɵɵelementEnd();
|
|
75
76
|
} if (rf & 2) {
|
|
@@ -77,17 +78,17 @@ function EntityViewerComponent_Conditional_1_Conditional_3_Template(rf, ctx) { i
|
|
|
77
78
|
i0.ɵɵproperty("Provider", ctx_r1.Provider)("Entity", ctx_r1.EffectiveEntity)("ActiveViewTypeID", ctx_r1.ActiveViewTypeId);
|
|
78
79
|
} }
|
|
79
80
|
function EntityViewerComponent_Conditional_1_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
80
|
-
i0.ɵɵelement(0, "mj-recycle-bin-chip",
|
|
81
|
+
i0.ɵɵelement(0, "mj-recycle-bin-chip", 14);
|
|
81
82
|
} if (rf & 2) {
|
|
82
83
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
83
84
|
i0.ɵɵproperty("EntityName", (ctx_r1.EffectiveEntity == null ? null : ctx_r1.EffectiveEntity.Name) ?? null);
|
|
84
85
|
} }
|
|
85
86
|
function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
86
87
|
i0.ɵɵelementStart(0, "div", 2);
|
|
87
|
-
i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div",
|
|
88
|
-
i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div",
|
|
89
|
-
i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_3_Template, 1, 3, "mj-view-type-switcher",
|
|
90
|
-
i0.ɵɵconditionalCreate(4, EntityViewerComponent_Conditional_1_Conditional_4_Template, 1, 1, "mj-recycle-bin-chip",
|
|
88
|
+
i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Conditional_1_Template, 4, 3, "div", 11);
|
|
89
|
+
i0.ɵɵconditionalCreate(2, EntityViewerComponent_Conditional_1_Conditional_2_Template, 3, 1, "div", 12);
|
|
90
|
+
i0.ɵɵconditionalCreate(3, EntityViewerComponent_Conditional_1_Conditional_3_Template, 1, 3, "mj-view-type-switcher", 13);
|
|
91
|
+
i0.ɵɵconditionalCreate(4, EntityViewerComponent_Conditional_1_Conditional_4_Template, 1, 1, "mj-recycle-bin-chip", 14);
|
|
91
92
|
i0.ɵɵelementEnd();
|
|
92
93
|
} if (rf & 2) {
|
|
93
94
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
@@ -100,7 +101,16 @@ function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
|
100
101
|
i0.ɵɵadvance();
|
|
101
102
|
i0.ɵɵconditional(ctx_r1.ShowRecycleBin ? 4 : -1);
|
|
102
103
|
} }
|
|
103
|
-
function
|
|
104
|
+
function EntityViewerComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
105
|
+
i0.ɵɵelement(0, "mj-empty-state", 8);
|
|
106
|
+
} }
|
|
107
|
+
function EntityViewerComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
108
|
+
i0.ɵɵelement(0, "mj-empty-state", 9);
|
|
109
|
+
} if (rf & 2) {
|
|
110
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
111
|
+
i0.ɵɵproperty("Variant", ctx_r1.IsNoRecordsFiltered ? "no-results" : "empty")("Title", ctx_r1.NoRecordsTitle);
|
|
112
|
+
} }
|
|
113
|
+
function EntityViewerComponent_ng_template_10_Template(rf, ctx) { }
|
|
104
114
|
/**
|
|
105
115
|
* EntityViewerComponent - Full-featured composite component for viewing entity data
|
|
106
116
|
*
|
|
@@ -394,11 +404,28 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
394
404
|
this.deferReload();
|
|
395
405
|
}
|
|
396
406
|
}
|
|
407
|
+
_gridState = null;
|
|
397
408
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
409
|
+
* Canonical grid state for the current view — the single, framework-wide source of truth for a
|
|
410
|
+
* view's columns (visibility / order / width / formatting), sort, filter and aggregates. It is
|
|
411
|
+
* the `UserView.GridState` column, also read by `MJUserViewEntity.Columns`, the GraphQL data
|
|
412
|
+
* provider's server-side field list, the config panel, and export.
|
|
413
|
+
*
|
|
414
|
+
* Any view type that {@link IViewTypeDescriptor.UsesCanonicalGridState} is backed by this store
|
|
415
|
+
* rather than by an opaque per-view-type blob (see {@link effectiveRendererConfig}). Reactive:
|
|
416
|
+
* when the host updates this after the renderer is mounted (e.g. the config panel just saved new
|
|
417
|
+
* columns), we re-push the active renderer's config so the change reflects without a full reload.
|
|
400
418
|
*/
|
|
401
|
-
GridState
|
|
419
|
+
set GridState(value) {
|
|
420
|
+
const previous = this._gridState;
|
|
421
|
+
this._gridState = value;
|
|
422
|
+
if (this._initialized && value !== previous) {
|
|
423
|
+
this.refreshCanonicalGridStateRenderer();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
get GridState() {
|
|
427
|
+
return this._gridState;
|
|
428
|
+
}
|
|
402
429
|
/**
|
|
403
430
|
* Whether to render the Recycle Bin chip in the viewer header.
|
|
404
431
|
* The chip auto-hides itself when the entity has no deleted records,
|
|
@@ -443,6 +470,12 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
443
470
|
* the outer app; the container forwards it without acting on it.
|
|
444
471
|
*/
|
|
445
472
|
CreateRecordRequested = new EventEmitter();
|
|
473
|
+
/**
|
|
474
|
+
* Bubbled up from a plug-in renderer asking the host to open this view's configuration UI
|
|
475
|
+
* (e.g. the grid's "Manage Columns" affordance). The host (the view workspace) owns the config
|
|
476
|
+
* panel; the container just forwards the request. See {@link IViewRenderer.configureRequested}.
|
|
477
|
+
*/
|
|
478
|
+
ConfigureRequested = new EventEmitter();
|
|
446
479
|
/**
|
|
447
480
|
* The initial/active view type to open in, by `MJ: View Types` row ID. Hosts that persist
|
|
448
481
|
* the selection (e.g. Explorer's `UserView.ViewTypeID`) bind this so the viewer opens in the
|
|
@@ -616,6 +649,14 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
616
649
|
}
|
|
617
650
|
return null;
|
|
618
651
|
}
|
|
652
|
+
/** Title shown in the "no records" empty state — varies with the active filter. */
|
|
653
|
+
get NoRecordsTitle() {
|
|
654
|
+
return this.DebouncedFilterText ? 'No matching records' : 'No records found';
|
|
655
|
+
}
|
|
656
|
+
/** True when the "no records" empty state is the result of an active filter. */
|
|
657
|
+
get IsNoRecordsFiltered() {
|
|
658
|
+
return !!this.DebouncedFilterText;
|
|
659
|
+
}
|
|
619
660
|
/**
|
|
620
661
|
* Gets EntityInfo from a ViewEntity with multiple fallback strategies.
|
|
621
662
|
* Priority: 1) ViewEntityInfo property (set by Load)
|
|
@@ -1157,6 +1198,118 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1157
1198
|
}
|
|
1158
1199
|
}
|
|
1159
1200
|
// ========================================
|
|
1201
|
+
// PROGRAMMATIC GRID CONTROL (agent / external driver)
|
|
1202
|
+
// ========================================
|
|
1203
|
+
// These thin, public methods expose the same pagination/sort the user drives
|
|
1204
|
+
// interactively so an external driver (e.g. the AI agent, via the dashboard's
|
|
1205
|
+
// ViewWorkspace passthrough) can page and sort the grid. They are no-ops when
|
|
1206
|
+
// records are externally supplied (no internal RunView to retrigger).
|
|
1207
|
+
/** Current (1-based) page number the grid is showing. Internally stored 0-based. */
|
|
1208
|
+
get CurrentPageNumber() {
|
|
1209
|
+
return this.Pagination.currentPage + 1;
|
|
1210
|
+
}
|
|
1211
|
+
/** Number of records loaded per page. */
|
|
1212
|
+
get CurrentPageSize() {
|
|
1213
|
+
return this.Pagination.pageSize;
|
|
1214
|
+
}
|
|
1215
|
+
/** Total records available for the current entity/view/filter (from the server). */
|
|
1216
|
+
get TotalRecords() {
|
|
1217
|
+
return this.Pagination.totalRecords;
|
|
1218
|
+
}
|
|
1219
|
+
/** The grid's active sort state, or null when unsorted. */
|
|
1220
|
+
get CurrentSortState() {
|
|
1221
|
+
return this.InternalSortState;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Navigate to a specific (1-based) page. Clamps to the valid range. Returns the
|
|
1225
|
+
* page actually navigated to, or null when paging isn't applicable (externally
|
|
1226
|
+
* supplied records).
|
|
1227
|
+
*/
|
|
1228
|
+
GoToPageNumber(pageNumber) {
|
|
1229
|
+
if (this._records) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
const totalPages = this.TotalPageCount;
|
|
1233
|
+
const clamped = Math.min(Math.max(1, Math.floor(pageNumber)), Math.max(1, totalPages));
|
|
1234
|
+
this.OnPageChange({ PageNumber: clamped, PageSize: this.Pagination.pageSize });
|
|
1235
|
+
return clamped;
|
|
1236
|
+
}
|
|
1237
|
+
/** Total page count derived from total records and page size (min 1). */
|
|
1238
|
+
get TotalPageCount() {
|
|
1239
|
+
const size = this.Pagination.pageSize;
|
|
1240
|
+
if (size <= 0 || this.Pagination.totalRecords <= 0) {
|
|
1241
|
+
return 1;
|
|
1242
|
+
}
|
|
1243
|
+
return Math.ceil(this.Pagination.totalRecords / size);
|
|
1244
|
+
}
|
|
1245
|
+
/** Advance to the next page (no-op past the last page). Returns the new 1-based page or null. */
|
|
1246
|
+
NextPage() {
|
|
1247
|
+
return this.GoToPageNumber(this.CurrentPageNumber + 1);
|
|
1248
|
+
}
|
|
1249
|
+
/** Go to the previous page (no-op before the first page). Returns the new 1-based page or null. */
|
|
1250
|
+
PreviousPage() {
|
|
1251
|
+
return this.GoToPageNumber(this.CurrentPageNumber - 1);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Set the server-side page size and reload from page 1. Returns the size applied,
|
|
1255
|
+
* or null when paging isn't applicable (externally supplied records).
|
|
1256
|
+
*/
|
|
1257
|
+
SetServerPageSize(pageSize) {
|
|
1258
|
+
if (this._records || !Number.isFinite(pageSize) || pageSize <= 0) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
const size = Math.floor(pageSize);
|
|
1262
|
+
this.Pagination.pageSize = size;
|
|
1263
|
+
this.OnPageChange({ PageNumber: 1, PageSize: size });
|
|
1264
|
+
return size;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Apply a server-side sort by field + direction and reload. Returns true when the
|
|
1268
|
+
* sort was applied, false when sorting isn't applicable (externally supplied records).
|
|
1269
|
+
*/
|
|
1270
|
+
ApplySort(field, direction) {
|
|
1271
|
+
if (this._records) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
// Drive through the public SortState input so the reload logic stays in one place.
|
|
1275
|
+
this.SortState = { field, direction };
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Programmatically select a record — the no-UI equivalent of a user row-click. Highlights
|
|
1280
|
+
* the row (by pushing {@link SelectedRecordID} down to the active renderer) AND emits
|
|
1281
|
+
* {@link RecordSelected} so the host runs its selection path (open the detail panel, etc.).
|
|
1282
|
+
* Returns false when there's no entity context. This is the entry point for an external
|
|
1283
|
+
* driver (the AI agent's SelectRecord tool); the record must be one of the loaded rows.
|
|
1284
|
+
*
|
|
1285
|
+
* @param record - a record from the currently-loaded set
|
|
1286
|
+
* @returns true when selection was applied, false when no entity context is available
|
|
1287
|
+
*/
|
|
1288
|
+
SelectRecord(record) {
|
|
1289
|
+
const entity = this.EffectiveEntity;
|
|
1290
|
+
if (!entity || !record) {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
const compositeKey = buildCompositeKey(record, entity);
|
|
1294
|
+
// Drive the highlight through the same input the user-click path uses.
|
|
1295
|
+
this.SelectedRecordID = compositeKey.ToConcatenatedString();
|
|
1296
|
+
this.RecordSelected.emit({ record, entity, compositeKey });
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Export the current view's records via the active renderer's optional imperative export
|
|
1301
|
+
* ({@link IViewRenderer.exportRecords}). Returns false when no renderer is mounted or the
|
|
1302
|
+
* active view type doesn't support export (e.g. timeline/map). The grid renderer downloads
|
|
1303
|
+
* the file itself; this is the no-UI entry point for an external driver (the AI agent).
|
|
1304
|
+
*/
|
|
1305
|
+
async ExportRecords(format) {
|
|
1306
|
+
const renderer = this.dynamicRendererRef?.instance;
|
|
1307
|
+
if (!renderer || typeof renderer.exportRecords !== 'function') {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
return renderer.exportRecords(format);
|
|
1311
|
+
}
|
|
1312
|
+
// ========================================
|
|
1160
1313
|
// VIEW MODE
|
|
1161
1314
|
// ========================================
|
|
1162
1315
|
/**
|
|
@@ -1520,6 +1673,9 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1520
1673
|
inst.dataRequest
|
|
1521
1674
|
?.pipe(takeUntil(this.destroy$))
|
|
1522
1675
|
.subscribe((req) => this.onDynamicDataRequest(req));
|
|
1676
|
+
inst.configureRequested
|
|
1677
|
+
?.pipe(takeUntil(this.destroy$))
|
|
1678
|
+
.subscribe(() => this.ConfigureRequested.emit());
|
|
1523
1679
|
const created = { ref, inputs };
|
|
1524
1680
|
this.dynamicRendererCache.set(option.viewTypeId, created);
|
|
1525
1681
|
return created;
|
|
@@ -1572,12 +1728,90 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1572
1728
|
this.setDynamicInput(ref, 'records', this.FilteredRecords);
|
|
1573
1729
|
this.setDynamicInput(ref, 'selectedRecordId', this.SelectedRecordID);
|
|
1574
1730
|
this.setDynamicInput(ref, 'filterText', this.DebouncedFilterText);
|
|
1575
|
-
this.setDynamicInput(ref, 'config', this.
|
|
1731
|
+
this.setDynamicInput(ref, 'config', this.effectiveRendererConfig(option));
|
|
1576
1732
|
this.setDynamicInput(ref, 'totalRecordCount', this.TotalRecordCount);
|
|
1577
1733
|
this.setDynamicInput(ref, 'page', this.Pagination.currentPage + 1);
|
|
1578
1734
|
this.setDynamicInput(ref, 'pageSize', this.Pagination.pageSize);
|
|
1579
1735
|
this.setDynamicInput(ref, 'isLoading', this.IsLoading);
|
|
1580
1736
|
}
|
|
1737
|
+
// ========================================
|
|
1738
|
+
// CANONICAL GRID-STATE BRIDGE
|
|
1739
|
+
// (keeps view types that render the view's columns reading/writing the one source of truth —
|
|
1740
|
+
// UserView.GridState — instead of a divergent per-view-type copy in DisplayState.viewTypeConfigs)
|
|
1741
|
+
// ========================================
|
|
1742
|
+
/**
|
|
1743
|
+
* The config object pushed to {@link option}'s renderer. For a view type that
|
|
1744
|
+
* {@link IViewTypeDescriptor.UsesCanonicalGridState}, its `gridState` is backed by the canonical
|
|
1745
|
+
* store ({@link GridState}) so the grid renders the same columns the config panel, the server
|
|
1746
|
+
* query, and export use — rather than the opaque per-view-type blob. All other view types get
|
|
1747
|
+
* their per-view-type config verbatim.
|
|
1748
|
+
*/
|
|
1749
|
+
effectiveRendererConfig(option) {
|
|
1750
|
+
const base = this.viewTypeConfigById.get(option.viewTypeId) ?? {};
|
|
1751
|
+
if (!option.descriptor.UsesCanonicalGridState) {
|
|
1752
|
+
return base;
|
|
1753
|
+
}
|
|
1754
|
+
const gridState = this.resolveCanonicalGridState(base);
|
|
1755
|
+
return gridState ? { ...base, gridState } : base;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Resolve the grid state a canonical view type should render, preferring the canonical store: the
|
|
1759
|
+
* explicit {@link GridState} input (hosts that own persistence, e.g. the view workspace, feed it),
|
|
1760
|
+
* else the loaded `UserView` record's GridState column (hosts that pass only `[viewEntity]`, e.g.
|
|
1761
|
+
* Explorer's view-resource). Falls back to any columns the per-view-type blob carried — a one-time
|
|
1762
|
+
* migration for views whose in-grid column edits landed in `DisplayState.viewTypeConfigs` before
|
|
1763
|
+
* this bridge existed; once such a view is re-saved, its columns persist to the canonical store.
|
|
1764
|
+
*/
|
|
1765
|
+
resolveCanonicalGridState(base) {
|
|
1766
|
+
const canonical = this._gridState ?? this.viewRecordGridState();
|
|
1767
|
+
if (canonical?.columnSettings?.length) {
|
|
1768
|
+
return canonical;
|
|
1769
|
+
}
|
|
1770
|
+
const fromBlob = base['gridState'];
|
|
1771
|
+
if (fromBlob?.columnSettings?.length) {
|
|
1772
|
+
return fromBlob;
|
|
1773
|
+
}
|
|
1774
|
+
// Neither has columns — return canonical (may still carry sort/aggregates) or nothing.
|
|
1775
|
+
return canonical ?? null;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* The canonical grid state off the loaded `UserView` record's GridState column, surfaced as a
|
|
1779
|
+
* {@link ViewGridState}. `GridStateObject` (`MJUserViewEntity_IGridState`) and `ViewGridState`
|
|
1780
|
+
* share the same persisted shape — the grid and config panel treat them interchangeably.
|
|
1781
|
+
*/
|
|
1782
|
+
viewRecordGridState() {
|
|
1783
|
+
const obj = this._viewEntity?.GridStateObject;
|
|
1784
|
+
return obj ? obj : null;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Re-push the active renderer's config when the canonical {@link GridState} changes after mount
|
|
1788
|
+
* (e.g. the config panel saved new columns). No-op unless a renderer is mounted and the active
|
|
1789
|
+
* view type uses the canonical grid state — so non-grid view types and the pre-mount phase are
|
|
1790
|
+
* unaffected. Pushing a fresh `config` object reference triggers the renderer's input-change path.
|
|
1791
|
+
*/
|
|
1792
|
+
refreshCanonicalGridStateRenderer() {
|
|
1793
|
+
const ref = this.dynamicRendererRef;
|
|
1794
|
+
const option = this.ActiveDynamicOption;
|
|
1795
|
+
if (!ref || !option?.descriptor.UsesCanonicalGridState) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
this.setDynamicInput(ref, 'config', this.effectiveRendererConfig(option));
|
|
1799
|
+
ref.changeDetectorRef.detectChanges();
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Persist a canonical grid-state change to the loaded `UserView` record's GridState column — the
|
|
1803
|
+
* same canonical store the config panel writes to. Only the record target reaches here (the
|
|
1804
|
+
* per-user default-view case is handled by the existing user-settings persistence). Logs (does
|
|
1805
|
+
* not throw) on failure, mirroring {@link saveViewEntity}.
|
|
1806
|
+
*/
|
|
1807
|
+
async persistCanonicalGridState(gridState) {
|
|
1808
|
+
const ve = this._viewEntity;
|
|
1809
|
+
if (!gridState || !ve?.ID) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
ve.GridStateObject = gridState;
|
|
1813
|
+
await this.saveViewEntity(ve);
|
|
1814
|
+
}
|
|
1581
1815
|
/**
|
|
1582
1816
|
* Guarded `setInput`: lean plug-ins implement only the core contract, so we only set an input the
|
|
1583
1817
|
* mounted plug-in actually declares (per {@link dynamicInputNames}, captured at mount via
|
|
@@ -1610,6 +1844,25 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1610
1844
|
return;
|
|
1611
1845
|
}
|
|
1612
1846
|
this.viewTypeConfigById.set(viewTypeId, config);
|
|
1847
|
+
// For a canonical-grid-state view type (e.g. the grid), the columns/sort the renderer just
|
|
1848
|
+
// changed belong in the canonical `UserView.GridState` store — the same place the config panel,
|
|
1849
|
+
// the server-side field list, and export read. Mirror it into the input so re-pushes/reads stay
|
|
1850
|
+
// fresh, and (for a saved view record) persist to the GridState column instead of the opaque
|
|
1851
|
+
// per-view-type blob. The per-user default-view case has no record, so it keeps the existing
|
|
1852
|
+
// user-settings persistence (which already captures the full config including gridState).
|
|
1853
|
+
const usesCanonicalGridState = this.AvailableViewTypes.find(o => o.viewTypeId === viewTypeId)
|
|
1854
|
+
?.descriptor.UsesCanonicalGridState === true;
|
|
1855
|
+
if (usesCanonicalGridState) {
|
|
1856
|
+
const newGridState = config['gridState'];
|
|
1857
|
+
if (newGridState) {
|
|
1858
|
+
this._gridState = newGridState;
|
|
1859
|
+
}
|
|
1860
|
+
if (this.AutoSaveView && this.persistenceTarget() === 'record') {
|
|
1861
|
+
void this.persistCanonicalGridState(newGridState);
|
|
1862
|
+
this.AfterViewTypeConfigChange.emit({ ViewTypeID: viewTypeId, Config: config, Cancel: false });
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1613
1866
|
if (this.AutoSaveView) {
|
|
1614
1867
|
void this.persistViewTypeConfig(viewTypeId);
|
|
1615
1868
|
}
|
|
@@ -1684,7 +1937,7 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1684
1937
|
} if (rf & 2) {
|
|
1685
1938
|
let _t;
|
|
1686
1939
|
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:
|
|
1940
|
+
} }, 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", ConfigureRequested: "ConfigureRequested", BeforeViewTypeChange: "BeforeViewTypeChange", AfterViewTypeChange: "AfterViewTypeChange", BeforeViewTypeConfigChange: "BeforeViewTypeConfigChange", AfterViewTypeConfigChange: "AfterViewTypeConfigChange" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 12, vars: 10, 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"], ["Icon", "fa-solid fa-database", "Title", "Select an entity to view records", 1, "empty-state-fill"], [1, "empty-state-fill", 3, "Variant", "Title"], [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
1941
|
i0.ɵɵelementStart(0, "div", 1);
|
|
1689
1942
|
i0.ɵɵconditionalCreate(1, EntityViewerComponent_Conditional_1_Template, 5, 4, "div", 2);
|
|
1690
1943
|
i0.ɵɵelementStart(2, "div", 3)(3, "div", 4);
|
|
@@ -1693,18 +1946,10 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1693
1946
|
i0.ɵɵelementStart(5, "div", 6);
|
|
1694
1947
|
i0.ɵɵelement(6, "mj-loading", 7);
|
|
1695
1948
|
i0.ɵɵelementEnd();
|
|
1696
|
-
i0.ɵɵ
|
|
1697
|
-
i0.ɵɵ
|
|
1698
|
-
i0.ɵɵelementStart(9, "
|
|
1699
|
-
i0.ɵɵ
|
|
1700
|
-
i0.ɵɵelementEnd()();
|
|
1701
|
-
i0.ɵɵelementStart(11, "div", 8);
|
|
1702
|
-
i0.ɵɵelement(12, "i", 10);
|
|
1703
|
-
i0.ɵɵelementStart(13, "p");
|
|
1704
|
-
i0.ɵɵtext(14);
|
|
1705
|
-
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);
|
|
1949
|
+
i0.ɵɵconditionalCreate(7, EntityViewerComponent_Conditional_7_Template, 1, 0, "mj-empty-state", 8);
|
|
1950
|
+
i0.ɵɵconditionalCreate(8, EntityViewerComponent_Conditional_8_Template, 1, 2, "mj-empty-state", 9);
|
|
1951
|
+
i0.ɵɵelementStart(9, "div", 10);
|
|
1952
|
+
i0.ɵɵtemplate(10, EntityViewerComponent_ng_template_10_Template, 0, 0, "ng-template", null, 0, i0.ɵɵtemplateRefExtractor);
|
|
1708
1953
|
i0.ɵɵelementEnd()()();
|
|
1709
1954
|
} if (rf & 2) {
|
|
1710
1955
|
i0.ɵɵstyleProp("height", ctx.EffectiveConfig.height);
|
|
@@ -1719,20 +1964,18 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1719
1964
|
i0.ɵɵadvance();
|
|
1720
1965
|
i0.ɵɵproperty("text", ctx.LoadingMessage);
|
|
1721
1966
|
i0.ɵɵadvance();
|
|
1722
|
-
i0.ɵɵ
|
|
1723
|
-
i0.ɵɵadvance(
|
|
1724
|
-
i0.ɵɵ
|
|
1725
|
-
i0.ɵɵadvance(3);
|
|
1726
|
-
i0.ɵɵtextInterpolate(ctx.DebouncedFilterText ? "No matching records" : "No records found");
|
|
1967
|
+
i0.ɵɵconditional(!ctx.EffectiveEntity ? 7 : -1);
|
|
1968
|
+
i0.ɵɵadvance();
|
|
1969
|
+
i0.ɵɵconditional(ctx.EffectiveEntity && ctx.FilteredRecords.length === 0 && !ctx.IsLoading && !ctx.IsDynamicViewActive ? 8 : -1);
|
|
1727
1970
|
i0.ɵɵadvance();
|
|
1728
1971
|
i0.ɵɵproperty("hidden", !ctx.EffectiveEntity);
|
|
1729
|
-
} }, dependencies: [i1.LoadingComponent, i2.
|
|
1972
|
+
} }, dependencies: [i1.LoadingComponent, i2.MJEmptyStateComponent, i3.RecycleBinChipComponent, i4.ViewTypeSwitcherComponent, i5.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-fill[_ngcontent-%COMP%] {\n height: 100%;\n}\n\n\n\n.dynamic-view-host[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}"] });
|
|
1730
1973
|
}
|
|
1731
1974
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
|
|
1732
1975
|
type: Component,
|
|
1733
1976
|
args: [{ standalone: false, selector: 'mj-entity-viewer', host: {
|
|
1734
1977
|
'style': 'display: block; height: 100%;'
|
|
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 <
|
|
1978
|
+
}, 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 @if (!EffectiveEntity) {\n <mj-empty-state\n class=\"empty-state-fill\"\n Icon=\"fa-solid fa-database\"\n Title=\"Select an entity to view records\" />\n }\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 @if (EffectiveEntity && FilteredRecords.length === 0 && !IsLoading && !IsDynamicViewActive) {\n <mj-empty-state\n class=\"empty-state-fill\"\n [Variant]=\"IsNoRecordsFiltered ? 'no-results' : 'empty'\"\n [Title]=\"NoRecordsTitle\" />\n }\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 \u2014 fill the content pane vertically */\n.empty-state-fill {\n height: 100%;\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
1979
|
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }], { Entity: [{
|
|
1737
1980
|
type: Input
|
|
1738
1981
|
}], EntityName: [{
|
|
@@ -1771,6 +2014,8 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1771
2014
|
type: Output
|
|
1772
2015
|
}], CreateRecordRequested: [{
|
|
1773
2016
|
type: Output
|
|
2017
|
+
}], ConfigureRequested: [{
|
|
2018
|
+
type: Output
|
|
1774
2019
|
}], ViewTypeID: [{
|
|
1775
2020
|
type: Input
|
|
1776
2021
|
}], ViewTypeConfigs: [{
|
|
@@ -1789,5 +2034,5 @@ export class EntityViewerComponent extends BaseAngularComponent {
|
|
|
1789
2034
|
type: ViewChild,
|
|
1790
2035
|
args: ['dynamicViewHost', { read: ViewContainerRef }]
|
|
1791
2036
|
}] }); })();
|
|
1792
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber:
|
|
2037
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 134 }); })();
|
|
1793
2038
|
//# sourceMappingURL=entity-viewer.component.js.map
|