@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,558 @@
1
+ import { Component, Input, Output, EventEmitter, ViewEncapsulation, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
2
+ import { LogError } from '@memberjunction/core';
3
+ import { UUIDsEqual } from '@memberjunction/global';
4
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
5
+ import { buildPkString, buildCompositeKey } from '../../utils/record.util';
6
+ import * as i0 from "@angular/core";
7
+ import * as i1 from "@memberjunction/ng-list-management";
8
+ import * as i2 from "../../entity-data-grid/entity-data-grid.component";
9
+ import * as i3 from "../../confirm-dialog/confirm-dialog.component";
10
+ const _c0 = ["grid"];
11
+ const _c1 = () => ({});
12
+ function GridViewRendererComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
13
+ const _r2 = i0.ɵɵgetCurrentView();
14
+ i0.ɵɵelementStart(0, "mj-list-management-dialog", 4);
15
+ i0.ɵɵlistener("complete", function GridViewRendererComponent_Conditional_2_Template_mj_list_management_dialog_complete_0_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onListManagementComplete($event)); })("cancel", function GridViewRendererComponent_Conditional_2_Template_mj_list_management_dialog_cancel_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onListManagementCancel()); });
16
+ i0.ɵɵelementEnd();
17
+ } if (rf & 2) {
18
+ const ctx_r2 = i0.ɵɵnextContext();
19
+ i0.ɵɵproperty("Provider", ctx_r2.Provider)("visible", ctx_r2.showListManagementDialog)("config", ctx_r2.listManagementConfig);
20
+ } }
21
+ /**
22
+ * GridViewRendererComponent
23
+ * -------------------------
24
+ * The Grid **view type** renderer — a fully self-contained {@link IViewRenderer} adapter that hosts
25
+ * the existing {@link EntityDataGridComponent} (`<mj-entity-data-grid>`) inside the entity-viewer's
26
+ * pluggable view-type system. It is the dynamic-mounted plug-in that the `GridViewType` descriptor
27
+ * points at.
28
+ *
29
+ * **Architectural intent — the container knows nothing about grids, and grid *features* never bubble
30
+ * up.** The host (entity-viewer) binds only the generic {@link IViewRenderer} surface: the core
31
+ * inputs (`entity` / `provider` / `records` / `selectedRecordId` / `filterText` / `config`), the
32
+ * generic data-context inputs (`totalRecordCount` / `page` / `pageSize` / `isLoading`), and a small
33
+ * set of generic outputs. There is **no opaque `hostAction` channel** — everything a grid does is
34
+ * resolved through one of these three categories:
35
+ *
36
+ * 1. **Self-contained (owned end-to-end — never bubbles up):**
37
+ * - **Export** → owned entirely by `<mj-entity-data-grid>` itself (its `onExportClick` opens its
38
+ * OWN export dialog with format/sampling via `ExportService`). This wrapper does NOT host an
39
+ * export dialog — doing so produced two stacked dialogs.
40
+ * - **Add to List** → hosts `<mj-list-management-dialog>` ({@link ListManagementModule}). On
41
+ * `(AddToListRequested)` the wrapper builds a {@link ListManagementDialogConfig} from the
42
+ * entity + selected records and opens the dialog.
43
+ * - **Delete** → hosts the Generic `<mj-ev-confirm-dialog>` (from {@link EntityViewerModule}).
44
+ * On `(DeleteButtonClick)` the wrapper confirms, deletes via the MJ entity layer, then
45
+ * re-requests data so the host reloads the current page.
46
+ * - **Refresh** → on `(RefreshButtonClick)` the wrapper re-emits {@link dataRequest} so the host
47
+ * reloads. No feature event leaves the wrapper.
48
+ * - **Selection** → kept INTERNAL; it only drives the wrapper's own add-to-list. Never bubbles.
49
+ *
50
+ * 2. **Navigation (the ONLY legitimate upward signals — routing lives in the outer app):**
51
+ * - Row double-click → {@link recordOpened}.
52
+ * - Foreign-key cell click `(ForeignKeyClick)` → {@link openRelatedRecordRequested}.
53
+ * - New button `(NewButtonClick)` → {@link createRecordRequested}.
54
+ *
55
+ * 3. **Container ↔ plug-in generic coordination (NOT outer-app signals):**
56
+ * - Sort `(AfterSort)` → {@link dataRequest} (`sort`) + persisted into `config.gridState` via
57
+ * {@link configChanged}.
58
+ * - Pager `(PageChange)` → {@link dataRequest} (`page` / `pageSize`).
59
+ * - Generic grid-state `(GridStateChanged)` → merged into `config.gridState` + {@link configChanged}.
60
+ *
61
+ * **Seeding the grid from `config` with defaults:** absent config fields fall back to sensible
62
+ * defaults so a brand-new view (`config === {}`) renders a fully-functional grid — toolbar on,
63
+ * checkbox selection, add-to-list on, pager on. The grid never loads its own data
64
+ * (`[AllowLoad]="false"`) — the host owns the fetch.
65
+ *
66
+ * This is an NgModule-declared (`standalone: false`) component, declared in `EntityViewerModule`.
67
+ * It renders `<mj-entity-data-grid>` + the Generic `<mj-list-management-dialog>` / `<mj-ev-confirm-dialog>`
68
+ * straight from the module's compilation scope (the module imports `ListManagementModule` and declares
69
+ * the grid + confirm dialog; the grid component brings its own export dialog) — so there's
70
+ * no `imports` array and, crucially, no self-import of `EntityViewerModule`: the module loads the
71
+ * view-type descriptors, which reference these wrappers, so a wrapper importing the module back would
72
+ * form a runtime import cycle (NG0919).
73
+ *
74
+ * Inputs use the camelCase names mandated by the {@link IViewRenderer} contract (the host binds them
75
+ * by those exact names via `setInput`), rather than MJ's usual PascalCase for public members —
76
+ * mirroring the Cards and Cluster renderers.
77
+ */
78
+ export class GridViewRendererComponent extends BaseAngularComponent {
79
+ /** Change detection ref used to flush dialog visibility toggles driven by grid events. */
80
+ cdr = inject(ChangeDetectorRef);
81
+ // ---- IViewRenderer core inputs (camelCase per the host contract) ----
82
+ /** The entity whose records are being rendered. Used to build default params + resolve selection. */
83
+ entity = null;
84
+ /** The records to render (already loaded / filtered / paged by the host). Fed into `[Data]`. */
85
+ records = [];
86
+ /** Primary-key string of the currently selected record, if any. */
87
+ selectedRecordId = null;
88
+ /** Active filter text — passed through for the grid's cell highlighting. */
89
+ filterText = null;
90
+ /** Opaque per-view configuration. Seeds the grid bindings; mutated + re-emitted on grid changes. */
91
+ config = {};
92
+ // ---- IViewRenderer generic data-context inputs ----
93
+ /** Total record count across all pages, for the grid's pager. Fed into `[TotalRowCount]`. */
94
+ totalRecordCount;
95
+ /** One-based current page the host is showing. Fed into `[PagerPageNumber]` (already 1-based). */
96
+ page;
97
+ /** Page size the host is using — fallback when `config.pageSize` is absent. */
98
+ pageSize;
99
+ /** Whether the host is currently (re)loading the record set. Accepted per contract; unused here. */
100
+ isLoading;
101
+ // ---- IViewRenderer outputs (generic + navigation channels only) ----
102
+ /** Emitted when a row is single-clicked — payload is the raw record object (host builds the key). */
103
+ recordSelected = new EventEmitter();
104
+ /** Emitted when a row is double-clicked — payload is the raw record object (host builds the key). */
105
+ recordOpened = new EventEmitter();
106
+ /** Emitted when this renderer mutates its opaque {@link config} (sort / grid-state persistence). */
107
+ configChanged = new EventEmitter();
108
+ /** Generic data-access channel: ask the host to re-load with different sort / page. */
109
+ dataRequest = new EventEmitter();
110
+ /**
111
+ * NAVIGATION: open a related record on a (possibly different) entity from a foreign-key cell click.
112
+ * Routing lives in the outer app, so this is one of the few signals that legitimately bubbles up.
113
+ */
114
+ openRelatedRecordRequested = new EventEmitter();
115
+ /** NAVIGATION: create a new record of the current entity (grid "New" button). Bubbles up for routing. */
116
+ createRecordRequested = new EventEmitter();
117
+ /**
118
+ * Reference to the hosted grid. Currently only retained for parity / future selection resolution;
119
+ * selection is mapped from the {@link records} input rather than read from the grid, per contract.
120
+ */
121
+ grid;
122
+ // ================================================================
123
+ // Self-contained dialog state (never surfaced to the host)
124
+ // ================================================================
125
+ // NOTE: Export has no state here — mj-entity-data-grid owns its export dialog.
126
+ /** Whether the add-to-list dialog is visible. Toggled internally by {@link onAddToListRequested}. */
127
+ showListManagementDialog = false;
128
+ /** Config fed into `<mj-list-management-dialog>`. Built from the entity + selected records. */
129
+ listManagementConfig = null;
130
+ /** Whether the delete-confirmation dialog is visible. Toggled internally by {@link onDeleteButtonClick}. */
131
+ showDeleteConfirm = false;
132
+ /** Records staged for deletion while the confirm dialog is open. */
133
+ pendingDeleteRecords = [];
134
+ /** Dynamic message for the delete-confirmation dialog (reflects the staged record count). */
135
+ deleteConfirmMessage = 'Are you sure you want to delete the selected records?';
136
+ // ================================================================
137
+ // Effective binding accessors — seed grid inputs from config + defaults
138
+ // ================================================================
139
+ /** Effective toolbar visibility — defaults to `true` when not set in config. */
140
+ get effectiveShowToolbar() {
141
+ return this.config.showToolbar ?? true;
142
+ }
143
+ /** Effective selection mode — defaults to `'checkbox'` when not set in config. */
144
+ get effectiveSelectionMode() {
145
+ return this.config.selectionMode ?? 'checkbox';
146
+ }
147
+ /** Effective add-to-list button visibility — defaults to `true` when not set in config. */
148
+ get effectiveShowAddToListButton() {
149
+ return this.config.showAddToListButton ?? true;
150
+ }
151
+ /** Effective pager visibility — defaults to `true` when not set in config. */
152
+ get effectiveShowPager() {
153
+ return this.config.showPager ?? true;
154
+ }
155
+ /** Effective page size — config wins, else the generic `pageSize` input, else the grid's own default. */
156
+ get effectivePageSize() {
157
+ return this.config.pageSize ?? this.pageSize ?? 100;
158
+ }
159
+ /**
160
+ * Effective RunViewParams for the grid. Uses `config.params` when supplied; otherwise builds a
161
+ * minimal dynamic-view params object from {@link entity}. Returns `null` when no entity is known
162
+ * yet (the grid renders empty until the host provides one).
163
+ */
164
+ get effectiveParams() {
165
+ if (this.config.params) {
166
+ return this.config.params;
167
+ }
168
+ if (this.entity) {
169
+ return { EntityName: this.entity.Name };
170
+ }
171
+ return null;
172
+ }
173
+ // ================================================================
174
+ // Navigation + generic coordination output mapping
175
+ // ================================================================
176
+ /**
177
+ * Row single-click → {@link recordSelected}. Extracts the raw record (`event.row`) so the host
178
+ * builds the composite key itself, matching the other renderers.
179
+ */
180
+ onAfterRowClick(event) {
181
+ if (event.row) {
182
+ this.recordSelected.emit(event.row);
183
+ }
184
+ }
185
+ /** Row double-click → {@link recordOpened} (NAVIGATION), emitting the raw record object. */
186
+ onAfterRowDoubleClick(event) {
187
+ if (event.row) {
188
+ this.recordOpened.emit(event.row);
189
+ }
190
+ }
191
+ /**
192
+ * Sort change → both generic coordination channels:
193
+ * 1. {@link dataRequest} with the generic `sort` shape so the host re-loads with a new OrderBy.
194
+ * 2. persists the sort into `config.gridState.sortSettings` and emits {@link configChanged} so
195
+ * the grid's sort survives view reloads via the opaque config channel.
196
+ */
197
+ onAfterSort(event) {
198
+ const sort = (event.newSortState ?? []).map((s) => ({ field: s.field, direction: s.direction }));
199
+ this.dataRequest.emit({ sort });
200
+ const gridState = { ...(this.config.gridState ?? {}) };
201
+ gridState.sortSettings = (event.newSortState ?? []).map((s) => ({ field: s.field, dir: s.direction }));
202
+ this.config = { ...this.config, gridState };
203
+ this.configChanged.emit(this.config);
204
+ }
205
+ /**
206
+ * Generic grid-state change (column resize / reorder / visibility) → merge the grid's updated
207
+ * {@link ViewGridState} into the opaque config and emit {@link configChanged}; the host persists
208
+ * the blob verbatim. No `dataRequest` here — column changes don't alter the loaded record set.
209
+ */
210
+ onGridStateChanged(event) {
211
+ this.config = { ...this.config, gridState: event.gridState };
212
+ this.configChanged.emit(this.config);
213
+ }
214
+ /**
215
+ * Selection change → kept INTERNAL. The grid drives its own add-to-list from the selection, so the
216
+ * wrapper has no need to surface it. We intentionally do not bubble selection anywhere.
217
+ */
218
+ onSelectionChange(_selectedKeys) {
219
+ // No-op: selection is internal to the grid + this wrapper's add-to-list. It never bubbles up.
220
+ }
221
+ /**
222
+ * Foreign-key link click → {@link openRelatedRecordRequested} (NAVIGATION). Maps the grid's
223
+ * {@link ForeignKeyClickEvent} (which carries the related entity + the FK value) onto the generic
224
+ * {@link ViewRelatedRecordNavigation} the container forwards to the outer app for routing. The
225
+ * related entity name is taken from the event when present, else resolved from the metadata by ID.
226
+ */
227
+ onForeignKeyClick(event) {
228
+ const entityName = event.relatedEntityName ?? this.resolveEntityNameById(event.relatedEntityId);
229
+ if (!entityName) {
230
+ return;
231
+ }
232
+ this.openRelatedRecordRequested.emit({ entityName, recordKey: event.recordId });
233
+ }
234
+ /** New button → {@link createRecordRequested} (NAVIGATION) — opening the create form is routing. */
235
+ onNewButtonClick() {
236
+ this.createRecordRequested.emit();
237
+ }
238
+ /**
239
+ * Pager navigation → generic {@link dataRequest}. Maps the grid's {@link PageChangeEvent}
240
+ * (`PageNumber` is already 1-based, `PageSize`) onto the generic `{ page, pageSize }` shape the
241
+ * host honors against its own RunView.
242
+ */
243
+ onPageChange(event) {
244
+ this.dataRequest.emit({ page: event.PageNumber, pageSize: event.PageSize });
245
+ }
246
+ // ================================================================
247
+ // Self-contained: Refresh
248
+ // ================================================================
249
+ /**
250
+ * Refresh button → re-emit {@link dataRequest} so the host reloads the current page. This is
251
+ * generic container ↔ plug-in coordination (a data-access request), NOT a feature event surfaced
252
+ * to the outer app.
253
+ */
254
+ onRefreshButtonClick() {
255
+ this.dataRequest.emit(this.currentPageDataRequest());
256
+ }
257
+ // ================================================================
258
+ // Export: handled entirely by mj-entity-data-grid (its onExportClick opens its own export dialog
259
+ // via ExportService). This wrapper intentionally does NOT host an export dialog — doing so produced
260
+ // two stacked dialogs. Nothing to do here.
261
+ // ================================================================
262
+ // ================================================================
263
+ // Self-contained: Add to List (hosts <mj-list-management-dialog>)
264
+ // ================================================================
265
+ /**
266
+ * Add-to-list button → build a {@link ListManagementDialogConfig} from the entity + the records the
267
+ * grid supplied, then open the Generic list-management dialog. The dialog persists membership
268
+ * changes itself — nothing bubbles up. Mirrors the config construction the legacy host did in
269
+ * `data-explorer-dashboard`'s `onAddToListRequested()`.
270
+ */
271
+ onAddToListRequested(event) {
272
+ const entity = event.entityInfo ?? this.entity;
273
+ if (!entity || !event.records || event.records.length === 0) {
274
+ return;
275
+ }
276
+ const recordDisplayNames = event.records.map((record) => this.getRecordDisplayName(record, entity));
277
+ const recordIds = this.buildRawRecordIds(event.records, entity);
278
+ if (recordIds.length === 0) {
279
+ return;
280
+ }
281
+ const recordCount = event.records.length;
282
+ const dialogTitle = recordCount === 1 ? `Manage Lists for "${recordDisplayNames[0]}"` : `Add ${recordCount} Records to List`;
283
+ this.listManagementConfig = {
284
+ mode: 'manage',
285
+ entityId: entity.ID,
286
+ entityName: entity.Name,
287
+ recordIds,
288
+ recordDisplayNames,
289
+ allowCreate: true,
290
+ allowRemove: recordCount === 1,
291
+ showMembership: recordCount === 1,
292
+ dialogTitle,
293
+ };
294
+ this.showListManagementDialog = true;
295
+ this.cdr.detectChanges();
296
+ }
297
+ /** List-management dialog completed (membership changes applied) → tear down state. */
298
+ onListManagementComplete(_result) {
299
+ this.showListManagementDialog = false;
300
+ this.listManagementConfig = null;
301
+ this.cdr.detectChanges();
302
+ }
303
+ /** List-management dialog cancelled → tear down state. */
304
+ onListManagementCancel() {
305
+ this.showListManagementDialog = false;
306
+ this.listManagementConfig = null;
307
+ this.cdr.detectChanges();
308
+ }
309
+ /**
310
+ * Resolve a human-friendly display name for a record using the entity's NameField, falling back to
311
+ * the composite-key string. Mirrors the legacy host's display-name resolution.
312
+ */
313
+ getRecordDisplayName(record, entity) {
314
+ if (entity.NameField) {
315
+ const nameValue = record[entity.NameField.Name];
316
+ if (nameValue != null) {
317
+ return String(nameValue);
318
+ }
319
+ }
320
+ return buildPkString(record, entity) || 'Unknown';
321
+ }
322
+ /**
323
+ * Build the list of RAW primary-key values (not concatenated composite-key strings) used by list
324
+ * membership matching. Mirrors the legacy host, which deliberately extracts the first PK field's
325
+ * raw value so it matches how List Details store records.
326
+ */
327
+ buildRawRecordIds(records, entity) {
328
+ const pkFieldName = entity.PrimaryKeys[0]?.Name;
329
+ if (!pkFieldName) {
330
+ return [];
331
+ }
332
+ return records
333
+ .map((record) => {
334
+ const value = record[pkFieldName];
335
+ return value == null ? '' : String(value);
336
+ })
337
+ .filter((id) => id !== '');
338
+ }
339
+ // ================================================================
340
+ // Self-contained: Delete (hosts <mj-ev-confirm-dialog> + MJ entity layer)
341
+ // ================================================================
342
+ /**
343
+ * Delete button → stage the records and open the Generic confirm dialog. The actual delete happens
344
+ * in {@link onDeleteConfirmed} after the user confirms — nothing bubbles up.
345
+ */
346
+ onDeleteButtonClick(records) {
347
+ if (!records || records.length === 0) {
348
+ return;
349
+ }
350
+ this.pendingDeleteRecords = records;
351
+ const count = records.length;
352
+ this.deleteConfirmMessage = count === 1 ? 'Are you sure you want to delete this record?' : `Are you sure you want to delete these ${count} records?`;
353
+ this.showDeleteConfirm = true;
354
+ this.cdr.detectChanges();
355
+ }
356
+ /**
357
+ * Delete confirmed → delete each staged record through the MJ entity layer
358
+ * (`ProviderToUse.GetEntityObject(name, compositeKey, user)` → `Delete`), checking the boolean
359
+ * result and surfacing failures via {@link LogError}. After deletion, re-request the current page
360
+ * so the host reloads. Self-contained — no feature event leaves the wrapper.
361
+ */
362
+ async onDeleteConfirmed() {
363
+ this.showDeleteConfirm = false;
364
+ const entity = this.entity;
365
+ const records = this.pendingDeleteRecords;
366
+ this.pendingDeleteRecords = [];
367
+ if (!entity || records.length === 0) {
368
+ return;
369
+ }
370
+ let anyDeleted = false;
371
+ const provider = this.ProviderToUse;
372
+ const user = provider.CurrentUser;
373
+ for (const record of records) {
374
+ const key = buildCompositeKey(record, entity);
375
+ try {
376
+ // The (name, key, user) overload instantiates AND loads in one call (throws if it can't load).
377
+ const obj = await provider.GetEntityObject(entity.Name, key, user);
378
+ const deleted = await obj.Delete();
379
+ if (deleted) {
380
+ anyDeleted = true;
381
+ }
382
+ else {
383
+ LogError(`Delete failed: ${obj.LatestResult?.CompleteMessage ?? 'unknown error'}`);
384
+ }
385
+ }
386
+ catch (err) {
387
+ LogError(`Delete failed for ${entity.Name} (${key.ToString()}): ${err instanceof Error ? err.message : String(err)}`);
388
+ }
389
+ }
390
+ if (anyDeleted) {
391
+ // Re-request the current page so the host reloads the (now smaller) record set.
392
+ this.dataRequest.emit(this.currentPageDataRequest());
393
+ }
394
+ this.cdr.detectChanges();
395
+ }
396
+ /** Delete cancelled → discard the staged records and close the confirm dialog. */
397
+ onDeleteCancelled() {
398
+ this.showDeleteConfirm = false;
399
+ this.pendingDeleteRecords = [];
400
+ this.cdr.detectChanges();
401
+ }
402
+ // ================================================================
403
+ // Helpers
404
+ // ================================================================
405
+ /** Build a {@link ViewDataRequest} that re-requests the host's current page/pageSize, if known. */
406
+ currentPageDataRequest() {
407
+ const req = {};
408
+ if (this.page != null) {
409
+ req.page = this.page;
410
+ }
411
+ const ps = this.config.pageSize ?? this.pageSize;
412
+ if (ps != null) {
413
+ req.pageSize = ps;
414
+ }
415
+ return req;
416
+ }
417
+ /**
418
+ * Resolve an entity's name from its ID via the active provider's metadata. Used when a foreign-key
419
+ * click event omits the related entity name and only carries its ID.
420
+ */
421
+ resolveEntityNameById(entityId) {
422
+ if (!entityId) {
423
+ return null;
424
+ }
425
+ const match = this.ProviderToUse.Entities.find((e) => UUIDsEqual(e.ID, entityId));
426
+ return match ? match.Name : null;
427
+ }
428
+ static ɵfac = /*@__PURE__*/ (() => { let ɵGridViewRendererComponent_BaseFactory; return function GridViewRendererComponent_Factory(__ngFactoryType__) { return (ɵGridViewRendererComponent_BaseFactory || (ɵGridViewRendererComponent_BaseFactory = i0.ɵɵgetInheritedFactory(GridViewRendererComponent)))(__ngFactoryType__ || GridViewRendererComponent); }; })();
429
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: GridViewRendererComponent, selectors: [["mj-grid-view-renderer"]], viewQuery: function GridViewRendererComponent_Query(rf, ctx) { if (rf & 1) {
430
+ i0.ɵɵviewQuery(_c0, 5);
431
+ } if (rf & 2) {
432
+ let _t;
433
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.grid = _t.first);
434
+ } }, inputs: { entity: "entity", records: "records", selectedRecordId: "selectedRecordId", filterText: "filterText", config: "config", totalRecordCount: "totalRecordCount", page: "page", pageSize: "pageSize", isLoading: "isLoading" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", configChanged: "configChanged", dataRequest: "dataRequest", openRelatedRecordRequested: "openRelatedRecordRequested", createRecordRequested: "createRecordRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 4, vars: 19, consts: [["grid", ""], [3, "AfterRowClick", "AfterRowDoubleClick", "AfterSort", "GridStateChanged", "SelectionChange", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "AddToListRequested", "ForeignKeyClick", "PageChange", "Provider", "Data", "Params", "FilterText", "GridState", "Height", "AllowLoad", "ShowToolbar", "ToolbarConfig", "SelectionMode", "ShowAddToListButton", "ShowPager", "PageSize", "TotalRowCount", "PagerPageNumber"], [3, "Provider", "visible", "config"], ["Title", "Delete Records", "DetailMessage", "This action cannot be undone.", "ConfirmText", "Delete", "ConfirmStyle", "danger", "Icon", "fa-solid fa-trash", 3, "Confirmed", "Cancelled", "IsOpen", "Message"], [3, "complete", "cancel", "Provider", "visible", "config"]], template: function GridViewRendererComponent_Template(rf, ctx) { if (rf & 1) {
435
+ const _r1 = i0.ɵɵgetCurrentView();
436
+ i0.ɵɵelementStart(0, "mj-entity-data-grid", 1, 0);
437
+ i0.ɵɵlistener("AfterRowClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowClick_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onAfterRowClick($event)); })("AfterRowDoubleClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onAfterRowDoubleClick($event)); })("AfterSort", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterSort_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onAfterSort($event)); })("GridStateChanged", function GridViewRendererComponent_Template_mj_entity_data_grid_GridStateChanged_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onGridStateChanged($event)); })("SelectionChange", function GridViewRendererComponent_Template_mj_entity_data_grid_SelectionChange_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onSelectionChange($event)); })("NewButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_NewButtonClick_0_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onNewButtonClick()); })("RefreshButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_RefreshButtonClick_0_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onRefreshButtonClick()); })("DeleteButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_DeleteButtonClick_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDeleteButtonClick($event)); })("AddToListRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_AddToListRequested_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onAddToListRequested($event)); })("ForeignKeyClick", function GridViewRendererComponent_Template_mj_entity_data_grid_ForeignKeyClick_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onForeignKeyClick($event)); })("PageChange", function GridViewRendererComponent_Template_mj_entity_data_grid_PageChange_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onPageChange($event)); });
438
+ i0.ɵɵelementEnd();
439
+ i0.ɵɵconditionalCreate(2, GridViewRendererComponent_Conditional_2_Template, 1, 3, "mj-list-management-dialog", 2);
440
+ i0.ɵɵelementStart(3, "mj-ev-confirm-dialog", 3);
441
+ i0.ɵɵlistener("Confirmed", function GridViewRendererComponent_Template_mj_ev_confirm_dialog_Confirmed_3_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDeleteConfirmed()); })("Cancelled", function GridViewRendererComponent_Template_mj_ev_confirm_dialog_Cancelled_3_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDeleteCancelled()); });
442
+ i0.ɵɵelementEnd();
443
+ } if (rf & 2) {
444
+ i0.ɵɵproperty("Provider", ctx.Provider)("Data", ctx.records)("Params", ctx.effectiveParams)("FilterText", ctx.filterText ?? "")("GridState", ctx.config.gridState ?? null)("Height", "auto")("AllowLoad", false)("ShowToolbar", ctx.effectiveShowToolbar)("ToolbarConfig", ctx.config.toolbarConfig ?? i0.ɵɵpureFunction0(18, _c1))("SelectionMode", ctx.effectiveSelectionMode)("ShowAddToListButton", ctx.effectiveShowAddToListButton)("ShowPager", ctx.effectiveShowPager)("PageSize", ctx.effectivePageSize)("TotalRowCount", ctx.totalRecordCount ?? 0)("PagerPageNumber", ctx.page ?? 1);
445
+ i0.ɵɵadvance(2);
446
+ i0.ɵɵconditional(ctx.listManagementConfig ? 2 : -1);
447
+ i0.ɵɵadvance();
448
+ i0.ɵɵproperty("IsOpen", ctx.showDeleteConfirm)("Message", ctx.deleteConfirmMessage);
449
+ } }, dependencies: [i1.ListManagementDialogComponent, i2.EntityDataGridComponent, i3.ConfirmDialogComponent], styles: ["\n :host {\n display: block;\n height: 100%;\n }\n "], encapsulation: 2 });
450
+ }
451
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(GridViewRendererComponent, [{
452
+ type: Component,
453
+ args: [{ standalone: false, selector: 'mj-grid-view-renderer', encapsulation: ViewEncapsulation.None, template: `
454
+ <mj-entity-data-grid
455
+ #grid
456
+ [Provider]="Provider"
457
+ [Data]="records"
458
+ [Params]="effectiveParams"
459
+ [FilterText]="filterText ?? ''"
460
+ [GridState]="config.gridState ?? null"
461
+ [Height]="'auto'"
462
+ [AllowLoad]="false"
463
+ [ShowToolbar]="effectiveShowToolbar"
464
+ [ToolbarConfig]="config.toolbarConfig ?? {}"
465
+ [SelectionMode]="effectiveSelectionMode"
466
+ [ShowAddToListButton]="effectiveShowAddToListButton"
467
+ [ShowPager]="effectiveShowPager"
468
+ [PageSize]="effectivePageSize"
469
+ [TotalRowCount]="totalRecordCount ?? 0"
470
+ [PagerPageNumber]="page ?? 1"
471
+ (AfterRowClick)="onAfterRowClick($event)"
472
+ (AfterRowDoubleClick)="onAfterRowDoubleClick($event)"
473
+ (AfterSort)="onAfterSort($event)"
474
+ (GridStateChanged)="onGridStateChanged($event)"
475
+ (SelectionChange)="onSelectionChange($event)"
476
+ (NewButtonClick)="onNewButtonClick()"
477
+ (RefreshButtonClick)="onRefreshButtonClick()"
478
+ (DeleteButtonClick)="onDeleteButtonClick($event)"
479
+ (AddToListRequested)="onAddToListRequested($event)"
480
+ (ForeignKeyClick)="onForeignKeyClick($event)"
481
+ (PageChange)="onPageChange($event)"
482
+ >
483
+ </mj-entity-data-grid>
484
+
485
+ <!-- NOTE: Export is NOT handled here — mj-entity-data-grid hosts its OWN export dialog
486
+ (its onExportClick opens it with formats/sampling via ExportService). Adding a second
487
+ dialog here produced two stacked export dialogs. The grid owns export self-contained. -->
488
+
489
+ <!-- Self-contained Add-to-List dialog (Generic) — owned by this wrapper, never bubbles up. -->
490
+ @if (listManagementConfig) {
491
+ <mj-list-management-dialog
492
+ [Provider]="Provider"
493
+ [visible]="showListManagementDialog"
494
+ [config]="listManagementConfig"
495
+ (complete)="onListManagementComplete($event)"
496
+ (cancel)="onListManagementCancel()"
497
+ >
498
+ </mj-list-management-dialog>
499
+ }
500
+
501
+ <!-- Self-contained Delete confirmation (Generic) — owned by this wrapper, never bubbles up. -->
502
+ <mj-ev-confirm-dialog
503
+ [IsOpen]="showDeleteConfirm"
504
+ Title="Delete Records"
505
+ [Message]="deleteConfirmMessage"
506
+ DetailMessage="This action cannot be undone."
507
+ ConfirmText="Delete"
508
+ ConfirmStyle="danger"
509
+ Icon="fa-solid fa-trash"
510
+ (Confirmed)="onDeleteConfirmed()"
511
+ (Cancelled)="onDeleteCancelled()"
512
+ >
513
+ </mj-ev-confirm-dialog>
514
+ `, styles: ["\n :host {\n display: block;\n height: 100%;\n }\n "] }]
515
+ }], null, { entity: [{
516
+ type: Input
517
+ }], records: [{
518
+ type: Input
519
+ }], selectedRecordId: [{
520
+ type: Input
521
+ }], filterText: [{
522
+ type: Input
523
+ }], config: [{
524
+ type: Input
525
+ }], totalRecordCount: [{
526
+ type: Input
527
+ }], page: [{
528
+ type: Input
529
+ }], pageSize: [{
530
+ type: Input
531
+ }], isLoading: [{
532
+ type: Input
533
+ }], recordSelected: [{
534
+ type: Output
535
+ }], recordOpened: [{
536
+ type: Output
537
+ }], configChanged: [{
538
+ type: Output
539
+ }], dataRequest: [{
540
+ type: Output
541
+ }], openRelatedRecordRequested: [{
542
+ type: Output
543
+ }], createRecordRequested: [{
544
+ type: Output
545
+ }], grid: [{
546
+ type: ViewChild,
547
+ args: ['grid']
548
+ }] }); })();
549
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(GridViewRendererComponent, { className: "GridViewRendererComponent", filePath: "src/lib/view-types/renderers/grid-view-renderer.component.ts", lineNumber: 182 }); })();
550
+ /**
551
+ * Tree-shaking guard. Force-references this renderer so bundlers (ESBuild/Vite) don't drop the
552
+ * component in builds that only mount it dynamically via the ClassFactory/descriptor. Mirrors the
553
+ * Cards/Cluster renderers' load guards; the parent wires this into a barrel/module load path.
554
+ */
555
+ export function LoadGridViewRenderer() {
556
+ // no-op; presence prevents tree-shaking of this component
557
+ }
558
+ //# sourceMappingURL=grid-view-renderer.component.js.map