@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.
Files changed (75) hide show
  1. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts +5 -0
  2. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts.map +1 -1
  3. package/dist/lib/aggregate-panel/aggregate-panel.component.js +45 -47
  4. package/dist/lib/aggregate-panel/aggregate-panel.component.js.map +1 -1
  5. package/dist/lib/entity-cards/entity-cards.component.js +7 -10
  6. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
  7. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +10 -1
  8. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  9. package/dist/lib/entity-data-grid/entity-data-grid.component.js +35 -34
  10. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  11. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +0 -4
  12. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -1
  13. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +166 -207
  14. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
  15. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +109 -5
  16. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  17. package/dist/lib/entity-viewer/entity-viewer.component.js +287 -42
  18. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  19. package/dist/lib/recycle-bin/recycle-bin.component.js +21 -44
  20. package/dist/lib/recycle-bin/recycle-bin.component.js.map +1 -1
  21. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js +19 -20
  22. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js.map +1 -1
  23. package/dist/lib/view-config-panel/view-config-panel.component.js +268 -301
  24. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  25. package/dist/lib/view-selector/view-selector.component.d.ts +26 -1
  26. package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -1
  27. package/dist/lib/view-selector/view-selector.component.js +118 -76
  28. package/dist/lib/view-selector/view-selector.component.js.map +1 -1
  29. package/dist/lib/view-types/descriptors/grid-view-type.d.ts +7 -0
  30. package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -1
  31. package/dist/lib/view-types/descriptors/grid-view-type.js +7 -0
  32. package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -1
  33. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +18 -1
  34. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -1
  35. package/dist/lib/view-types/renderers/grid-view-renderer.component.js +29 -3
  36. package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -1
  37. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -1
  38. package/dist/lib/view-types/renderers/map-view-renderer.component.js +11 -14
  39. package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -1
  40. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -1
  41. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +12 -14
  42. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -1
  43. package/dist/lib/view-types/view-type.contracts.d.ts +44 -0
  44. package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -1
  45. package/dist/lib/view-types/view-type.contracts.js +6 -0
  46. package/dist/lib/view-types/view-type.contracts.js.map +1 -1
  47. package/dist/lib/view-workspace/view-workspace.component.d.ts +32 -1
  48. package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -1
  49. package/dist/lib/view-workspace/view-workspace.component.js +68 -7
  50. package/dist/lib/view-workspace/view-workspace.component.js.map +1 -1
  51. package/dist/module.d.ts +1 -1
  52. package/dist/module.d.ts.map +1 -1
  53. package/dist/module.js +10 -1
  54. package/dist/module.js.map +1 -1
  55. package/package.json +18 -17
  56. package/dist/__tests__/entity-cards-selection.test.d.ts +0 -2
  57. package/dist/__tests__/entity-cards-selection.test.d.ts.map +0 -1
  58. package/dist/__tests__/entity-cards-selection.test.js +0 -149
  59. package/dist/__tests__/entity-cards-selection.test.js.map +0 -1
  60. package/dist/__tests__/entity-cards-viewmodel.test.d.ts +0 -2
  61. package/dist/__tests__/entity-cards-viewmodel.test.d.ts.map +0 -1
  62. package/dist/__tests__/entity-cards-viewmodel.test.js +0 -208
  63. package/dist/__tests__/entity-cards-viewmodel.test.js.map +0 -1
  64. package/dist/__tests__/highlight.test.d.ts +0 -2
  65. package/dist/__tests__/highlight.test.d.ts.map +0 -1
  66. package/dist/__tests__/highlight.test.js +0 -67
  67. package/dist/__tests__/highlight.test.js.map +0 -1
  68. package/dist/__tests__/index.test.d.ts +0 -2
  69. package/dist/__tests__/index.test.d.ts.map +0 -1
  70. package/dist/__tests__/index.test.js +0 -7
  71. package/dist/__tests__/index.test.js.map +0 -1
  72. package/dist/__tests__/view-types.test.d.ts +0 -2
  73. package/dist/__tests__/view-types.test.d.ts.map +0 -1
  74. package/dist/__tests__/view-types.test.js +0 -102
  75. 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 "../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";
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", 19);
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", 20);
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", 12);
27
- i0.ɵɵelement(1, "i", 16);
28
- i0.ɵɵelementStart(2, "input", 17);
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", 18);
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", 13);
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", 21);
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", 15);
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", 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);
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 EntityViewerComponent_ng_template_16_Template(rf, ctx) { }
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
- * Grid state configuration from a User View
399
- * Controls column visibility, widths, order, and sort settings
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 = null;
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.viewTypeConfigById.get(option.viewTypeId) ?? {});
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: 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) {
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.ɵɵelementStart(7, "div", 8);
1697
- i0.ɵɵelement(8, "i", 9);
1698
- i0.ɵɵelementStart(9, "p");
1699
- i0.ɵɵtext(10, "Select an entity to view records");
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.ɵɵproperty("hidden", !!ctx.EffectiveEntity);
1723
- i0.ɵɵadvance(4);
1724
- i0.ɵɵproperty("hidden", !ctx.EffectiveEntity || ctx.FilteredRecords.length > 0 || ctx.IsLoading || ctx.IsDynamicViewActive);
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.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}"] });
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 <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"] }]
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: 133 }); })();
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