@memberjunction/ng-entity-viewer 5.39.0 → 5.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/__tests__/view-types.test.d.ts +2 -0
  2. package/dist/__tests__/view-types.test.d.ts.map +1 -0
  3. package/dist/__tests__/view-types.test.js +102 -0
  4. package/dist/__tests__/view-types.test.js.map +1 -0
  5. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  6. package/dist/lib/entity-data-grid/entity-data-grid.component.js +8 -0
  7. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  8. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +356 -341
  9. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  10. package/dist/lib/entity-viewer/entity-viewer.component.js +993 -1097
  11. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  12. package/dist/lib/view-config-panel/view-config-panel.component.d.ts +126 -126
  13. package/dist/lib/view-config-panel/view-config-panel.component.js +635 -635
  14. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  15. package/dist/lib/view-selector/view-selector.component.d.ts +226 -0
  16. package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -0
  17. package/dist/lib/view-selector/view-selector.component.js +861 -0
  18. package/dist/lib/view-selector/view-selector.component.js.map +1 -0
  19. package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts +114 -0
  20. package/dist/lib/view-type-switcher/view-type-switcher.component.d.ts.map +1 -0
  21. package/dist/lib/view-type-switcher/view-type-switcher.component.js +209 -0
  22. package/dist/lib/view-type-switcher/view-type-switcher.component.js.map +1 -0
  23. package/dist/lib/view-types/descriptors/cards-view-type.d.ts +18 -0
  24. package/dist/lib/view-types/descriptors/cards-view-type.d.ts.map +1 -0
  25. package/dist/lib/view-types/descriptors/cards-view-type.js +31 -0
  26. package/dist/lib/view-types/descriptors/cards-view-type.js.map +1 -0
  27. package/dist/lib/view-types/descriptors/grid-view-type.d.ts +17 -0
  28. package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -0
  29. package/dist/lib/view-types/descriptors/grid-view-type.js +30 -0
  30. package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -0
  31. package/dist/lib/view-types/descriptors/map-view-type.d.ts +21 -0
  32. package/dist/lib/view-types/descriptors/map-view-type.d.ts.map +1 -0
  33. package/dist/lib/view-types/descriptors/map-view-type.js +35 -0
  34. package/dist/lib/view-types/descriptors/map-view-type.js.map +1 -0
  35. package/dist/lib/view-types/descriptors/timeline-view-type.d.ts +22 -0
  36. package/dist/lib/view-types/descriptors/timeline-view-type.d.ts.map +1 -0
  37. package/dist/lib/view-types/descriptors/timeline-view-type.js +40 -0
  38. package/dist/lib/view-types/descriptors/timeline-view-type.js.map +1 -0
  39. package/dist/lib/view-types/index.d.ts +20 -0
  40. package/dist/lib/view-types/index.d.ts.map +1 -0
  41. package/dist/lib/view-types/index.js +29 -0
  42. package/dist/lib/view-types/index.js.map +1 -0
  43. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts +93 -0
  44. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts.map +1 -0
  45. package/dist/lib/view-types/renderers/cards-view-renderer.component.js +144 -0
  46. package/dist/lib/view-types/renderers/cards-view-renderer.component.js.map +1 -0
  47. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +273 -0
  48. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -0
  49. package/dist/lib/view-types/renderers/grid-view-renderer.component.js +558 -0
  50. package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -0
  51. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts +135 -0
  52. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -0
  53. package/dist/lib/view-types/renderers/map-view-renderer.component.js +216 -0
  54. package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -0
  55. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts +176 -0
  56. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -0
  57. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +535 -0
  58. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -0
  59. package/dist/lib/view-types/view-type.contracts.d.ts +235 -0
  60. package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -0
  61. package/dist/lib/view-types/view-type.contracts.js +51 -0
  62. package/dist/lib/view-types/view-type.contracts.js.map +1 -0
  63. package/dist/lib/view-types/view-type.engine.d.ts +76 -0
  64. package/dist/lib/view-types/view-type.engine.d.ts.map +1 -0
  65. package/dist/lib/view-types/view-type.engine.js +138 -0
  66. package/dist/lib/view-types/view-type.engine.js.map +1 -0
  67. package/dist/lib/view-workspace/view-workspace.component.d.ts +451 -0
  68. package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -0
  69. package/dist/lib/view-workspace/view-workspace.component.js +1212 -0
  70. package/dist/lib/view-workspace/view-workspace.component.js.map +1 -0
  71. package/dist/module.d.ts +20 -11
  72. package/dist/module.d.ts.map +1 -1
  73. package/dist/module.js +50 -8
  74. package/dist/module.js.map +1 -1
  75. package/dist/public-api.d.ts +8 -0
  76. package/dist/public-api.d.ts.map +1 -1
  77. package/dist/public-api.js +14 -0
  78. package/dist/public-api.js.map +1 -1
  79. package/package.json +16 -15
@@ -0,0 +1,1212 @@
1
+ import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
2
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
+ import { LogError } from '@memberjunction/core';
4
+ import { UUIDsEqual } from '@memberjunction/global';
5
+ import { UserInfoEngine } from '@memberjunction/core-entities';
6
+ import { EntityViewerComponent } from '../entity-viewer/entity-viewer.component';
7
+ import { ViewSelectorComponent } from '../view-selector/view-selector.component';
8
+ import { ViewConfigPanelComponent } from '../view-config-panel/view-config-panel.component';
9
+ import * as i0 from "@angular/core";
10
+ import * as i1 from "@memberjunction/ng-ui-components";
11
+ import * as i2 from "@memberjunction/ng-filter-builder";
12
+ import * as i3 from "../entity-viewer/entity-viewer.component";
13
+ import * as i4 from "../view-config-panel/view-config-panel.component";
14
+ import * as i5 from "../quick-save-dialog/quick-save-dialog.component";
15
+ import * as i6 from "../duplicate-view-dialog/duplicate-view-dialog.component";
16
+ import * as i7 from "../shared-view-warning-dialog/shared-view-warning-dialog.component";
17
+ import * as i8 from "../view-selector/view-selector.component";
18
+ import * as i9 from "../view-type-switcher/view-type-switcher.component";
19
+ function ViewWorkspaceComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
20
+ const _r1 = i0.ɵɵgetCurrentView();
21
+ i0.ɵɵelementStart(0, "mj-entity-viewer", 11);
22
+ i0.ɵɵlistener("FilterTextChange", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_FilterTextChange_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onFilterTextChanged($event)); })("RecordSelected", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_RecordSelected_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onRecordSelected($event)); })("RecordOpened", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_RecordOpened_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onRecordOpened($event)); })("DataLoaded", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_DataLoaded_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onDataLoaded($event)); })("FilteredCountChanged", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_FilteredCountChanged_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onFilteredCountChanged($event)); })("OpenRelatedRecordRequested", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_OpenRelatedRecordRequested_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onOpenRelatedRecordRequested($event)); })("CreateRecordRequested", function ViewWorkspaceComponent_Conditional_6_Template_mj_entity_viewer_CreateRecordRequested_0_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onCreateRecordRequested()); });
23
+ i0.ɵɵelementEnd();
24
+ } if (rf & 2) {
25
+ const ctx_r1 = i0.ɵɵnextContext();
26
+ i0.ɵɵproperty("Provider", ctx_r1.Provider)("Entity", ctx_r1.Entity)("ViewEntity", ctx_r1.currentViewEntity)("FilterText", ctx_r1.filterText)("SelectedRecordID", ctx_r1.selectedRecordId)("GridState", ctx_r1.currentGridState)("AutoSaveView", ctx_r1.AutoSaveView)("Config", ctx_r1.innerViewerConfig);
27
+ } }
28
+ function ViewWorkspaceComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
29
+ const _r3 = i0.ɵɵgetCurrentView();
30
+ i0.ɵɵelementStart(0, "div", 12);
31
+ i0.ɵɵlistener("click", function ViewWorkspaceComponent_Conditional_8_Template_div_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onCloseFilterDialog()); });
32
+ i0.ɵɵelementEnd();
33
+ i0.ɵɵelementStart(1, "div", 13)(2, "div", 14)(3, "div", 15);
34
+ i0.ɵɵelement(4, "i", 16);
35
+ i0.ɵɵelementStart(5, "span");
36
+ i0.ɵɵtext(6, "Edit Filters");
37
+ i0.ɵɵelementEnd()();
38
+ i0.ɵɵelementStart(7, "button", 17);
39
+ i0.ɵɵlistener("click", function ViewWorkspaceComponent_Conditional_8_Template_button_click_7_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onCloseFilterDialog()); });
40
+ i0.ɵɵelement(8, "i", 18);
41
+ i0.ɵɵelementEnd()();
42
+ i0.ɵɵelementStart(9, "div", 19)(10, "mj-filter-builder", 20);
43
+ i0.ɵɵlistener("apply", function ViewWorkspaceComponent_Conditional_8_Template_mj_filter_builder_apply_10_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onFilterApplied($event)); })("filterChange", function ViewWorkspaceComponent_Conditional_8_Template_mj_filter_builder_filterChange_10_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.filterDialogState = $event); });
44
+ i0.ɵɵelementEnd()();
45
+ i0.ɵɵelementStart(11, "div", 21)(12, "button", 22);
46
+ i0.ɵɵlistener("click", function ViewWorkspaceComponent_Conditional_8_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.filterDialogState && ctx_r1.onFilterApplied(ctx_r1.filterDialogState)); });
47
+ i0.ɵɵelement(13, "i", 23);
48
+ i0.ɵɵtext(14, " Apply Filters ");
49
+ i0.ɵɵelementEnd();
50
+ i0.ɵɵelementStart(15, "button", 24);
51
+ i0.ɵɵlistener("click", function ViewWorkspaceComponent_Conditional_8_Template_button_click_15_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onCloseFilterDialog()); });
52
+ i0.ɵɵtext(16, " Cancel ");
53
+ i0.ɵɵelementEnd()()();
54
+ } if (rf & 2) {
55
+ const ctx_r1 = i0.ɵɵnextContext();
56
+ i0.ɵɵadvance(10);
57
+ i0.ɵɵproperty("fields", ctx_r1.filterDialogFields)("filter", ctx_r1.filterDialogState)("showSummary", true);
58
+ } }
59
+ /**
60
+ * `mj-view-workspace` — the reusable "browse an entity's data across saved views" workspace.
61
+ *
62
+ * This composite component orchestrates the full saved-view lifecycle (select / save / save-as-new /
63
+ * rename / duplicate / delete / revert / quick-save / save-defaults) on top of an entity's data, by
64
+ * composing the existing Generic lego components in this package:
65
+ *
66
+ * - {@link ViewSelectorComponent} — the saved-view dropdown.
67
+ * - {@link EntityViewerComponent} — the data renderer (grid / cards / timeline / map / plug-ins).
68
+ * - {@link ViewConfigPanelComponent} — the slide-in column/sort/filter configuration panel.
69
+ * - `mj-view-type-switcher` — the toolbar dropdown for switching the active view type.
70
+ * - `mj-quick-save-dialog`, `mj-duplicate-view-dialog`, `mj-shared-view-warning-dialog`,
71
+ * `mj-ev-confirm-dialog` — the focused modals.
72
+ *
73
+ * It is the generic extraction of what `DataExplorerDashboardComponent` does today, **minus routing,
74
+ * URL/query-param sync, and the Explorer state service**. Anything that requires app-level routing is
75
+ * emitted as an event ({@link OpenRecordRequested}, {@link OpenViewInTabRequested},
76
+ * {@link CreateNewRecordRequested}) for the host to handle — this component never imports `Router`.
77
+ *
78
+ * ## Persistence model
79
+ * - When {@link AutoSaveView} is `true`, the workspace persists view CRUD itself via the
80
+ * `MJUserViewEntityExtended` BaseEntity (`.Save()` / `.Delete()`) and `UserInfoEngine` for the
81
+ * per-user default-view settings, firing the cancelable `Before…` events and notification `After…`
82
+ * events around each operation.
83
+ * - When {@link AutoSaveView} is `false` (the default), the workspace performs no persistence — it
84
+ * emits the change-request events ({@link SaveViewRequested}, {@link DeleteViewRequested},
85
+ * {@link DuplicateViewRequested}, etc.) and the host is responsible for persisting and feeding back
86
+ * the updated view via the `[SelectedView]` input.
87
+ *
88
+ * State is held in plain component fields — there is no state-management service.
89
+ *
90
+ * @example
91
+ * ```html
92
+ * <mj-view-workspace
93
+ * [EntityName]="'Accounts'"
94
+ * [AutoSaveView]="true"
95
+ * (OpenRecordRequested)="navService.openRecord($event.entity, $event.record)"
96
+ * (OpenViewInTabRequested)="navService.openView($event)"
97
+ * (CreateNewRecordRequested)="navService.newRecord($event)">
98
+ * </mj-view-workspace>
99
+ * ```
100
+ */
101
+ export class ViewWorkspaceComponent extends BaseAngularComponent {
102
+ cdr;
103
+ // ========================================
104
+ // INPUTS
105
+ // ========================================
106
+ _entity = null;
107
+ _initialized = false;
108
+ /**
109
+ * The entity whose data this workspace browses. May be supplied directly, or resolved from
110
+ * {@link EntityName} / {@link EntityID}. When it changes the selected view and config panel reset.
111
+ */
112
+ get Entity() {
113
+ return this._entity;
114
+ }
115
+ set Entity(value) {
116
+ const previous = this._entity;
117
+ this._entity = value;
118
+ if (this._initialized && value && (!previous || !UUIDsEqual(value.ID, previous.ID))) {
119
+ this.resetForEntityChange();
120
+ }
121
+ }
122
+ /**
123
+ * Convenience input: resolve {@link Entity} by entity **name** via the active provider.
124
+ * Use this OR {@link Entity} OR {@link EntityID}.
125
+ */
126
+ set EntityName(value) {
127
+ if (!value) {
128
+ return;
129
+ }
130
+ const resolved = this.ProviderToUse?.EntityByName(value) ?? null;
131
+ if (resolved) {
132
+ this.Entity = resolved;
133
+ }
134
+ }
135
+ /**
136
+ * Convenience input: resolve {@link Entity} by entity **ID** via the active provider.
137
+ * Use this OR {@link Entity} OR {@link EntityName}.
138
+ */
139
+ set EntityID(value) {
140
+ if (!value) {
141
+ return;
142
+ }
143
+ const resolved = this.ProviderToUse?.Entities.find(e => UUIDsEqual(e.ID, value)) ?? null;
144
+ if (resolved) {
145
+ this.Entity = resolved;
146
+ }
147
+ }
148
+ /**
149
+ * When `true`, the workspace persists view CRUD itself (BaseEntity `.Save()`/`.Delete()` plus
150
+ * `UserInfoEngine` for the default-view settings) and fires the cancelable `Before…` / notification
151
+ * `After…` events around each persistence operation. When `false` (the default) the workspace only
152
+ * emits the change-request events and the host persists. Persistence is provider-based and
153
+ * generic-safe — it never performs routing.
154
+ */
155
+ AutoSaveView = false;
156
+ /**
157
+ * The currently-selected view. Hosts that own persistence (when {@link AutoSaveView} is `false`)
158
+ * feed the loaded view back in here after handling a save/select request. Bound two-way friendly:
159
+ * the workspace emits {@link SelectedViewChange} whenever the selection changes internally.
160
+ */
161
+ get SelectedView() {
162
+ return this.currentViewEntity;
163
+ }
164
+ set SelectedView(value) {
165
+ this.currentViewEntity = value;
166
+ this.currentGridState = value ? this.parseViewGridState(value) : this.loadUserDefaultGridState();
167
+ this.viewModified = false;
168
+ }
169
+ // ----- Host-driven passthrough inputs (forwarded to the inner entity-viewer) -----
170
+ //
171
+ // These let a host (e.g. the Explorer DataExplorer dashboard) own the chrome — filter box, record
172
+ // selection — while the workspace owns the saved-view lifecycle. All view-type-specific chrome
173
+ // (grid toolbar, timeline date/orientation, map render mode) is now self-contained in each plug-in
174
+ // renderer, so the workspace no longer forwards any of it.
175
+ /**
176
+ * The free-text filter to apply to the inner viewer. When a host owns the filter box it binds
177
+ * this (typically a debounced value); the workspace's {@link filterText} field tracks it.
178
+ */
179
+ get FilterText() {
180
+ return this.filterText;
181
+ }
182
+ set FilterText(value) {
183
+ this.filterText = value ?? '';
184
+ }
185
+ /**
186
+ * The composite-key string of the record to highlight in the inner viewer. When a host owns
187
+ * record selection (e.g. via a detail panel) it binds this; the workspace's
188
+ * {@link selectedRecordId} field tracks it.
189
+ */
190
+ get SelectedRecordId() {
191
+ return this.selectedRecordId;
192
+ }
193
+ set SelectedRecordId(value) {
194
+ this.selectedRecordId = value;
195
+ }
196
+ /** Partial {@link EntityViewerConfig} forwarded to the inner viewer (toolbar/pagination/etc.). */
197
+ ViewerConfig = null;
198
+ // ========================================
199
+ // OUTPUTS — routing / host concerns (this component never routes)
200
+ // ========================================
201
+ /**
202
+ * Emitted when the user opens a record (double-click / open). The host performs the navigation.
203
+ */
204
+ OpenRecordRequested = new EventEmitter();
205
+ /**
206
+ * Emitted when the user asks to open the current view in its own tab. Carries the `MJ: User Views`
207
+ * row ID. The host performs the navigation.
208
+ */
209
+ OpenViewInTabRequested = new EventEmitter();
210
+ /**
211
+ * Emitted when the user asks to create a new record for the current entity — either from the
212
+ * selector or bubbled up from a plug-in renderer's "New" button. The host opens the form.
213
+ */
214
+ CreateNewRecordRequested = new EventEmitter();
215
+ /**
216
+ * NAVIGATION request bubbled up from a plug-in renderer (via the inner viewer) to open a *related*
217
+ * record on a (possibly different) entity — e.g. a grid foreign-key drill-through. The host routes.
218
+ */
219
+ OpenRelatedRecordRequested = new EventEmitter();
220
+ /**
221
+ * Emitted when a record is selected (single click) in the viewer.
222
+ */
223
+ RecordSelected = new EventEmitter();
224
+ /**
225
+ * Emitted when the selected view changes (selection, save, delete). Notification only.
226
+ */
227
+ ViewSelected = new EventEmitter();
228
+ /**
229
+ * Two-way-binding companion for {@link SelectedView}.
230
+ */
231
+ SelectedViewChange = new EventEmitter();
232
+ // ----- Forwarded inner-viewer events (the generic, view-type-agnostic ones) -----
233
+ //
234
+ // The workspace owns the inner entity-viewer; it re-emits the viewer's generic signals (filter,
235
+ // data-load, counts) so a host can drive Explorer-only concerns (URL sync, detail panel). All
236
+ // view-type-specific feature events (grid selection/add-to-list, map state, grid state) are now
237
+ // self-contained in the plug-in renderers and no longer bubble through the workspace.
238
+ /**
239
+ * Emitted when the inner viewer's filter text changes (two-way friendly with {@link FilterText}).
240
+ */
241
+ FilterTextChanged = new EventEmitter();
242
+ /**
243
+ * Emitted when the inner viewer finishes loading data — carries counts, timing and the records.
244
+ */
245
+ DataLoaded = new EventEmitter();
246
+ /**
247
+ * Emitted when the inner viewer's filtered/total counts change.
248
+ */
249
+ FilteredCountChanged = new EventEmitter();
250
+ // ----- Persistence lifecycle events -----
251
+ /**
252
+ * Emitted (cancelable) BEFORE a view save is persisted. A handler may set `Cancel = true` to veto.
253
+ * Only fires when {@link AutoSaveView} is `true`.
254
+ */
255
+ BeforeViewSave = new EventEmitter();
256
+ /**
257
+ * Emitted AFTER a view save has been persisted. Notification only.
258
+ */
259
+ AfterViewSave = new EventEmitter();
260
+ /**
261
+ * Emitted (cancelable) BEFORE a view delete is persisted. A handler may set `Cancel = true` to veto.
262
+ * Only fires when {@link AutoSaveView} is `true`.
263
+ */
264
+ BeforeViewDelete = new EventEmitter();
265
+ /**
266
+ * Emitted AFTER a view delete has been persisted. Notification only.
267
+ */
268
+ AfterViewDelete = new EventEmitter();
269
+ // ----- Change-request events (the host persists when AutoSaveView is false) -----
270
+ /**
271
+ * Emitted when the user requests a save and {@link AutoSaveView} is `false`. The host persists.
272
+ */
273
+ SaveViewRequested = new EventEmitter();
274
+ /**
275
+ * Emitted when the user requests a delete and {@link AutoSaveView} is `false`. The host persists.
276
+ */
277
+ DeleteViewRequested = new EventEmitter();
278
+ /**
279
+ * Emitted when the user requests a duplicate and {@link AutoSaveView} is `false`. The host persists.
280
+ * Carries the source view ID and the chosen name for the copy.
281
+ */
282
+ DuplicateViewRequested = new EventEmitter();
283
+ /**
284
+ * Emitted when the user saves per-user default view settings and {@link AutoSaveView} is `false`.
285
+ */
286
+ SaveDefaultsRequested = new EventEmitter();
287
+ // ========================================
288
+ // CHILD COMPONENT REFERENCES
289
+ // ========================================
290
+ /** Reference to the view selector (for reloading the view list after CRUD). */
291
+ viewSelectorRef;
292
+ /** Reference to the entity viewer (for flushing pending grid changes + reloading data). */
293
+ entityViewerRef;
294
+ /** Reference to the config panel (for building summaries + canEdit checks). */
295
+ viewConfigPanelRef;
296
+ // ========================================
297
+ // INTERNAL STATE (plain fields — no state service)
298
+ // ========================================
299
+ /** The currently selected view entity (null = the default/unsaved view). */
300
+ currentViewEntity = null;
301
+ /** The currently selected view ID (null = default view). */
302
+ selectedViewId = null;
303
+ /** Live grid state (column widths / order / sort / aggregates) reflecting user interaction. */
304
+ currentGridState = null;
305
+ /** Whether the current view has unsaved modifications. */
306
+ viewModified = false;
307
+ /** The current free-text filter. */
308
+ filterText = '';
309
+ /** The currently selected record's composite-key string (for grid highlight). */
310
+ selectedRecordId = null;
311
+ /** Records loaded by the viewer, kept for config-panel sample data. */
312
+ loadedRecords = [];
313
+ /** Whether a save operation is in progress (drives the saving spinners). */
314
+ isSavingView = false;
315
+ // ----- Dialog / panel open flags -----
316
+ /** Whether the slide-in config panel is open. */
317
+ isConfigPanelOpen = false;
318
+ /** Whether the quick-save dialog is open. */
319
+ showQuickSaveDialog = false;
320
+ /** Whether the duplicate-view dialog is open. */
321
+ showDuplicateDialog = false;
322
+ /** Whether the shared-view warning dialog is open. */
323
+ showSharedViewWarning = false;
324
+ /** Whether the config panel opens in "save as new" mode. */
325
+ defaultSaveAsNew = false;
326
+ // ----- Pending state carried across dialogs -----
327
+ /** Summary shown in the quick-save dialog. */
328
+ quickSaveSummary = null;
329
+ /** Summary shown in the duplicate-view dialog. */
330
+ duplicateSummary = null;
331
+ /** Source view name shown in the duplicate-view dialog. */
332
+ duplicateSourceViewName = '';
333
+ /** The view ID being duplicated. */
334
+ duplicateTargetViewId = null;
335
+ /** A quick-save event held while the shared-view warning is shown. */
336
+ pendingQuickSaveEvent = null;
337
+ /** Pre-populated name carried from quick-save dialog into the config panel. */
338
+ pendingNewViewName = '';
339
+ /** Pre-populated description carried from quick-save dialog into the config panel. */
340
+ pendingNewViewDescription = '';
341
+ /** Pre-populated sharing preference carried from quick-save dialog into the config panel. */
342
+ pendingNewViewIsShared = false;
343
+ // ----- Filter dialog (rendered at workspace level for full width) -----
344
+ /** Whether the full-width filter dialog is open. */
345
+ isFilterDialogOpen = false;
346
+ /** The filter state currently being edited in the filter dialog. */
347
+ filterDialogState = null;
348
+ /** The filter fields available in the filter dialog. */
349
+ filterDialogFields = [];
350
+ constructor(cdr) {
351
+ super();
352
+ this.cdr = cdr;
353
+ }
354
+ // ========================================
355
+ // LIFECYCLE
356
+ // ========================================
357
+ /** Initializes the workspace and loads the per-user default grid state for the entity. */
358
+ ngOnInit() {
359
+ this._initialized = true;
360
+ if (this._entity && !this.currentViewEntity) {
361
+ this.currentGridState = this.loadUserDefaultGridState();
362
+ }
363
+ }
364
+ /** Resets all view/selection state when the bound entity changes to a different entity. */
365
+ resetForEntityChange() {
366
+ this.currentViewEntity = null;
367
+ this.selectedViewId = null;
368
+ this.viewModified = false;
369
+ this.selectedRecordId = null;
370
+ this.currentGridState = this.loadUserDefaultGridState();
371
+ this.closeAllDialogs();
372
+ this.cdr.detectChanges();
373
+ }
374
+ /** Closes every dialog/panel — used on entity change. */
375
+ closeAllDialogs() {
376
+ this.isConfigPanelOpen = false;
377
+ this.showQuickSaveDialog = false;
378
+ this.showDuplicateDialog = false;
379
+ this.showSharedViewWarning = false;
380
+ this.isFilterDialogOpen = false;
381
+ }
382
+ // ========================================
383
+ // VIEW-TYPE SWITCHER (lifted into the toolbar)
384
+ // ========================================
385
+ /**
386
+ * Drive the inner entity-viewer to switch to the chosen view type. The toolbar-level
387
+ * {@link ViewTypeSwitcherComponent} emits the selection; we route it to the viewer's
388
+ * {@link EntityViewerComponent.SelectViewTypeById} (its existing lifecycle: cancelable events
389
+ * + persistence). The viewer's own header switcher is suppressed via Config, so this is the
390
+ * single switcher in the workspace.
391
+ */
392
+ onToolbarViewTypeSelected(event) {
393
+ this.entityViewerRef?.SelectViewTypeById(event.viewTypeId);
394
+ this.cdr.detectChanges();
395
+ }
396
+ /**
397
+ * The active view type's `MJ: View Types` row ID, read from the inner viewer for the toolbar
398
+ * switcher's active highlight. Safe to read in the template — it reflects the viewer's own
399
+ * resolved state rather than driving it.
400
+ */
401
+ get activeViewTypeId() {
402
+ return this.entityViewerRef?.ActiveViewTypeId ?? null;
403
+ }
404
+ /**
405
+ * The Config forwarded to the inner viewer. Merges the host-supplied {@link ViewerConfig} but
406
+ * always forces `showViewModeToggle: false` so the view-type switcher appears only once — in the
407
+ * workspace toolbar, not duplicated in the viewer's own header.
408
+ */
409
+ get innerViewerConfig() {
410
+ return { ...(this.ViewerConfig ?? {}), showViewModeToggle: false };
411
+ }
412
+ // ========================================
413
+ // VIEW SELECTION
414
+ // ========================================
415
+ /**
416
+ * Handle a view selection from the selector dropdown. Applies the view's grid state and filter,
417
+ * resets the modified flag, and notifies the host. (Generalized from DataExplorer.onViewSelected.)
418
+ */
419
+ onViewSelected(event) {
420
+ this.entityViewerRef?.EnsurePendingChangesSaved();
421
+ this.currentViewEntity = event.View;
422
+ this.selectedViewId = event.ViewID;
423
+ this.viewModified = false;
424
+ this.filterText = '';
425
+ this.currentGridState = event.View
426
+ ? this.parseViewGridState(event.View)
427
+ : this.loadUserDefaultGridState();
428
+ this.ViewSelected.emit(event.View);
429
+ this.SelectedViewChange.emit(event.View);
430
+ this.cdr.detectChanges();
431
+ }
432
+ /** Handle filter text change from the viewer — re-emit so the host can sync its filter box / URL. */
433
+ onFilterTextChanged(filterText) {
434
+ this.filterText = filterText;
435
+ this.FilterTextChanged.emit(filterText);
436
+ }
437
+ /** Track loaded records so the config panel has sample data, and re-emit the full event. */
438
+ onDataLoaded(event) {
439
+ this.loadedRecords = event.records;
440
+ this.DataLoaded.emit(event);
441
+ }
442
+ /** Re-emit the inner viewer's filtered-count change for the host. */
443
+ onFilteredCountChanged(event) {
444
+ this.FilteredCountChanged.emit(event);
445
+ }
446
+ // ========================================
447
+ // RECORD SELECTION / OPENING / NAVIGATION (emit for host)
448
+ // ========================================
449
+ /** Handle a record single-click — track selection and re-emit for the host. */
450
+ onRecordSelected(event) {
451
+ this.selectedRecordId = event.compositeKey.ToConcatenatedString();
452
+ this.RecordSelected.emit(event);
453
+ }
454
+ /** Handle a record open (double-click) — emit for the host to route to the record. */
455
+ onRecordOpened(event) {
456
+ if (event.record) {
457
+ this.OpenRecordRequested.emit({ entity: event.entity, record: event.record });
458
+ }
459
+ }
460
+ /**
461
+ * Handle a plug-in renderer's request (via the inner viewer) to open a related record on a
462
+ * (possibly different) entity — re-emit for the host to route.
463
+ */
464
+ onOpenRelatedRecordRequested(nav) {
465
+ this.OpenRelatedRecordRequested.emit(nav);
466
+ }
467
+ /**
468
+ * Handle a plug-in renderer's request (via the inner viewer) to create a new record of the
469
+ * current entity (e.g. a grid's "New" button) — re-emit for the host to open the form.
470
+ */
471
+ onCreateRecordRequested() {
472
+ if (this._entity) {
473
+ this.CreateNewRecordRequested.emit(this._entity);
474
+ }
475
+ }
476
+ // ========================================
477
+ // VIEW SELECTOR ACTIONS
478
+ // ========================================
479
+ /** Handle the selector's "open in tab" request — emit for the host to route. */
480
+ onOpenInTabRequested(viewId) {
481
+ this.OpenViewInTabRequested.emit(viewId);
482
+ }
483
+ /** Handle the selector's "create new record" request — emit for the host. */
484
+ onCreateNewRecordRequested() {
485
+ if (this._entity) {
486
+ this.CreateNewRecordRequested.emit(this._entity);
487
+ }
488
+ }
489
+ /** Handle the selector's "configure view" request — open the config panel. */
490
+ onConfigureViewRequested() {
491
+ this.defaultSaveAsNew = false;
492
+ this.isConfigPanelOpen = true;
493
+ this.cdr.detectChanges();
494
+ }
495
+ /** Handle the selector's "save view" request — open the config panel in the requested mode. */
496
+ onSaveViewRequested(event) {
497
+ this.defaultSaveAsNew = event.SaveAsNew || false;
498
+ this.isConfigPanelOpen = true;
499
+ this.cdr.detectChanges();
500
+ }
501
+ // ========================================
502
+ // CONFIG PANEL
503
+ // ========================================
504
+ /** Close the config panel and clear any pending new-view carry-over state. */
505
+ onCloseConfigPanel() {
506
+ this.isConfigPanelOpen = false;
507
+ this.clearPendingNewViewState();
508
+ this.cdr.detectChanges();
509
+ }
510
+ /** Reset the carry-over state used when continuing a new-view flow into the config panel. */
511
+ clearPendingNewViewState() {
512
+ this.pendingNewViewName = '';
513
+ this.pendingNewViewDescription = '';
514
+ this.pendingNewViewIsShared = false;
515
+ this.defaultSaveAsNew = false;
516
+ }
517
+ // ----- Filter dialog -----
518
+ /** Open the full-width filter dialog from the config panel's request. */
519
+ onOpenFilterDialogRequest(event) {
520
+ this.filterDialogState = event.filterState;
521
+ this.filterDialogFields = event.filterFields;
522
+ this.isFilterDialogOpen = true;
523
+ this.cdr.detectChanges();
524
+ }
525
+ /** Close the filter dialog. */
526
+ onCloseFilterDialog() {
527
+ this.isFilterDialogOpen = false;
528
+ this.cdr.detectChanges();
529
+ }
530
+ /** Apply a filter from the dialog — the config panel picks it up via `externalFilterState`. */
531
+ onFilterApplied(filter) {
532
+ this.filterDialogState = filter;
533
+ this.isFilterDialogOpen = false;
534
+ this.cdr.detectChanges();
535
+ }
536
+ // ========================================
537
+ // SAVE VIEW
538
+ // ========================================
539
+ /**
540
+ * Handle a save from the config panel. When {@link AutoSaveView}, persists the view itself
541
+ * (create-new or update) via the BaseEntity; otherwise emits {@link SaveViewRequested} for the host.
542
+ * (Faithful generalization of DataExplorer.onSaveView, minus routing/state-service/notifications.)
543
+ */
544
+ async onSaveView(event) {
545
+ if (!this._entity) {
546
+ return;
547
+ }
548
+ if (!this.AutoSaveView) {
549
+ this.SaveViewRequested.emit(event);
550
+ this.isConfigPanelOpen = false;
551
+ this.clearPendingNewViewState();
552
+ this.cdr.detectChanges();
553
+ return;
554
+ }
555
+ this.isSavingView = true;
556
+ this.cdr.detectChanges();
557
+ const isNew = event.SaveAsNew || !this.currentViewEntity;
558
+ const success = isNew
559
+ ? await this.persistNewView(event)
560
+ : await this.persistExistingView(event);
561
+ if (success) {
562
+ this.isConfigPanelOpen = false;
563
+ this.clearPendingNewViewState();
564
+ await this.viewSelectorRef?.LoadViews();
565
+ await this.entityViewerRef?.LoadData();
566
+ }
567
+ this.isSavingView = false;
568
+ this.cdr.detectChanges();
569
+ }
570
+ /**
571
+ * Create and persist a brand-new view from the save event. Fires the cancelable
572
+ * {@link BeforeViewSave} before saving and {@link AfterViewSave} after success.
573
+ */
574
+ async persistNewView(event) {
575
+ const provider = this.ProviderToUse;
576
+ const newView = await provider.GetEntityObject('MJ: User Views', provider.CurrentUser);
577
+ newView.Name = event.Name || 'Custom';
578
+ newView.Description = event.Description;
579
+ newView.EntityID = this._entity.ID;
580
+ newView.UserID = provider.CurrentUser.ID;
581
+ newView.IsShared = event.IsShared;
582
+ newView.IsDefault = false;
583
+ const gridState = this.buildGridState(event);
584
+ if (gridState) {
585
+ newView.GridStateObject = gridState;
586
+ }
587
+ const sortState = this.buildSortState(event);
588
+ if (sortState) {
589
+ newView.SortStateObject = sortState;
590
+ }
591
+ newView.SmartFilterEnabled = event.SmartFilterEnabled;
592
+ newView.SmartFilterPrompt = event.SmartFilterPrompt;
593
+ newView.FilterState = this.buildFilterStateJson(event);
594
+ const before = { Data: { View: newView, IsNew: true }, Cancel: false };
595
+ this.BeforeViewSave.emit(before);
596
+ if (before.Cancel) {
597
+ return false;
598
+ }
599
+ const saved = await newView.Save();
600
+ if (!saved) {
601
+ LogError(`[ViewWorkspace] Failed to create view: ${newView.LatestResult?.CompleteMessage ?? 'unknown error'}`);
602
+ return false;
603
+ }
604
+ this.currentViewEntity = newView;
605
+ this.selectedViewId = newView.ID;
606
+ this.viewModified = false;
607
+ this.currentGridState = this.parseViewGridState(newView);
608
+ this.ViewSelected.emit(newView);
609
+ this.SelectedViewChange.emit(newView);
610
+ this.AfterViewSave.emit({ View: newView, IsNew: true });
611
+ return true;
612
+ }
613
+ /**
614
+ * Update and persist the currently-selected view from the save event. Fires the cancelable
615
+ * {@link BeforeViewSave} before saving and {@link AfterViewSave} after success.
616
+ */
617
+ async persistExistingView(event) {
618
+ const view = this.currentViewEntity;
619
+ view.Name = event.Name;
620
+ view.Description = event.Description;
621
+ view.IsShared = event.IsShared;
622
+ const gridState = this.buildGridState(event);
623
+ if (gridState) {
624
+ view.GridStateObject = gridState;
625
+ }
626
+ const sortState = this.buildSortState(event);
627
+ if (sortState) {
628
+ view.SortStateObject = sortState;
629
+ }
630
+ view.SmartFilterEnabled = event.SmartFilterEnabled;
631
+ view.SmartFilterPrompt = event.SmartFilterPrompt;
632
+ view.FilterState = this.buildFilterStateJson(event);
633
+ const before = { Data: { View: view, IsNew: false }, Cancel: false };
634
+ this.BeforeViewSave.emit(before);
635
+ if (before.Cancel) {
636
+ return false;
637
+ }
638
+ const saved = await view.Save();
639
+ if (!saved) {
640
+ LogError(`[ViewWorkspace] Failed to update view: ${view.LatestResult?.CompleteMessage ?? 'unknown error'}`);
641
+ return false;
642
+ }
643
+ this.viewModified = false;
644
+ this.currentGridState = this.parseViewGridState(view);
645
+ this.AfterViewSave.emit({ View: view, IsNew: false });
646
+ return true;
647
+ }
648
+ /**
649
+ * Handle saving per-user default view settings from the config panel. When {@link AutoSaveView},
650
+ * persists to `UserInfoEngine`; otherwise emits {@link SaveDefaultsRequested}.
651
+ * (Generalized from DataExplorer.onSaveDefaultViewSettings.)
652
+ */
653
+ async onSaveDefaultViewSettings(event) {
654
+ if (!this._entity) {
655
+ return;
656
+ }
657
+ if (!this.AutoSaveView) {
658
+ this.SaveDefaultsRequested.emit(event);
659
+ this.isConfigPanelOpen = false;
660
+ this.cdr.detectChanges();
661
+ return;
662
+ }
663
+ this.isSavingView = true;
664
+ this.cdr.detectChanges();
665
+ const gridState = this.buildGridState(event);
666
+ if (gridState) {
667
+ gridState.sortSettings = this.buildGridSortSettings(event) ?? gridState.sortSettings;
668
+ const settingKey = `default-view-setting/${this._entity.Name}`;
669
+ const saved = await UserInfoEngine.Instance.SetSetting(settingKey, JSON.stringify(gridState));
670
+ if (saved) {
671
+ this.currentGridState = {
672
+ columnSettings: gridState.columnSettings,
673
+ sortSettings: gridState.sortSettings,
674
+ aggregates: gridState.aggregates
675
+ };
676
+ this.cdr.detectChanges();
677
+ this.entityViewerRef?.Refresh();
678
+ }
679
+ else {
680
+ LogError('[ViewWorkspace] Failed to save default view settings');
681
+ }
682
+ }
683
+ this.isConfigPanelOpen = false;
684
+ this.isSavingView = false;
685
+ this.cdr.detectChanges();
686
+ }
687
+ // ========================================
688
+ // DELETE VIEW
689
+ // ========================================
690
+ /**
691
+ * Handle a delete from the config panel. When {@link AutoSaveView}, persists the delete itself
692
+ * (firing cancelable {@link BeforeViewDelete} / notification {@link AfterViewDelete}); otherwise
693
+ * emits {@link DeleteViewRequested}. (Generalized from DataExplorer.onDeleteView.)
694
+ */
695
+ async onDeleteView() {
696
+ if (!this.currentViewEntity) {
697
+ return;
698
+ }
699
+ if (!this.AutoSaveView) {
700
+ this.DeleteViewRequested.emit(this.currentViewEntity);
701
+ this.isConfigPanelOpen = false;
702
+ this.cdr.detectChanges();
703
+ return;
704
+ }
705
+ const view = this.currentViewEntity;
706
+ const viewId = view.ID;
707
+ const viewName = view.Name;
708
+ const before = { Data: view, Cancel: false };
709
+ this.BeforeViewDelete.emit(before);
710
+ if (before.Cancel) {
711
+ return;
712
+ }
713
+ const deleted = await view.Delete();
714
+ if (!deleted) {
715
+ LogError(`[ViewWorkspace] Failed to delete view: ${view.LatestResult?.CompleteMessage ?? 'unknown error'}`);
716
+ return;
717
+ }
718
+ this.currentViewEntity = null;
719
+ this.selectedViewId = null;
720
+ this.viewModified = false;
721
+ this.isConfigPanelOpen = false;
722
+ this.currentGridState = this.loadUserDefaultGridState();
723
+ await this.viewSelectorRef?.LoadViews();
724
+ this.ViewSelected.emit(null);
725
+ this.SelectedViewChange.emit(null);
726
+ this.AfterViewDelete.emit({ ViewID: viewId, ViewName: viewName });
727
+ this.cdr.detectChanges();
728
+ }
729
+ // ========================================
730
+ // QUICK SAVE
731
+ // ========================================
732
+ /**
733
+ * Handle a quick-save request from the selector (F-001). Builds a summary from the config panel
734
+ * and opens the quick-save dialog. (Generalized from DataExplorer.onQuickSaveRequested.)
735
+ */
736
+ onQuickSaveRequested(saveAsNew) {
737
+ this.defaultSaveAsNew = saveAsNew;
738
+ this.quickSaveSummary = this.viewConfigPanelRef?.BuildSummary() ?? null;
739
+ this.showQuickSaveDialog = true;
740
+ this.cdr.detectChanges();
741
+ }
742
+ /**
743
+ * Handle the quick-save dialog's save. Intercepts updates to a shared view with the shared-view
744
+ * warning; otherwise executes the save. (Generalized from DataExplorer.onQuickSave.)
745
+ */
746
+ async onQuickSave(event) {
747
+ this.showQuickSaveDialog = false;
748
+ if (!event.SaveAsNew && this.currentViewEntity?.IsShared) {
749
+ this.pendingQuickSaveEvent = event;
750
+ this.showSharedViewWarning = true;
751
+ this.cdr.detectChanges();
752
+ return;
753
+ }
754
+ await this.executeQuickSave(event);
755
+ }
756
+ /** Build a `ViewSaveEvent` from a `QuickSaveEvent` and delegate to {@link onSaveView}. */
757
+ async executeQuickSave(event) {
758
+ const viewSaveEvent = {
759
+ Name: event.Name,
760
+ Description: event.Description,
761
+ IsShared: event.IsShared,
762
+ SaveAsNew: event.SaveAsNew,
763
+ Columns: [],
764
+ SortField: null,
765
+ SortDirection: 'asc',
766
+ SortItems: [],
767
+ SmartFilterEnabled: false,
768
+ SmartFilterPrompt: '',
769
+ FilterState: this.filterDialogState ?? null,
770
+ AggregatesConfig: null
771
+ };
772
+ await this.onSaveView(viewSaveEvent);
773
+ }
774
+ /** Handle the shared-view warning action (update / save-as-copy / cancel). */
775
+ async onSharedViewAction(action) {
776
+ this.showSharedViewWarning = false;
777
+ const event = this.pendingQuickSaveEvent;
778
+ this.pendingQuickSaveEvent = null;
779
+ if (!event) {
780
+ return;
781
+ }
782
+ if (action === 'update-shared') {
783
+ await this.executeQuickSave(event);
784
+ }
785
+ else if (action === 'save-as-copy') {
786
+ await this.executeQuickSave({ ...event, SaveAsNew: true, IsShared: false });
787
+ }
788
+ this.cdr.detectChanges();
789
+ }
790
+ /** Handle the shared-view warning cancel. */
791
+ onSharedViewWarningCancel() {
792
+ this.showSharedViewWarning = false;
793
+ this.pendingQuickSaveEvent = null;
794
+ this.cdr.detectChanges();
795
+ }
796
+ /** Handle the quick-save dialog close. */
797
+ onQuickSaveClose() {
798
+ this.showQuickSaveDialog = false;
799
+ this.cdr.detectChanges();
800
+ }
801
+ /** Handle "open advanced" from the quick-save dialog — carry data into the config panel. */
802
+ onQuickSaveOpenAdvanced(event) {
803
+ this.pendingNewViewName = event.Name;
804
+ this.pendingNewViewDescription = event.Description;
805
+ this.pendingNewViewIsShared = event.IsShared;
806
+ this.defaultSaveAsNew = true;
807
+ this.showQuickSaveDialog = false;
808
+ this.isConfigPanelOpen = true;
809
+ this.cdr.detectChanges();
810
+ }
811
+ // ========================================
812
+ // DUPLICATE VIEW
813
+ // ========================================
814
+ /**
815
+ * Handle a duplicate request (F-005). Opens the duplicate dialog so the user names the copy.
816
+ * (Generalized from DataExplorer.onDuplicateView.)
817
+ */
818
+ onDuplicateViewRequested(viewId) {
819
+ const targetId = viewId || this.currentViewEntity?.ID;
820
+ if (!targetId || !this._entity) {
821
+ return;
822
+ }
823
+ const allViews = [...(this.viewSelectorRef?.MyViews ?? []), ...(this.viewSelectorRef?.SharedViews ?? [])];
824
+ const viewItem = allViews.find(v => v.id === targetId);
825
+ this.duplicateTargetViewId = targetId;
826
+ this.duplicateSourceViewName = viewItem?.name || this.currentViewEntity?.Name || 'View';
827
+ this.duplicateSummary = this.buildDuplicateSummary(viewItem?.entity ?? this.currentViewEntity);
828
+ this.showDuplicateDialog = true;
829
+ this.cdr.detectChanges();
830
+ }
831
+ /** Build a {@link ViewConfigSummary} from a view entity for the duplicate dialog. */
832
+ buildDuplicateSummary(view) {
833
+ if (!view) {
834
+ return null;
835
+ }
836
+ let columnCount = 0;
837
+ let filterCount = 0;
838
+ let sortCount = 0;
839
+ try {
840
+ const gridState = view.GridStateObject;
841
+ if (gridState?.columnSettings && Array.isArray(gridState.columnSettings)) {
842
+ columnCount = gridState.columnSettings.filter((c) => !c.hidden).length;
843
+ }
844
+ }
845
+ catch {
846
+ /* ignore parse errors */
847
+ }
848
+ try {
849
+ const filterState = view.FilterStateObject;
850
+ if (filterState?.filters?.length) {
851
+ filterCount = filterState.filters.length;
852
+ }
853
+ }
854
+ catch {
855
+ /* ignore parse errors */
856
+ }
857
+ try {
858
+ const sortState = view.SortStateObject;
859
+ if (Array.isArray(sortState)) {
860
+ sortCount = sortState.length;
861
+ }
862
+ }
863
+ catch {
864
+ /* ignore parse errors */
865
+ }
866
+ return {
867
+ ColumnCount: columnCount,
868
+ FilterCount: filterCount,
869
+ SortCount: sortCount,
870
+ SmartFilterActive: view.SmartFilterEnabled || false,
871
+ SmartFilterPrompt: view.SmartFilterPrompt || '',
872
+ AggregateCount: 0
873
+ };
874
+ }
875
+ /**
876
+ * Handle the duplicate dialog confirmation. When {@link AutoSaveView}, loads the source view,
877
+ * copies its config into a new personal view, and persists it; otherwise emits
878
+ * {@link DuplicateViewRequested}. (Generalized from DataExplorer.onDuplicateConfirmed.)
879
+ */
880
+ async onDuplicateConfirmed(event) {
881
+ this.showDuplicateDialog = false;
882
+ const targetId = this.duplicateTargetViewId;
883
+ this.duplicateTargetViewId = null;
884
+ if (!targetId || !this._entity) {
885
+ return;
886
+ }
887
+ if (!this.AutoSaveView) {
888
+ this.DuplicateViewRequested.emit({ sourceViewId: targetId, newName: event.Name });
889
+ return;
890
+ }
891
+ await this.persistDuplicate(targetId, event.Name);
892
+ }
893
+ /** Load the source view, copy its config into a new personal view, and persist it. */
894
+ async persistDuplicate(sourceViewId, newName) {
895
+ const provider = this.ProviderToUse;
896
+ const sourceView = await provider.GetEntityObject('MJ: User Views', provider.CurrentUser);
897
+ const loaded = await sourceView.Load(sourceViewId);
898
+ if (!loaded) {
899
+ LogError(`[ViewWorkspace] Could not load view to duplicate: ${sourceView.LatestResult?.CompleteMessage ?? 'not found'}`);
900
+ return;
901
+ }
902
+ const newView = await provider.GetEntityObject('MJ: User Views', provider.CurrentUser);
903
+ newView.Name = newName;
904
+ newView.Description = sourceView.Description || '';
905
+ newView.EntityID = sourceView.EntityID;
906
+ newView.UserID = provider.CurrentUser.ID;
907
+ newView.IsShared = false;
908
+ newView.IsDefault = false;
909
+ newView.GridState = sourceView.GridState;
910
+ newView.FilterState = sourceView.FilterState;
911
+ newView.SortState = sourceView.SortState;
912
+ newView.SmartFilterEnabled = sourceView.SmartFilterEnabled || false;
913
+ newView.SmartFilterPrompt = sourceView.SmartFilterPrompt || '';
914
+ const saved = await newView.Save();
915
+ if (!saved) {
916
+ LogError(`[ViewWorkspace] Failed to duplicate view: ${newView.LatestResult?.CompleteMessage ?? 'unknown error'}`);
917
+ return;
918
+ }
919
+ await this.viewSelectorRef?.LoadViews();
920
+ this.AfterViewSave.emit({ View: newView, IsNew: true });
921
+ }
922
+ /** Handle the duplicate dialog cancel. */
923
+ onDuplicateCancel() {
924
+ this.showDuplicateDialog = false;
925
+ this.duplicateTargetViewId = null;
926
+ this.cdr.detectChanges();
927
+ }
928
+ /** Handle duplicate triggered from the config panel — duplicate the selected view. */
929
+ onDuplicateFromPanel() {
930
+ if (this.currentViewEntity?.ID) {
931
+ this.isConfigPanelOpen = false;
932
+ this.onDuplicateViewRequested(this.currentViewEntity.ID);
933
+ }
934
+ }
935
+ // ========================================
936
+ // REVERT
937
+ // ========================================
938
+ /**
939
+ * Handle a revert request (F-007). Re-parses the saved view's grid state, clears the modified
940
+ * flag, and reloads. (Generalized from DataExplorer.onRevertView.)
941
+ */
942
+ async onRevertView() {
943
+ if (!this.currentViewEntity) {
944
+ return;
945
+ }
946
+ const gridState = this.parseViewGridState(this.currentViewEntity);
947
+ if (gridState) {
948
+ this.currentGridState = gridState;
949
+ }
950
+ this.viewModified = false;
951
+ await this.entityViewerRef?.LoadData();
952
+ this.cdr.detectChanges();
953
+ }
954
+ // ========================================
955
+ // GRID/SORT/FILTER STATE BUILDERS
956
+ // (Generalized from DataExplorer — no state-service / selectedEntity coupling.)
957
+ // ========================================
958
+ /**
959
+ * Build a `GridState` (Kendo-compatible) from the save event. Priority: explicit columns from the
960
+ * config panel → live grid state → entity `DefaultInView` fields. Returns null when no columns.
961
+ */
962
+ buildGridState(event) {
963
+ let columnSettings;
964
+ if (event.Columns.length > 0) {
965
+ columnSettings = event.Columns.map((col, idx) => ({
966
+ ID: col.fieldId,
967
+ Name: col.fieldName,
968
+ DisplayName: col.displayName,
969
+ userDisplayName: col.userDisplayName,
970
+ hidden: false,
971
+ width: col.width || undefined,
972
+ orderIndex: idx,
973
+ format: col.format
974
+ }));
975
+ }
976
+ else if (this.currentGridState?.columnSettings && this.currentGridState.columnSettings.length > 0) {
977
+ columnSettings = this.currentGridState.columnSettings;
978
+ }
979
+ else if (this._entity) {
980
+ columnSettings = this._entity.Fields
981
+ .filter(f => f.DefaultInView)
982
+ .map((f, idx) => ({
983
+ ID: f.ID,
984
+ Name: f.Name,
985
+ DisplayName: f.DisplayNameOrName,
986
+ hidden: false,
987
+ width: f.DefaultColumnWidth || undefined,
988
+ orderIndex: idx
989
+ }));
990
+ if (columnSettings.length === 0) {
991
+ return null;
992
+ }
993
+ }
994
+ else {
995
+ return null;
996
+ }
997
+ const sortSettings = this.buildGridSortSettings(event);
998
+ let aggregates = event.AggregatesConfig ?? undefined;
999
+ if (!aggregates && this.currentGridState?.aggregates) {
1000
+ aggregates = this.currentGridState.aggregates;
1001
+ }
1002
+ return { columnSettings, sortSettings, aggregates };
1003
+ }
1004
+ /** Build grid sort settings (`{field, dir}[]`) from the save event, falling back to live state. */
1005
+ buildGridSortSettings(event) {
1006
+ if (event.SortItems && event.SortItems.length > 0) {
1007
+ return event.SortItems.map(item => ({ field: item.field, dir: item.direction }));
1008
+ }
1009
+ if (event.SortField) {
1010
+ return [{ field: event.SortField, dir: event.SortDirection }];
1011
+ }
1012
+ if (this.currentGridState?.sortSettings && this.currentGridState.sortSettings.length > 0) {
1013
+ return this.currentGridState.sortSettings;
1014
+ }
1015
+ return undefined;
1016
+ }
1017
+ /** Build a `SortState` (`{field, direction}[]`) from the save event. Returns null when no sort. */
1018
+ buildSortState(event) {
1019
+ if (event.SortItems && event.SortItems.length > 0) {
1020
+ return event.SortItems.map(item => ({ field: item.field, direction: item.direction }));
1021
+ }
1022
+ if (event.SortField) {
1023
+ return [{ field: event.SortField, direction: event.SortDirection }];
1024
+ }
1025
+ return null;
1026
+ }
1027
+ /** Serialize the filter state to JSON, defaulting to an empty Kendo filter. */
1028
+ buildFilterStateJson(event) {
1029
+ return event.FilterState
1030
+ ? JSON.stringify(event.FilterState)
1031
+ : JSON.stringify({ logic: 'and', filters: [] });
1032
+ }
1033
+ // ========================================
1034
+ // GRID STATE PARSE / DEFAULTS
1035
+ // ========================================
1036
+ /**
1037
+ * Parse a view's `GridState` into a {@link ViewGridState}, validating columns/sorts against the
1038
+ * current entity to avoid stale fields from a previous entity. Returns null when no valid columns.
1039
+ */
1040
+ parseViewGridState(view) {
1041
+ if (!view.GridState) {
1042
+ return null;
1043
+ }
1044
+ try {
1045
+ const parsed = view.GridStateObject;
1046
+ if (parsed && Array.isArray(parsed.columnSettings)) {
1047
+ const validColumns = this._entity
1048
+ ? parsed.columnSettings.filter((col) => this._entity.Fields.some(f => f.Name === col.Name))
1049
+ : parsed.columnSettings;
1050
+ const validSorts = this._entity
1051
+ ? (parsed.sortSettings || []).filter((s) => this._entity.Fields.some(f => f.Name === s.field))
1052
+ : parsed.sortSettings || [];
1053
+ if (validColumns.length > 0) {
1054
+ return {
1055
+ columnSettings: validColumns,
1056
+ sortSettings: validSorts,
1057
+ aggregates: parsed.aggregates || undefined
1058
+ };
1059
+ }
1060
+ }
1061
+ return null;
1062
+ }
1063
+ catch (error) {
1064
+ LogError(`[ViewWorkspace] Failed to parse GridState: ${error instanceof Error ? error.message : String(error)}`);
1065
+ return null;
1066
+ }
1067
+ }
1068
+ /**
1069
+ * Load the per-user default grid state for the current entity from `UserInfoEngine`, validating
1070
+ * columns/sorts against the entity. Returns null when none saved.
1071
+ */
1072
+ loadUserDefaultGridState() {
1073
+ if (!this._entity) {
1074
+ return null;
1075
+ }
1076
+ try {
1077
+ const settingKey = `default-view-setting/${this._entity.Name}`;
1078
+ const savedState = UserInfoEngine.Instance.GetSetting(settingKey);
1079
+ if (savedState) {
1080
+ const gridState = JSON.parse(savedState);
1081
+ if (gridState && Array.isArray(gridState.columnSettings)) {
1082
+ const validColumns = gridState.columnSettings.filter((col) => this._entity.Fields.some(f => f.Name === col.Name));
1083
+ const validSorts = (gridState.sortSettings || []).filter((s) => this._entity.Fields.some(f => f.Name === s.field));
1084
+ if (validColumns.length > 0) {
1085
+ return { columnSettings: validColumns, sortSettings: validSorts };
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ catch (error) {
1091
+ LogError(`[ViewWorkspace] Failed to load user default grid state: ${error instanceof Error ? error.message : String(error)}`);
1092
+ }
1093
+ return null;
1094
+ }
1095
+ static ɵfac = function ViewWorkspaceComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ViewWorkspaceComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1096
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ViewWorkspaceComponent, selectors: [["mj-view-workspace"]], viewQuery: function ViewWorkspaceComponent_Query(rf, ctx) { if (rf & 1) {
1097
+ i0.ɵɵviewQuery(ViewSelectorComponent, 5)(EntityViewerComponent, 5)(ViewConfigPanelComponent, 5);
1098
+ } if (rf & 2) {
1099
+ let _t;
1100
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.viewSelectorRef = _t.first);
1101
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.entityViewerRef = _t.first);
1102
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.viewConfigPanelRef = _t.first);
1103
+ } }, inputs: { Entity: "Entity", EntityName: "EntityName", EntityID: "EntityID", AutoSaveView: "AutoSaveView", SelectedView: "SelectedView", FilterText: "FilterText", SelectedRecordId: "SelectedRecordId", ViewerConfig: "ViewerConfig" }, outputs: { OpenRecordRequested: "OpenRecordRequested", OpenViewInTabRequested: "OpenViewInTabRequested", CreateNewRecordRequested: "CreateNewRecordRequested", OpenRelatedRecordRequested: "OpenRelatedRecordRequested", RecordSelected: "RecordSelected", ViewSelected: "ViewSelected", SelectedViewChange: "SelectedViewChange", FilterTextChanged: "FilterTextChanged", DataLoaded: "DataLoaded", FilteredCountChanged: "FilteredCountChanged", BeforeViewSave: "BeforeViewSave", AfterViewSave: "AfterViewSave", BeforeViewDelete: "BeforeViewDelete", AfterViewDelete: "AfterViewDelete", SaveViewRequested: "SaveViewRequested", DeleteViewRequested: "DeleteViewRequested", DuplicateViewRequested: "DuplicateViewRequested", SaveDefaultsRequested: "SaveDefaultsRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 12, vars: 32, consts: [[1, "view-workspace"], [1, "workspace-toolbar"], [3, "ViewSelected", "SaveViewRequested", "OpenInTabRequested", "ConfigureViewRequested", "CreateNewRecordRequested", "DuplicateViewRequested", "QuickSaveRequested", "RevertRequested", "Provider", "Entity", "SelectedViewID", "ViewModified"], [1, "workspace-toolbar-spacer"], [3, "ViewTypeSelected", "Provider", "Entity", "ActiveViewTypeID"], [1, "workspace-body"], [3, "Provider", "Entity", "ViewEntity", "FilterText", "SelectedRecordID", "GridState", "AutoSaveView", "Config"], [3, "Close", "Save", "SaveDefaults", "Delete", "Duplicate", "OpenFilterDialogRequest", "Provider", "Entity", "ViewEntity", "IsOpen", "CurrentGridState", "SampleData", "ExternalFilterState", "IsSaving", "DefaultSaveAsNew", "PendingNewViewName", "PendingNewViewDescription", "PendingNewViewIsShared"], [3, "Save", "Close", "OpenAdvanced", "IsOpen", "ViewEntity", "EntityName", "Summary", "IsSaving", "DefaultSaveAsNew"], [3, "Duplicate", "Cancel", "IsOpen", "SourceViewName", "Summary"], [3, "Action", "Cancel", "IsOpen", "ViewName"], [3, "FilterTextChange", "RecordSelected", "RecordOpened", "DataLoaded", "FilteredCountChanged", "OpenRelatedRecordRequested", "CreateRecordRequested", "Provider", "Entity", "ViewEntity", "FilterText", "SelectedRecordID", "GridState", "AutoSaveView", "Config"], [1, "filter-dialog-backdrop", 3, "click"], ["role", "dialog", "aria-modal", "true", 1, "filter-dialog"], [1, "filter-dialog-header"], [1, "filter-dialog-title"], [1, "fa-solid", "fa-filter"], ["type", "button", "title", "Close", 1, "filter-dialog-close", 3, "click"], [1, "fa-solid", "fa-times"], [1, "filter-dialog-content"], [3, "apply", "filterChange", "fields", "filter", "showSummary"], [1, "filter-dialog-footer"], ["mjButton", "", "variant", "primary", "size", "sm", "type", "button", 3, "click"], [1, "fa-solid", "fa-check"], ["mjButton", "", "variant", "flat", "size", "sm", "type", "button", 3, "click"]], template: function ViewWorkspaceComponent_Template(rf, ctx) { if (rf & 1) {
1104
+ i0.ɵɵelementStart(0, "div", 0)(1, "div", 1)(2, "mj-view-selector", 2);
1105
+ i0.ɵɵlistener("ViewSelected", function ViewWorkspaceComponent_Template_mj_view_selector_ViewSelected_2_listener($event) { return ctx.onViewSelected($event); })("SaveViewRequested", function ViewWorkspaceComponent_Template_mj_view_selector_SaveViewRequested_2_listener($event) { return ctx.onSaveViewRequested($event); })("OpenInTabRequested", function ViewWorkspaceComponent_Template_mj_view_selector_OpenInTabRequested_2_listener($event) { return ctx.onOpenInTabRequested($event); })("ConfigureViewRequested", function ViewWorkspaceComponent_Template_mj_view_selector_ConfigureViewRequested_2_listener() { return ctx.onConfigureViewRequested(); })("CreateNewRecordRequested", function ViewWorkspaceComponent_Template_mj_view_selector_CreateNewRecordRequested_2_listener() { return ctx.onCreateNewRecordRequested(); })("DuplicateViewRequested", function ViewWorkspaceComponent_Template_mj_view_selector_DuplicateViewRequested_2_listener($event) { return ctx.onDuplicateViewRequested($event); })("QuickSaveRequested", function ViewWorkspaceComponent_Template_mj_view_selector_QuickSaveRequested_2_listener($event) { return ctx.onQuickSaveRequested($event); })("RevertRequested", function ViewWorkspaceComponent_Template_mj_view_selector_RevertRequested_2_listener() { return ctx.onRevertView(); });
1106
+ i0.ɵɵelementEnd();
1107
+ i0.ɵɵelement(3, "span", 3);
1108
+ i0.ɵɵelementStart(4, "mj-view-type-switcher", 4);
1109
+ i0.ɵɵlistener("ViewTypeSelected", function ViewWorkspaceComponent_Template_mj_view_type_switcher_ViewTypeSelected_4_listener($event) { return ctx.onToolbarViewTypeSelected($event); });
1110
+ i0.ɵɵelementEnd()();
1111
+ i0.ɵɵelementStart(5, "div", 5);
1112
+ i0.ɵɵconditionalCreate(6, ViewWorkspaceComponent_Conditional_6_Template, 1, 8, "mj-entity-viewer", 6);
1113
+ i0.ɵɵelementEnd();
1114
+ i0.ɵɵelementStart(7, "mj-view-config-panel", 7);
1115
+ i0.ɵɵlistener("Close", function ViewWorkspaceComponent_Template_mj_view_config_panel_Close_7_listener() { return ctx.onCloseConfigPanel(); })("Save", function ViewWorkspaceComponent_Template_mj_view_config_panel_Save_7_listener($event) { return ctx.onSaveView($event); })("SaveDefaults", function ViewWorkspaceComponent_Template_mj_view_config_panel_SaveDefaults_7_listener($event) { return ctx.onSaveDefaultViewSettings($event); })("Delete", function ViewWorkspaceComponent_Template_mj_view_config_panel_Delete_7_listener() { return ctx.onDeleteView(); })("Duplicate", function ViewWorkspaceComponent_Template_mj_view_config_panel_Duplicate_7_listener() { return ctx.onDuplicateFromPanel(); })("OpenFilterDialogRequest", function ViewWorkspaceComponent_Template_mj_view_config_panel_OpenFilterDialogRequest_7_listener($event) { return ctx.onOpenFilterDialogRequest($event); });
1116
+ i0.ɵɵelementEnd();
1117
+ i0.ɵɵconditionalCreate(8, ViewWorkspaceComponent_Conditional_8_Template, 17, 3);
1118
+ i0.ɵɵelementStart(9, "mj-quick-save-dialog", 8);
1119
+ i0.ɵɵlistener("Save", function ViewWorkspaceComponent_Template_mj_quick_save_dialog_Save_9_listener($event) { return ctx.onQuickSave($event); })("Close", function ViewWorkspaceComponent_Template_mj_quick_save_dialog_Close_9_listener() { return ctx.onQuickSaveClose(); })("OpenAdvanced", function ViewWorkspaceComponent_Template_mj_quick_save_dialog_OpenAdvanced_9_listener($event) { return ctx.onQuickSaveOpenAdvanced($event); });
1120
+ i0.ɵɵelementEnd();
1121
+ i0.ɵɵelementStart(10, "mj-duplicate-view-dialog", 9);
1122
+ i0.ɵɵlistener("Duplicate", function ViewWorkspaceComponent_Template_mj_duplicate_view_dialog_Duplicate_10_listener($event) { return ctx.onDuplicateConfirmed($event); })("Cancel", function ViewWorkspaceComponent_Template_mj_duplicate_view_dialog_Cancel_10_listener() { return ctx.onDuplicateCancel(); });
1123
+ i0.ɵɵelementEnd();
1124
+ i0.ɵɵelementStart(11, "mj-shared-view-warning-dialog", 10);
1125
+ i0.ɵɵlistener("Action", function ViewWorkspaceComponent_Template_mj_shared_view_warning_dialog_Action_11_listener($event) { return ctx.onSharedViewAction($event); })("Cancel", function ViewWorkspaceComponent_Template_mj_shared_view_warning_dialog_Cancel_11_listener() { return ctx.onSharedViewWarningCancel(); });
1126
+ i0.ɵɵelementEnd()();
1127
+ } if (rf & 2) {
1128
+ i0.ɵɵadvance(2);
1129
+ i0.ɵɵproperty("Provider", ctx.Provider)("Entity", ctx.Entity)("SelectedViewID", ctx.selectedViewId)("ViewModified", ctx.viewModified);
1130
+ i0.ɵɵadvance(2);
1131
+ i0.ɵɵproperty("Provider", ctx.Provider)("Entity", ctx.Entity)("ActiveViewTypeID", ctx.activeViewTypeId);
1132
+ i0.ɵɵadvance(2);
1133
+ i0.ɵɵconditional(ctx.Entity ? 6 : -1);
1134
+ i0.ɵɵadvance();
1135
+ i0.ɵɵproperty("Provider", ctx.Provider)("Entity", ctx.Entity)("ViewEntity", ctx.currentViewEntity)("IsOpen", ctx.isConfigPanelOpen)("CurrentGridState", ctx.currentGridState)("SampleData", ctx.loadedRecords)("ExternalFilterState", ctx.filterDialogState)("IsSaving", ctx.isSavingView)("DefaultSaveAsNew", ctx.defaultSaveAsNew)("PendingNewViewName", ctx.pendingNewViewName)("PendingNewViewDescription", ctx.pendingNewViewDescription)("PendingNewViewIsShared", ctx.pendingNewViewIsShared);
1136
+ i0.ɵɵadvance();
1137
+ i0.ɵɵconditional(ctx.isFilterDialogOpen ? 8 : -1);
1138
+ i0.ɵɵadvance();
1139
+ i0.ɵɵproperty("IsOpen", ctx.showQuickSaveDialog)("ViewEntity", ctx.currentViewEntity)("EntityName", (ctx.Entity == null ? null : ctx.Entity.DisplayNameOrName) ?? "")("Summary", ctx.quickSaveSummary)("IsSaving", ctx.isSavingView)("DefaultSaveAsNew", ctx.defaultSaveAsNew);
1140
+ i0.ɵɵadvance();
1141
+ i0.ɵɵproperty("IsOpen", ctx.showDuplicateDialog)("SourceViewName", ctx.duplicateSourceViewName)("Summary", ctx.duplicateSummary);
1142
+ i0.ɵɵadvance();
1143
+ i0.ɵɵproperty("IsOpen", ctx.showSharedViewWarning)("ViewName", (ctx.currentViewEntity == null ? null : ctx.currentViewEntity.Name) ?? "");
1144
+ } }, dependencies: [i1.MJButtonDirective, i2.FilterBuilderComponent, i3.EntityViewerComponent, i4.ViewConfigPanelComponent, i5.QuickSaveDialogComponent, i6.DuplicateViewDialogComponent, i7.SharedViewWarningDialogComponent, i8.ViewSelectorComponent, i9.ViewTypeSwitcherComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n}\n\n.view-workspace[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n background: var(--mj-bg-page);\n}\n\n\n\n.workspace-toolbar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n}\n\n\n\n.workspace-toolbar-spacer[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n\n\n.workspace-body[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.workspace-body[_ngcontent-%COMP%] mj-entity-viewer[_ngcontent-%COMP%] {\n display: block;\n height: 100%;\n}\n\n\n\n\n\n.filter-dialog-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 2000;\n animation: _ngcontent-%COMP%_vw-fade-in 0.15s ease;\n}\n\n.filter-dialog[_ngcontent-%COMP%] {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 90%;\n max-width: 800px;\n max-height: 85vh;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-text-primary) 25%, transparent);\n z-index: 2001;\n animation: _ngcontent-%COMP%_vw-slide-in 0.2s ease;\n}\n\n.filter-dialog-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-dialog-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 18px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.filter-dialog-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n.filter-dialog-close[_ngcontent-%COMP%] {\n width: 36px;\n height: 36px;\n border: none;\n background: transparent;\n border-radius: 6px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n.filter-dialog-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.filter-dialog-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n min-height: 200px;\n}\n\n.filter-dialog-footer[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-card);\n border-radius: 0 0 12px 12px;\n flex-shrink: 0;\n}\n\n@keyframes _ngcontent-%COMP%_vw-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes _ngcontent-%COMP%_vw-slide-in {\n from {\n opacity: 0;\n transform: translate(-50%, -48%);\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@media (max-width: 600px) {\n .filter-dialog[_ngcontent-%COMP%] {\n width: 95%;\n max-height: 90vh;\n border-radius: 8px;\n }\n\n .filter-dialog-footer[_ngcontent-%COMP%] {\n flex-direction: column;\n align-items: stretch;\n }\n}"] });
1145
+ }
1146
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ViewWorkspaceComponent, [{
1147
+ type: Component,
1148
+ args: [{ standalone: false, selector: 'mj-view-workspace', template: "<div class=\"view-workspace\">\n <!-- Toolbar: view selector + view-type switcher (single row) -->\n <div class=\"workspace-toolbar\">\n <mj-view-selector\n [Provider]=\"Provider\"\n [Entity]=\"Entity\"\n [SelectedViewID]=\"selectedViewId\"\n [ViewModified]=\"viewModified\"\n (ViewSelected)=\"onViewSelected($event)\"\n (SaveViewRequested)=\"onSaveViewRequested($event)\"\n (OpenInTabRequested)=\"onOpenInTabRequested($event)\"\n (ConfigureViewRequested)=\"onConfigureViewRequested()\"\n (CreateNewRecordRequested)=\"onCreateNewRecordRequested()\"\n (DuplicateViewRequested)=\"onDuplicateViewRequested($event)\"\n (QuickSaveRequested)=\"onQuickSaveRequested($event)\"\n (RevertRequested)=\"onRevertView()\">\n </mj-view-selector>\n\n <span class=\"workspace-toolbar-spacer\"></span>\n\n <!-- View-type switcher lifted into the top toolbar (same row as the selector). The inner\n entity-viewer's own header switcher is suppressed via innerViewerConfig so it appears\n exactly once. Switching routes to the viewer via SelectViewTypeById. -->\n <mj-view-type-switcher\n [Provider]=\"Provider\"\n [Entity]=\"Entity\"\n [ActiveViewTypeID]=\"activeViewTypeId\"\n (ViewTypeSelected)=\"onToolbarViewTypeSelected($event)\">\n </mj-view-type-switcher>\n </div>\n\n <!-- Data renderer -->\n <div class=\"workspace-body\">\n @if (Entity) {\n <mj-entity-viewer\n [Provider]=\"Provider\"\n [Entity]=\"Entity\"\n [ViewEntity]=\"currentViewEntity\"\n [FilterText]=\"filterText\"\n [SelectedRecordID]=\"selectedRecordId\"\n [GridState]=\"currentGridState\"\n [AutoSaveView]=\"AutoSaveView\"\n [Config]=\"innerViewerConfig\"\n (FilterTextChange)=\"onFilterTextChanged($event)\"\n (RecordSelected)=\"onRecordSelected($event)\"\n (RecordOpened)=\"onRecordOpened($event)\"\n (DataLoaded)=\"onDataLoaded($event)\"\n (FilteredCountChanged)=\"onFilteredCountChanged($event)\"\n (OpenRelatedRecordRequested)=\"onOpenRelatedRecordRequested($event)\"\n (CreateRecordRequested)=\"onCreateRecordRequested()\">\n </mj-entity-viewer>\n }\n </div>\n\n <!-- View configuration panel (slide-in) -->\n <mj-view-config-panel\n [Provider]=\"Provider\"\n [Entity]=\"Entity\"\n [ViewEntity]=\"currentViewEntity\"\n [IsOpen]=\"isConfigPanelOpen\"\n [CurrentGridState]=\"currentGridState\"\n [SampleData]=\"loadedRecords\"\n [ExternalFilterState]=\"filterDialogState\"\n [IsSaving]=\"isSavingView\"\n [DefaultSaveAsNew]=\"defaultSaveAsNew\"\n [PendingNewViewName]=\"pendingNewViewName\"\n [PendingNewViewDescription]=\"pendingNewViewDescription\"\n [PendingNewViewIsShared]=\"pendingNewViewIsShared\"\n (Close)=\"onCloseConfigPanel()\"\n (Save)=\"onSaveView($event)\"\n (SaveDefaults)=\"onSaveDefaultViewSettings($event)\"\n (Delete)=\"onDeleteView()\"\n (Duplicate)=\"onDuplicateFromPanel()\"\n (OpenFilterDialogRequest)=\"onOpenFilterDialogRequest($event)\">\n </mj-view-config-panel>\n\n <!-- Full-width filter dialog -->\n @if (isFilterDialogOpen) {\n <div class=\"filter-dialog-backdrop\" (click)=\"onCloseFilterDialog()\"></div>\n <div class=\"filter-dialog\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"filter-dialog-header\">\n <div class=\"filter-dialog-title\">\n <i class=\"fa-solid fa-filter\"></i>\n <span>Edit Filters</span>\n </div>\n <button class=\"filter-dialog-close\" type=\"button\" (click)=\"onCloseFilterDialog()\" title=\"Close\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n <div class=\"filter-dialog-content\">\n <mj-filter-builder\n [fields]=\"filterDialogFields\"\n [filter]=\"filterDialogState\"\n [showSummary]=\"true\"\n (apply)=\"onFilterApplied($event)\"\n (filterChange)=\"filterDialogState = $event\">\n </mj-filter-builder>\n </div>\n <div class=\"filter-dialog-footer\">\n <button mjButton variant=\"primary\" size=\"sm\" type=\"button\"\n (click)=\"filterDialogState && onFilterApplied(filterDialogState)\">\n <i class=\"fa-solid fa-check\"></i> Apply Filters\n </button>\n <button mjButton variant=\"flat\" size=\"sm\" type=\"button\" (click)=\"onCloseFilterDialog()\">\n Cancel\n </button>\n </div>\n </div>\n }\n\n <!-- Quick save dialog -->\n <mj-quick-save-dialog\n [IsOpen]=\"showQuickSaveDialog\"\n [ViewEntity]=\"currentViewEntity\"\n [EntityName]=\"Entity?.DisplayNameOrName ?? ''\"\n [Summary]=\"quickSaveSummary\"\n [IsSaving]=\"isSavingView\"\n [DefaultSaveAsNew]=\"defaultSaveAsNew\"\n (Save)=\"onQuickSave($event)\"\n (Close)=\"onQuickSaveClose()\"\n (OpenAdvanced)=\"onQuickSaveOpenAdvanced($event)\">\n </mj-quick-save-dialog>\n\n <!-- Duplicate view dialog -->\n <mj-duplicate-view-dialog\n [IsOpen]=\"showDuplicateDialog\"\n [SourceViewName]=\"duplicateSourceViewName\"\n [Summary]=\"duplicateSummary\"\n (Duplicate)=\"onDuplicateConfirmed($event)\"\n (Cancel)=\"onDuplicateCancel()\">\n </mj-duplicate-view-dialog>\n\n <!-- Shared view warning dialog -->\n <mj-shared-view-warning-dialog\n [IsOpen]=\"showSharedViewWarning\"\n [ViewName]=\"currentViewEntity?.Name ?? ''\"\n (Action)=\"onSharedViewAction($event)\"\n (Cancel)=\"onSharedViewWarningCancel()\">\n </mj-shared-view-warning-dialog>\n</div>\n", styles: [":host {\n display: block;\n height: 100%;\n}\n\n.view-workspace {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n background: var(--mj-bg-page);\n}\n\n/* Toolbar row: view selector + view-type switcher */\n.workspace-toolbar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n}\n\n/* Pushes the view-type switcher to the right end of the toolbar row */\n.workspace-toolbar-spacer {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Data renderer fills remaining space */\n.workspace-body {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.workspace-body mj-entity-viewer {\n display: block;\n height: 100%;\n}\n\n/* ============================\n Full-width filter dialog\n ============================ */\n.filter-dialog-backdrop {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 2000;\n animation: vw-fade-in 0.15s ease;\n}\n\n.filter-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 90%;\n max-width: 800px;\n max-height: 85vh;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-text-primary) 25%, transparent);\n z-index: 2001;\n animation: vw-slide-in 0.2s ease;\n}\n\n.filter-dialog-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-dialog-title {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 18px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.filter-dialog-title i {\n color: var(--mj-brand-primary);\n}\n\n.filter-dialog-close {\n width: 36px;\n height: 36px;\n border: none;\n background: transparent;\n border-radius: 6px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n.filter-dialog-close:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.filter-dialog-content {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n min-height: 200px;\n}\n\n.filter-dialog-footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-card);\n border-radius: 0 0 12px 12px;\n flex-shrink: 0;\n}\n\n@keyframes vw-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes vw-slide-in {\n from {\n opacity: 0;\n transform: translate(-50%, -48%);\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@media (max-width: 600px) {\n .filter-dialog {\n width: 95%;\n max-height: 90vh;\n border-radius: 8px;\n }\n\n .filter-dialog-footer {\n flex-direction: column;\n align-items: stretch;\n }\n}\n"] }]
1149
+ }], () => [{ type: i0.ChangeDetectorRef }], { Entity: [{
1150
+ type: Input
1151
+ }], EntityName: [{
1152
+ type: Input
1153
+ }], EntityID: [{
1154
+ type: Input
1155
+ }], AutoSaveView: [{
1156
+ type: Input
1157
+ }], SelectedView: [{
1158
+ type: Input
1159
+ }], FilterText: [{
1160
+ type: Input
1161
+ }], SelectedRecordId: [{
1162
+ type: Input
1163
+ }], ViewerConfig: [{
1164
+ type: Input
1165
+ }], OpenRecordRequested: [{
1166
+ type: Output
1167
+ }], OpenViewInTabRequested: [{
1168
+ type: Output
1169
+ }], CreateNewRecordRequested: [{
1170
+ type: Output
1171
+ }], OpenRelatedRecordRequested: [{
1172
+ type: Output
1173
+ }], RecordSelected: [{
1174
+ type: Output
1175
+ }], ViewSelected: [{
1176
+ type: Output
1177
+ }], SelectedViewChange: [{
1178
+ type: Output
1179
+ }], FilterTextChanged: [{
1180
+ type: Output
1181
+ }], DataLoaded: [{
1182
+ type: Output
1183
+ }], FilteredCountChanged: [{
1184
+ type: Output
1185
+ }], BeforeViewSave: [{
1186
+ type: Output
1187
+ }], AfterViewSave: [{
1188
+ type: Output
1189
+ }], BeforeViewDelete: [{
1190
+ type: Output
1191
+ }], AfterViewDelete: [{
1192
+ type: Output
1193
+ }], SaveViewRequested: [{
1194
+ type: Output
1195
+ }], DeleteViewRequested: [{
1196
+ type: Output
1197
+ }], DuplicateViewRequested: [{
1198
+ type: Output
1199
+ }], SaveDefaultsRequested: [{
1200
+ type: Output
1201
+ }], viewSelectorRef: [{
1202
+ type: ViewChild,
1203
+ args: [ViewSelectorComponent]
1204
+ }], entityViewerRef: [{
1205
+ type: ViewChild,
1206
+ args: [EntityViewerComponent]
1207
+ }], viewConfigPanelRef: [{
1208
+ type: ViewChild,
1209
+ args: [ViewConfigPanelComponent]
1210
+ }] }); })();
1211
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ViewWorkspaceComponent, { className: "ViewWorkspaceComponent", filePath: "src/lib/view-workspace/view-workspace.component.ts", lineNumber: 127 }); })();
1212
+ //# sourceMappingURL=view-workspace.component.js.map