@memberjunction/ng-entity-viewer 6.1.4 → 6.2.0-edge.1

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 (81) hide show
  1. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts +18 -2
  2. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts.map +1 -1
  3. package/dist/lib/aggregate-panel/aggregate-panel.component.js +47 -18
  4. package/dist/lib/aggregate-panel/aggregate-panel.component.js.map +1 -1
  5. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.d.ts +22 -0
  6. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.d.ts.map +1 -1
  7. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.js +57 -13
  8. package/dist/lib/aggregate-setup-dialog/aggregate-setup-dialog.component.js.map +1 -1
  9. package/dist/lib/entity-cards/entity-cards.component.d.ts +68 -5
  10. package/dist/lib/entity-cards/entity-cards.component.d.ts.map +1 -1
  11. package/dist/lib/entity-cards/entity-cards.component.js +213 -73
  12. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
  13. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +156 -14
  14. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  15. package/dist/lib/entity-data-grid/entity-data-grid.component.js +432 -138
  16. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  17. package/dist/lib/entity-data-grid/events/grid-events.js +67 -67
  18. package/dist/lib/entity-data-grid/events/grid-events.js.map +1 -1
  19. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +112 -8
  20. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -1
  21. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +255 -69
  22. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
  23. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +11 -0
  24. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  25. package/dist/lib/entity-viewer/entity-viewer.component.js +45 -8
  26. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  27. package/dist/lib/pill/pill.component.d.ts +13 -3
  28. package/dist/lib/pill/pill.component.d.ts.map +1 -1
  29. package/dist/lib/pill/pill.component.js +34 -10
  30. package/dist/lib/pill/pill.component.js.map +1 -1
  31. package/dist/lib/quick-save-dialog/quick-save-dialog.component.d.ts +29 -3
  32. package/dist/lib/quick-save-dialog/quick-save-dialog.component.d.ts.map +1 -1
  33. package/dist/lib/quick-save-dialog/quick-save-dialog.component.js +173 -114
  34. package/dist/lib/quick-save-dialog/quick-save-dialog.component.js.map +1 -1
  35. package/dist/lib/recycle-bin/recycle-bin.component.d.ts +4 -0
  36. package/dist/lib/recycle-bin/recycle-bin.component.d.ts.map +1 -1
  37. package/dist/lib/recycle-bin/recycle-bin.component.js +10 -2
  38. package/dist/lib/recycle-bin/recycle-bin.component.js.map +1 -1
  39. package/dist/lib/utils/aggregate-field.util.d.ts +21 -0
  40. package/dist/lib/utils/aggregate-field.util.d.ts.map +1 -0
  41. package/dist/lib/utils/aggregate-field.util.js +40 -0
  42. package/dist/lib/utils/aggregate-field.util.js.map +1 -0
  43. package/dist/lib/utils/highlight.util.d.ts +6 -0
  44. package/dist/lib/utils/highlight.util.d.ts.map +1 -1
  45. package/dist/lib/utils/highlight.util.js +23 -11
  46. package/dist/lib/utils/highlight.util.js.map +1 -1
  47. package/dist/lib/utils/record.util.d.ts +10 -0
  48. package/dist/lib/utils/record.util.d.ts.map +1 -1
  49. package/dist/lib/utils/record.util.js +20 -4
  50. package/dist/lib/utils/record.util.js.map +1 -1
  51. package/dist/lib/view-config-panel/view-config-panel.component.d.ts +22 -2
  52. package/dist/lib/view-config-panel/view-config-panel.component.d.ts.map +1 -1
  53. package/dist/lib/view-config-panel/view-config-panel.component.js +54 -14
  54. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  55. package/dist/lib/view-selector/view-selector.component.js +2 -2
  56. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts +15 -3
  57. package/dist/lib/view-types/renderers/cards-view-renderer.component.d.ts.map +1 -1
  58. package/dist/lib/view-types/renderers/cards-view-renderer.component.js +33 -5
  59. package/dist/lib/view-types/renderers/cards-view-renderer.component.js.map +1 -1
  60. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +160 -5
  61. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -1
  62. package/dist/lib/view-types/renderers/grid-view-renderer.component.js +576 -36
  63. package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -1
  64. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts +16 -2
  65. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -1
  66. package/dist/lib/view-types/renderers/map-view-renderer.component.js +37 -9
  67. package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -1
  68. package/dist/lib/view-types/view-type.contracts.js.map +1 -1
  69. package/dist/lib/view-workspace/view-workspace.component.d.ts +235 -24
  70. package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -1
  71. package/dist/lib/view-workspace/view-workspace.component.js +600 -174
  72. package/dist/lib/view-workspace/view-workspace.component.js.map +1 -1
  73. package/dist/module.d.ts +5 -4
  74. package/dist/module.d.ts.map +1 -1
  75. package/dist/module.js +8 -1
  76. package/dist/module.js.map +1 -1
  77. package/dist/public-api.d.ts +1 -0
  78. package/dist/public-api.d.ts.map +1 -1
  79. package/dist/public-api.js +1 -0
  80. package/dist/public-api.js.map +1 -1
  81. package/package.json +21 -20
@@ -1,23 +1,92 @@
1
1
  import { Component, Input, Output, EventEmitter, ViewEncapsulation, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
2
- import { LogError, CompositeKey } from '@memberjunction/core';
2
+ import { LogError, CompositeKey, RunView, RecordMergeRequest } from '@memberjunction/core';
3
+ import { RecordComparisonService, } from '@memberjunction/ng-record-merge';
3
4
  import { UUIDsEqual } from '@memberjunction/global';
4
5
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
5
- import { buildPkString, buildCompositeKey } from '../../utils/record.util';
6
+ import { BuildPkString, BuildCompositeKey } from '../../utils/record.util';
6
7
  import * as i0 from "@angular/core";
7
8
  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";
9
+ import * as i2 from "@memberjunction/ng-record-merge";
10
+ import * as i3 from "@memberjunction/ng-ui-components";
11
+ import * as i4 from "../../entity-data-grid/entity-data-grid.component";
12
+ import * as i5 from "../../confirm-dialog/confirm-dialog.component";
10
13
  const _c0 = ["grid"];
11
14
  const _c1 = () => ({});
12
15
  function GridViewRendererComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
13
16
  const _r1 = i0.ɵɵgetCurrentView();
14
- i0.ɵɵelementStart(0, "mj-list-management-dialog", 4);
17
+ i0.ɵɵelementStart(0, "mj-list-management-dialog", 6);
15
18
  i0.ɵɵlistener("complete", function GridViewRendererComponent_Conditional_2_Template_mj_list_management_dialog_complete_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onListManagementComplete($event)); })("cancel", function GridViewRendererComponent_Conditional_2_Template_mj_list_management_dialog_cancel_0_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onListManagementCancel()); });
16
19
  i0.ɵɵelementEnd();
17
20
  } if (rf & 2) {
18
21
  const ctx_r1 = i0.ɵɵnextContext();
19
22
  i0.ɵɵproperty("Provider", ctx_r1.Provider)("visible", ctx_r1.showListManagementDialog)("config", ctx_r1.listManagementConfig);
20
23
  } }
24
+ function GridViewRendererComponent_Conditional_4_Conditional_11_Template(rf, ctx) { if (rf & 1) {
25
+ i0.ɵɵelementStart(0, "p", 12);
26
+ i0.ɵɵtext(1);
27
+ i0.ɵɵelementEnd();
28
+ } if (rf & 2) {
29
+ const ctx_r1 = i0.ɵɵnextContext(2);
30
+ i0.ɵɵadvance();
31
+ i0.ɵɵtextInterpolate(ctx_r1.mergeState.DependencyNote);
32
+ } }
33
+ function GridViewRendererComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
34
+ const _r3 = i0.ɵɵgetCurrentView();
35
+ i0.ɵɵelementStart(0, "mj-dialog", 7);
36
+ i0.ɵɵlistener("Close", function GridViewRendererComponent_Conditional_4_Template_mj_dialog_Close_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMergeCancelled()); });
37
+ i0.ɵɵelementStart(1, "div", 8)(2, "span");
38
+ i0.ɵɵtext(3, "Keep");
39
+ i0.ɵɵelementEnd();
40
+ i0.ɵɵelementStart(4, "button", 9);
41
+ i0.ɵɵlistener("click", function GridViewRendererComponent_Conditional_4_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onSurvivorChange("left")); });
42
+ i0.ɵɵtext(5);
43
+ i0.ɵɵelementEnd();
44
+ i0.ɵɵelementStart(6, "button", 9);
45
+ i0.ɵɵlistener("click", function GridViewRendererComponent_Conditional_4_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onSurvivorChange("right")); });
46
+ i0.ɵɵtext(7);
47
+ i0.ɵɵelementEnd();
48
+ i0.ɵɵelementStart(8, "span", 10);
49
+ i0.ɵɵtext(9);
50
+ i0.ɵɵelementEnd()();
51
+ i0.ɵɵelementStart(10, "mj-record-merge-panel", 11);
52
+ i0.ɵɵlistener("MergeConfirmed", function GridViewRendererComponent_Conditional_4_Template_mj_record_merge_panel_MergeConfirmed_10_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMergeConfirmed($event)); })("MergeCancelled", function GridViewRendererComponent_Conditional_4_Template_mj_record_merge_panel_MergeCancelled_10_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onMergeCancelled()); });
53
+ i0.ɵɵelementEnd();
54
+ i0.ɵɵconditionalCreate(11, GridViewRendererComponent_Conditional_4_Conditional_11_Template, 2, 1, "p", 12);
55
+ i0.ɵɵelementEnd();
56
+ } if (rf & 2) {
57
+ const ctx_r1 = i0.ɵɵnextContext();
58
+ i0.ɵɵproperty("Visible", true)("Closeable", !ctx_r1.mergeState.IsMerging);
59
+ i0.ɵɵadvance(4);
60
+ i0.ɵɵproperty("variant", ctx_r1.mergeState.Config.SurvivorSide === "left" ? "primary" : "secondary")("disabled", ctx_r1.mergeState.IsMerging);
61
+ i0.ɵɵattribute("aria-checked", ctx_r1.mergeState.Config.SurvivorSide === "left");
62
+ i0.ɵɵadvance();
63
+ i0.ɵɵtextInterpolate(ctx_r1.mergeState.Config.LeftLabel);
64
+ i0.ɵɵadvance();
65
+ i0.ɵɵproperty("variant", ctx_r1.mergeState.Config.SurvivorSide === "right" ? "primary" : "secondary")("disabled", ctx_r1.mergeState.IsMerging);
66
+ i0.ɵɵattribute("aria-checked", ctx_r1.mergeState.Config.SurvivorSide === "right");
67
+ i0.ɵɵadvance();
68
+ i0.ɵɵtextInterpolate(ctx_r1.mergeState.Config.RightLabel);
69
+ i0.ɵɵadvance(2);
70
+ i0.ɵɵtextInterpolate1("", ctx_r1.mergeLoserLabel, " will be deleted; its linked records move to the record you keep.");
71
+ i0.ɵɵadvance();
72
+ i0.ɵɵproperty("Fields", ctx_r1.mergeState.Fields)("Config", ctx_r1.mergeState.Config)("MergeEnabled", !ctx_r1.mergeState.IsMerging)("IsMerging", ctx_r1.mergeState.IsMerging);
73
+ i0.ɵɵadvance();
74
+ i0.ɵɵconditional(ctx_r1.mergeState.DependencyNote ? 11 : -1);
75
+ } }
76
+ function GridViewRendererComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
77
+ const _r4 = i0.ɵɵgetCurrentView();
78
+ i0.ɵɵelementStart(0, "div", 5)(1, "span", 13);
79
+ i0.ɵɵtext(2);
80
+ i0.ɵɵelementEnd();
81
+ i0.ɵɵelementStart(3, "button", 14);
82
+ i0.ɵɵlistener("click", function GridViewRendererComponent_Conditional_5_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onDismissMergeNotice()); });
83
+ i0.ɵɵelement(4, "i", 15);
84
+ i0.ɵɵelementEnd()();
85
+ } if (rf & 2) {
86
+ const ctx_r1 = i0.ɵɵnextContext();
87
+ i0.ɵɵadvance(2);
88
+ i0.ɵɵtextInterpolate(ctx_r1.mergeNotice);
89
+ } }
21
90
  /**
22
91
  * GridViewRendererComponent
23
92
  * -------------------------
@@ -87,7 +156,15 @@ export class GridViewRendererComponent extends BaseAngularComponent {
87
156
  * Optional full-result-set fetcher supplied by the host, forwarded to the grid's ExportDataProvider
88
157
  * so exports cover every matching row rather than just the loaded page (bug C1).
89
158
  */
90
- exportDataProvider = null;
159
+ ExportDataProvider = null;
160
+ /** @deprecated Use {@link ExportDataProvider}. */
161
+ set exportDataProvider(value) {
162
+ this.ExportDataProvider = value;
163
+ }
164
+ /** @deprecated Use {@link ExportDataProvider}. */
165
+ get exportDataProvider() {
166
+ return this.ExportDataProvider;
167
+ }
91
168
  /** Primary-key string of the currently selected record, if any. */
92
169
  selectedRecordId = null;
93
170
  /** Active filter text — passed through for the grid's cell highlighting. */
@@ -175,38 +252,62 @@ export class GridViewRendererComponent extends BaseAngularComponent {
175
252
  // Effective binding accessors — seed grid inputs from config + defaults
176
253
  // ================================================================
177
254
  /** Effective toolbar visibility — defaults to `true` when not set in config. */
178
- get effectiveShowToolbar() {
255
+ get EffectiveShowToolbar() {
179
256
  return this.config.showToolbar ?? true;
180
257
  }
258
+ /** @deprecated Use {@link EffectiveShowToolbar}. */
259
+ get effectiveShowToolbar() {
260
+ return this.EffectiveShowToolbar;
261
+ }
181
262
  /**
182
263
  * Grid search defaults OFF when hosted in entity-viewer. The container already owns
183
264
  * `filterText` ("Filter records..."). Opt in with `toolbarConfig.showSearch: true`.
184
265
  */
185
- get effectiveShowSearch() {
266
+ get EffectiveShowSearch() {
186
267
  return this.config.toolbarConfig?.showSearch ?? false;
187
268
  }
269
+ /** @deprecated Use {@link EffectiveShowSearch}. */
270
+ get effectiveShowSearch() {
271
+ return this.EffectiveShowSearch;
272
+ }
188
273
  /** Effective selection mode — defaults to `'checkbox'` when not set in config. */
189
- get effectiveSelectionMode() {
274
+ get EffectiveSelectionMode() {
190
275
  return this.config.selectionMode ?? 'checkbox';
191
276
  }
277
+ /** @deprecated Use {@link EffectiveSelectionMode}. */
278
+ get effectiveSelectionMode() {
279
+ return this.EffectiveSelectionMode;
280
+ }
192
281
  /** Effective add-to-list button visibility — defaults to `true` when not set in config. */
193
- get effectiveShowAddToListButton() {
282
+ get EffectiveShowAddToListButton() {
194
283
  return this.config.showAddToListButton ?? true;
195
284
  }
285
+ /** @deprecated Use {@link EffectiveShowAddToListButton}. */
286
+ get effectiveShowAddToListButton() {
287
+ return this.EffectiveShowAddToListButton;
288
+ }
196
289
  /** Effective pager visibility — defaults to `true` when not set in config. */
197
- get effectiveShowPager() {
290
+ get EffectiveShowPager() {
198
291
  return this.config.showPager ?? true;
199
292
  }
293
+ /** @deprecated Use {@link EffectiveShowPager}. */
294
+ get effectiveShowPager() {
295
+ return this.EffectiveShowPager;
296
+ }
200
297
  /** Effective page size — config wins, else the generic `pageSize` input, else the grid's own default. */
201
- get effectivePageSize() {
298
+ get EffectivePageSize() {
202
299
  return this.config.pageSize ?? this.pageSize ?? 100;
203
300
  }
301
+ /** @deprecated Use {@link EffectivePageSize}. */
302
+ get effectivePageSize() {
303
+ return this.EffectivePageSize;
304
+ }
204
305
  /**
205
306
  * Effective RunViewParams for the grid. Uses `config.params` when supplied; otherwise builds a
206
307
  * minimal dynamic-view params object from {@link entity}. Returns `null` when no entity is known
207
308
  * yet (the grid renders empty until the host provides one).
208
309
  */
209
- get effectiveParams() {
310
+ get EffectiveParams() {
210
311
  if (this.config.params) {
211
312
  return this.config.params;
212
313
  }
@@ -215,6 +316,10 @@ export class GridViewRendererComponent extends BaseAngularComponent {
215
316
  }
216
317
  return null;
217
318
  }
319
+ /** @deprecated Use {@link EffectiveParams}. */
320
+ get effectiveParams() {
321
+ return this.EffectiveParams;
322
+ }
218
323
  // ================================================================
219
324
  // Navigation + generic coordination output mapping
220
325
  // ================================================================
@@ -222,24 +327,32 @@ export class GridViewRendererComponent extends BaseAngularComponent {
222
327
  * Row single-click → {@link recordSelected}. Extracts the raw record (`event.row`) so the host
223
328
  * builds the composite key itself, matching the other renderers.
224
329
  */
225
- onAfterRowClick(event) {
330
+ OnAfterRowClick(event) {
226
331
  if (event.row) {
227
332
  this.recordSelected.emit(event.row);
228
333
  }
229
334
  }
335
+ /** @deprecated Use {@link OnAfterRowClick}. */
336
+ onAfterRowClick(event) {
337
+ return this.OnAfterRowClick(event);
338
+ }
230
339
  /** Row double-click → {@link recordOpened} (NAVIGATION), emitting the raw record object. */
231
- onAfterRowDoubleClick(event) {
340
+ OnAfterRowDoubleClick(event) {
232
341
  if (event.row) {
233
342
  this.recordOpened.emit(event.row);
234
343
  }
235
344
  }
345
+ /** @deprecated Use {@link OnAfterRowDoubleClick}. */
346
+ onAfterRowDoubleClick(event) {
347
+ return this.OnAfterRowDoubleClick(event);
348
+ }
236
349
  /**
237
350
  * Sort change → both generic coordination channels:
238
351
  * 1. {@link dataRequest} with the generic `sort` shape so the host re-loads with a new OrderBy.
239
352
  * 2. persists the sort into `config.gridState.sortSettings` and emits {@link configChanged} so
240
353
  * the grid's sort survives view reloads via the opaque config channel.
241
354
  */
242
- onAfterSort(event) {
355
+ OnAfterSort(event) {
243
356
  const sort = (event.newSortState ?? []).map((s) => ({ field: s.field, direction: s.direction }));
244
357
  this.dataRequest.emit({ sort });
245
358
  const gridState = { ...(this.config.gridState ?? {}) };
@@ -247,47 +360,71 @@ export class GridViewRendererComponent extends BaseAngularComponent {
247
360
  this.config = { ...this.config, gridState };
248
361
  this.configChanged.emit(this.config);
249
362
  }
363
+ /** @deprecated Use {@link OnAfterSort}. */
364
+ onAfterSort(event) {
365
+ return this.OnAfterSort(event);
366
+ }
250
367
  /**
251
368
  * Generic grid-state change (column resize / reorder / visibility) → merge the grid's updated
252
369
  * {@link ViewGridState} into the opaque config and emit {@link configChanged}; the host persists
253
370
  * the blob verbatim. No `dataRequest` here — column changes don't alter the loaded record set.
254
371
  */
255
- onGridStateChanged(event) {
372
+ OnGridStateChanged(event) {
256
373
  this.config = { ...this.config, gridState: event.gridState };
257
374
  this.configChanged.emit(this.config);
258
375
  }
376
+ /** @deprecated Use {@link OnGridStateChanged}. */
377
+ onGridStateChanged(event) {
378
+ return this.OnGridStateChanged(event);
379
+ }
259
380
  /**
260
381
  * Selection change → kept INTERNAL. The grid drives its own add-to-list from the selection, so the
261
382
  * wrapper has no need to surface it. We intentionally do not bubble selection anywhere.
262
383
  */
263
- onSelectionChange(_selectedKeys) {
384
+ OnSelectionChange(_selectedKeys) {
264
385
  // No-op: selection is internal to the grid + this wrapper's add-to-list. It never bubbles up.
265
386
  }
387
+ /** @deprecated Use {@link OnSelectionChange}. */
388
+ onSelectionChange(_selectedKeys) {
389
+ return this.OnSelectionChange(_selectedKeys);
390
+ }
266
391
  /**
267
392
  * Foreign-key link click → {@link openRelatedRecordRequested} (NAVIGATION). Maps the grid's
268
393
  * {@link ForeignKeyClickEvent} (which carries the related entity + the FK value) onto the generic
269
394
  * {@link ViewRelatedRecordNavigation} the container forwards to the outer app for routing. The
270
395
  * related entity name is taken from the event when present, else resolved from the metadata by ID.
271
396
  */
272
- onForeignKeyClick(event) {
397
+ OnForeignKeyClick(event) {
273
398
  const entityName = event.relatedEntityName ?? this.resolveEntityNameById(event.relatedEntityId);
274
399
  if (!entityName) {
275
400
  return;
276
401
  }
277
402
  this.openRelatedRecordRequested.emit({ entityName, recordKey: event.recordId });
278
403
  }
404
+ /** @deprecated Use {@link OnForeignKeyClick}. */
405
+ onForeignKeyClick(event) {
406
+ return this.OnForeignKeyClick(event);
407
+ }
279
408
  /** New button → {@link createRecordRequested} (NAVIGATION) — opening the create form is routing. */
280
- onNewButtonClick() {
409
+ OnNewButtonClick() {
281
410
  this.createRecordRequested.emit();
282
411
  }
412
+ /** @deprecated Use {@link OnNewButtonClick}. */
413
+ onNewButtonClick() {
414
+ return this.OnNewButtonClick();
415
+ }
283
416
  /**
284
417
  * Pager navigation → generic {@link dataRequest}. Maps the grid's {@link PageChangeEvent}
285
418
  * (`PageNumber` is already 1-based, `PageSize`) onto the generic `{ page, pageSize }` shape the
286
419
  * host honors against its own RunView.
287
420
  */
288
- onPageChange(event) {
421
+ OnPageChange(event) {
289
422
  this.dataRequest.emit({ page: event.PageNumber, pageSize: event.PageSize });
290
423
  }
424
+ /** @deprecated Use {@link OnPageChange}. */
425
+ onPageChange(event) {
426
+ return this.OnPageChange(event);
427
+ }
291
428
  // ================================================================
292
429
  // Self-contained: Refresh
293
430
  // ================================================================
@@ -296,9 +433,13 @@ export class GridViewRendererComponent extends BaseAngularComponent {
296
433
  * generic container ↔ plug-in coordination (a data-access request), NOT a feature event surfaced
297
434
  * to the outer app.
298
435
  */
299
- onRefreshButtonClick() {
436
+ OnRefreshButtonClick() {
300
437
  this.dataRequest.emit(this.currentPageDataRequest());
301
438
  }
439
+ /** @deprecated Use {@link OnRefreshButtonClick}. */
440
+ onRefreshButtonClick() {
441
+ return this.OnRefreshButtonClick();
442
+ }
302
443
  // ================================================================
303
444
  // Export: handled entirely by mj-entity-data-grid (its onExportClick opens its own export dialog
304
445
  // via ExportService). This wrapper intentionally does NOT host an export dialog — doing so produced
@@ -313,7 +454,7 @@ export class GridViewRendererComponent extends BaseAngularComponent {
313
454
  * changes itself — nothing bubbles up. Mirrors the config construction the legacy host did in
314
455
  * `data-explorer-dashboard`'s `onAddToListRequested()`.
315
456
  */
316
- onAddToListRequested(event) {
457
+ OnAddToListRequested(event) {
317
458
  const entity = event.entityInfo ?? this.entity;
318
459
  if (!entity || !event.records || event.records.length === 0) {
319
460
  return;
@@ -339,18 +480,30 @@ export class GridViewRendererComponent extends BaseAngularComponent {
339
480
  this.showListManagementDialog = true;
340
481
  this.cdr.detectChanges();
341
482
  }
483
+ /** @deprecated Use {@link OnAddToListRequested}. */
484
+ onAddToListRequested(event) {
485
+ return this.OnAddToListRequested(event);
486
+ }
342
487
  /** List-management dialog completed (membership changes applied) → tear down state. */
343
- onListManagementComplete(_result) {
488
+ OnListManagementComplete(_result) {
344
489
  this.showListManagementDialog = false;
345
490
  this.listManagementConfig = null;
346
491
  this.cdr.detectChanges();
347
492
  }
493
+ /** @deprecated Use {@link OnListManagementComplete}. */
494
+ onListManagementComplete(_result) {
495
+ return this.OnListManagementComplete(_result);
496
+ }
348
497
  /** List-management dialog cancelled → tear down state. */
349
- onListManagementCancel() {
498
+ OnListManagementCancel() {
350
499
  this.showListManagementDialog = false;
351
500
  this.listManagementConfig = null;
352
501
  this.cdr.detectChanges();
353
502
  }
503
+ /** @deprecated Use {@link OnListManagementCancel}. */
504
+ onListManagementCancel() {
505
+ return this.OnListManagementCancel();
506
+ }
354
507
  /**
355
508
  * Resolve a human-friendly display name for a record using the entity's NameField, falling back to
356
509
  * the composite-key string. Mirrors the legacy host's display-name resolution.
@@ -362,7 +515,7 @@ export class GridViewRendererComponent extends BaseAngularComponent {
362
515
  return String(nameValue);
363
516
  }
364
517
  }
365
- return buildPkString(record, entity) || 'Unknown';
518
+ return BuildPkString(record, entity) || 'Unknown';
366
519
  }
367
520
  /**
368
521
  * Build the record-id strings used by list membership matching, in the compact form `MJ: List Details`
@@ -385,7 +538,7 @@ export class GridViewRendererComponent extends BaseAngularComponent {
385
538
  * Delete button → stage the records and open the Generic confirm dialog. The actual delete happens
386
539
  * in {@link onDeleteConfirmed} after the user confirms — nothing bubbles up.
387
540
  */
388
- onDeleteButtonClick(records) {
541
+ OnDeleteButtonClick(records) {
389
542
  if (!records || records.length === 0) {
390
543
  return;
391
544
  }
@@ -395,13 +548,17 @@ export class GridViewRendererComponent extends BaseAngularComponent {
395
548
  this.showDeleteConfirm = true;
396
549
  this.cdr.detectChanges();
397
550
  }
551
+ /** @deprecated Use {@link OnDeleteButtonClick}. */
552
+ onDeleteButtonClick(records) {
553
+ return this.OnDeleteButtonClick(records);
554
+ }
398
555
  /**
399
556
  * Delete confirmed → delete each staged record through the MJ entity layer
400
557
  * (`ProviderToUse.GetEntityObject(name, compositeKey, user)` → `Delete`), checking the boolean
401
558
  * result and surfacing failures via {@link LogError}. After deletion, re-request the current page
402
559
  * so the host reloads. Self-contained — no feature event leaves the wrapper.
403
560
  */
404
- async onDeleteConfirmed() {
561
+ async OnDeleteConfirmed() {
405
562
  this.showDeleteConfirm = false;
406
563
  const entity = this.entity;
407
564
  const records = this.pendingDeleteRecords;
@@ -413,7 +570,7 @@ export class GridViewRendererComponent extends BaseAngularComponent {
413
570
  const provider = this.ProviderToUse;
414
571
  const user = provider.CurrentUser;
415
572
  for (const record of records) {
416
- const key = buildCompositeKey(record, entity);
573
+ const key = BuildCompositeKey(record, entity);
417
574
  try {
418
575
  // The (name, key, user) overload instantiates AND loads in one call (throws if it can't load).
419
576
  const obj = await provider.GetEntityObject(entity.Name, key, user);
@@ -435,12 +592,344 @@ export class GridViewRendererComponent extends BaseAngularComponent {
435
592
  }
436
593
  this.cdr.detectChanges();
437
594
  }
595
+ /** @deprecated Use {@link OnDeleteConfirmed}. */
596
+ async onDeleteConfirmed() {
597
+ return this.OnDeleteConfirmed();
598
+ }
438
599
  /** Delete cancelled → discard the staged records and close the confirm dialog. */
439
- onDeleteCancelled() {
600
+ OnDeleteCancelled() {
440
601
  this.showDeleteConfirm = false;
441
602
  this.pendingDeleteRecords = [];
442
603
  this.cdr.detectChanges();
443
604
  }
605
+ /** @deprecated Use {@link OnDeleteCancelled}. */
606
+ onDeleteCancelled() {
607
+ return this.OnDeleteCancelled();
608
+ }
609
+ // ================================================================
610
+ // Record merge
611
+ // ================================================================
612
+ /**
613
+ * The in-flight merge, or null when the panel is closed. Holds everything the panel needs
614
+ * plus the dependency preview, so the panel itself stays a pure presentation component.
615
+ */
616
+ mergeState = null;
617
+ /** A one-line status shown under the grid — why a merge was refused, or how one ended. */
618
+ mergeNotice = null;
619
+ /** Auto-dismiss timer for {@link mergeNotice}; cleared whenever the notice is replaced. */
620
+ mergeNoticeTimer = null;
621
+ /**
622
+ * How long a merge notice stays up before dismissing itself. The banner is fixed-position over
623
+ * the whole application, so leaving it until the next merge pins it across navigation and every
624
+ * other record the user visits afterwards.
625
+ */
626
+ static MERGE_NOTICE_TIMEOUT_MS = 8000;
627
+ comparison = inject(RecordComparisonService);
628
+ ngOnDestroy() {
629
+ // The notice's auto-dismiss outlives the view otherwise, and fires detectChanges on a
630
+ // destroyed component.
631
+ if (this.mergeNoticeTimer) {
632
+ clearTimeout(this.mergeNoticeTimer);
633
+ this.mergeNoticeTimer = null;
634
+ }
635
+ }
636
+ /** Dismiss the merge notice from its own close button. */
637
+ OnDismissMergeNotice() {
638
+ this.setMergeNotice(null);
639
+ this.cdr.detectChanges();
640
+ }
641
+ /** @deprecated Use {@link OnDismissMergeNotice}. */
642
+ onDismissMergeNotice() {
643
+ return this.OnDismissMergeNotice();
644
+ }
645
+ /**
646
+ * Set (or clear) the merge notice, replacing any pending auto-dismiss. Every write to
647
+ * `mergeNotice` goes through here so a notice cannot outlive its timer or strand an old one.
648
+ */
649
+ setMergeNotice(message) {
650
+ if (this.mergeNoticeTimer) {
651
+ clearTimeout(this.mergeNoticeTimer);
652
+ this.mergeNoticeTimer = null;
653
+ }
654
+ this.mergeNotice = message;
655
+ if (message) {
656
+ this.mergeNoticeTimer = setTimeout(() => {
657
+ this.mergeNoticeTimer = null;
658
+ this.mergeNotice = null;
659
+ this.cdr.detectChanges();
660
+ }, GridViewRendererComponent.MERGE_NOTICE_TIMEOUT_MS);
661
+ }
662
+ }
663
+ /**
664
+ * Show the Merge button only where merging can actually succeed: the entity opts in and the
665
+ * user can both update the survivor and delete the loser.
666
+ */
667
+ get effectiveShowMergeButton() {
668
+ const entity = this.entity;
669
+ const user = this.ProviderToUse?.CurrentUser;
670
+ if (!entity?.AllowRecordMerge || !user)
671
+ return false;
672
+ const permissions = entity.GetUserPermisions(user);
673
+ return !!permissions && permissions.CanUpdate && permissions.CanDelete;
674
+ }
675
+ /** The record that will be deleted, named so the user sees the choice before confirming. */
676
+ get mergeLoserLabel() {
677
+ const config = this.mergeState?.Config;
678
+ if (!config)
679
+ return '';
680
+ return config.SurvivorSide === 'left' ? config.RightLabel : config.LeftLabel;
681
+ }
682
+ /**
683
+ * The grid asked to merge the selected rows. Compare them, preview what will move, and open
684
+ * the panel — nothing is written until the user confirms.
685
+ */
686
+ async OnMergeRequested(event) {
687
+ this.setMergeNotice(null);
688
+ try {
689
+ await this.prepareMerge(event);
690
+ }
691
+ catch (err) {
692
+ // A transport failure in any pre-flight query must not leave the user with nothing.
693
+ this.setMergeNotice(`Could not prepare the merge: ${err instanceof Error ? err.message : String(err)}`);
694
+ this.cdr.detectChanges();
695
+ }
696
+ }
697
+ /** @deprecated Use {@link OnMergeRequested}. */
698
+ async onMergeRequested(event) {
699
+ return this.OnMergeRequested(event);
700
+ }
701
+ /** The user chose which record survives; differing fields default to that record's values. */
702
+ OnSurvivorChange(side) {
703
+ if (!this.mergeState || this.mergeState.IsMerging)
704
+ return;
705
+ this.mergeState.Config = { ...this.mergeState.Config, SurvivorSide: side };
706
+ for (const field of this.mergeState.Fields) {
707
+ if (!field.IsReadOnly)
708
+ field.SelectedSide = side;
709
+ }
710
+ this.cdr.detectChanges();
711
+ }
712
+ /** @deprecated Use {@link OnSurvivorChange}. */
713
+ onSurvivorChange(side) {
714
+ return this.OnSurvivorChange(side);
715
+ }
716
+ async prepareMerge(event) {
717
+ const entity = event.entityInfo ?? this.entity;
718
+ if (!entity)
719
+ return;
720
+ // The merge panel is two-sided by construction, so n > 2 has nowhere to render.
721
+ if (event.records.length !== 2) {
722
+ this.setMergeNotice('Select exactly two records to merge.');
723
+ this.cdr.detectChanges();
724
+ return;
725
+ }
726
+ // Composite-key safe: the grid renders arbitrary entities, so never assume one key column.
727
+ const keys = event.records.map(r => CompositeKey.FromEntityRecord(entity, r));
728
+ if (keys.some(k => k.KeyValuePairs.length === 0 || k.KeyValuePairs.some(kv => kv.Value == null || kv.Value === ''))) {
729
+ this.setMergeNotice('Could not read the primary key of the selected records.');
730
+ this.cdr.detectChanges();
731
+ return;
732
+ }
733
+ // The merge panel's config is string-keyed, so keys travel through it as compact segments.
734
+ const [left, right] = keys.map(k => k.ToCompactURLSegment());
735
+ // Subtype records: the merge re-points only the keys that target this entity, then the loser's
736
+ // delete follows the shared key into the parent row, whose own references never moved.
737
+ if (entity.ParentID) {
738
+ this.setMergeNotice(`${entity.DisplayName} records extend a parent type; merging subtype records is not supported yet.`);
739
+ this.cdr.detectChanges();
740
+ return;
741
+ }
742
+ const childRows = await this.hasIsAChildRows(entity, keys);
743
+ if (childRows === null) {
744
+ this.setMergeNotice('Could not verify whether the selected records have subtype rows, so the merge was not started.');
745
+ this.cdr.detectChanges();
746
+ return;
747
+ }
748
+ if (childRows) {
749
+ this.setMergeNotice(`${entity.DisplayName} records that another app extends (shared-key subtype rows) cannot be merged yet — ` +
750
+ 'the merge would collide on the shared key. Merge them from the extending app instead.');
751
+ this.cdr.detectChanges();
752
+ return;
753
+ }
754
+ const result = await this.comparison.GetRecordComparison({
755
+ EntityName: entity.Name,
756
+ Keys: keys.map(k => ({
757
+ KeyValuePairs: k.KeyValuePairs.map(kv => ({ FieldName: kv.FieldName, Value: String(kv.Value) })),
758
+ })),
759
+ }, this.ProviderToUse);
760
+ if (!result.Success || !result.Output) {
761
+ this.setMergeNotice(result.ErrorMessage ?? 'Could not compare the selected records.');
762
+ this.cdr.detectChanges();
763
+ return;
764
+ }
765
+ this.mergeState = {
766
+ Config: {
767
+ EntityName: entity.Name,
768
+ LeftRecordID: left,
769
+ RightRecordID: right,
770
+ SurvivorSide: 'left',
771
+ LeftLabel: this.labelFor(event.records[0], entity, keys[0]),
772
+ RightLabel: this.labelFor(event.records[1], entity, keys[1]),
773
+ },
774
+ Fields: result.Output.Fields.map(delta => ({
775
+ FieldName: delta.FieldName,
776
+ DisplayLabel: delta.DisplayName,
777
+ LeftValue: delta.Cells[0]?.Value,
778
+ RightValue: delta.Cells[1]?.Value,
779
+ HasConflict: delta.Differs,
780
+ SelectedSide: 'left',
781
+ // What the ORM will refuse to write — keys, AllowUpdateAPI = 0, timestamps. A field the
782
+ // metadata does not know cannot be written either.
783
+ IsReadOnly: entity.FieldByName(delta.FieldName)?.ReadOnly ?? true,
784
+ DataType: 'string',
785
+ })),
786
+ IsMerging: false,
787
+ DependencyNote: await this.describeDependencies(entity, keys),
788
+ Entity: entity,
789
+ Keys: keys,
790
+ };
791
+ this.cdr.detectChanges();
792
+ }
793
+ /** The user confirmed: merge, then reload the page so the loser disappears from the grid. */
794
+ async OnMergeConfirmed(event) {
795
+ if (!this.mergeState)
796
+ return;
797
+ this.mergeState.IsMerging = true;
798
+ this.cdr.detectChanges();
799
+ const survivorIsLeft = event.Config.SurvivorSide === 'left';
800
+ const [leftKey, rightKey] = this.mergeState.Keys;
801
+ const survivor = survivorIsLeft ? leftKey : rightKey;
802
+ const loser = survivorIsLeft ? rightKey : leftKey;
803
+ const request = new RecordMergeRequest();
804
+ request.EntityName = event.Config.EntityName;
805
+ request.SurvivingRecordCompositeKey = survivor;
806
+ request.RecordsToMerge = [loser];
807
+ request.FieldMap = this.buildFieldMap(event, survivorIsLeft);
808
+ try {
809
+ const result = await this.ProviderToUse.MergeRecords(request);
810
+ this.mergeState = null;
811
+ this.setMergeNotice(result.Success ? 'Records merged.' : `Merge failed: ${result.OverallStatus}`);
812
+ if (result.Success) {
813
+ this.dataRequest.emit(this.currentPageDataRequest());
814
+ }
815
+ }
816
+ catch (err) {
817
+ this.mergeState = null;
818
+ this.setMergeNotice(`Merge failed: ${err instanceof Error ? err.message : String(err)}`);
819
+ }
820
+ this.cdr.detectChanges();
821
+ }
822
+ /** @deprecated Use {@link OnMergeConfirmed}. */
823
+ async onMergeConfirmed(event) {
824
+ return this.OnMergeConfirmed(event);
825
+ }
826
+ /** Merge cancelled → close the panel, write nothing. */
827
+ OnMergeCancelled() {
828
+ this.mergeState = null;
829
+ this.cdr.detectChanges();
830
+ }
831
+ /** @deprecated Use {@link OnMergeCancelled}. */
832
+ onMergeCancelled() {
833
+ return this.OnMergeCancelled();
834
+ }
835
+ /**
836
+ * Fields whose resolution has to travel, and only those: the survivor already holds the value
837
+ * for any field resolved to its own side.
838
+ *
839
+ * The distinction that matters is "the user did not change this" versus "the user chose the
840
+ * blank side". Both arrive as an absent value, so deciding from the value alone drops a real
841
+ * resolution — the survivor silently keeps its old value on the one operation where someone is
842
+ * reconciling two records field by field, and the loser is gone afterwards. So the skip is
843
+ * decided from WHICH SIDE was chosen, and a chosen blank is written through as null.
844
+ */
845
+ buildFieldMap(event, survivorIsLeft) {
846
+ const map = [];
847
+ for (const field of event.ResolvedFields) {
848
+ if (field.IsReadOnly || !field.HasConflict)
849
+ continue;
850
+ // Resolved to the survivor's own side — nothing to write, whatever the value is.
851
+ if (field.SelectedSide !== 'custom' && (field.SelectedSide === 'left') === survivorIsLeft)
852
+ continue;
853
+ const chosen = field.SelectedSide === 'custom'
854
+ ? field.CustomValue
855
+ : (survivorIsLeft ? field.RightValue : field.LeftValue);
856
+ // A chosen blank is a clear, not a no-op. Normalize undefined to null so the transport
857
+ // carries an explicit value rather than an absent one.
858
+ map.push({ FieldName: field.FieldName, Value: chosen === undefined ? null : chosen });
859
+ }
860
+ return map;
861
+ }
862
+ /**
863
+ * True when any of these records has a row in an IS-A child entity, null when that could not be
864
+ * determined (a child view the user cannot read answers `Success: false`, which must not pass
865
+ * for "no rows"). `MergeRecords` has no subtype handling: the child rows share the parent's key,
866
+ * so merging would collide. Refuse rather than half-merge.
867
+ */
868
+ async hasIsAChildRows(entity, keys) {
869
+ const children = entity.ChildEntities;
870
+ if (children.length === 0)
871
+ return false;
872
+ // An IS-A child shares its parent's key SEMANTICALLY; nothing requires it to share the column
873
+ // NAME. `CompositeKey.ToWhereClause()` emits the parent's names, so filtering a child view
874
+ // with it fails wherever the names differ — and a failed probe refuses the merge, which would
875
+ // disable merge for that entity permanently. Pair each key's values onto the child's own PK
876
+ // columns instead, the way the platform's own child discovery does
877
+ // (SQLServerDataProvider.BuildChildDiscoverySQL).
878
+ const probes = children
879
+ .map(child => ({ child, filter: this.buildChildKeyFilter(child, keys) }))
880
+ .filter((probe) => probe.filter !== null);
881
+ if (probes.length === 0)
882
+ return false;
883
+ const results = await RunView.FromMetadataProvider(this.ProviderToUse).RunViews(probes.map(({ child, filter }) => ({
884
+ EntityName: child.Name,
885
+ ExtraFilter: filter,
886
+ Fields: child.PrimaryKeys.map(f => f.Name),
887
+ ResultType: 'simple',
888
+ MaxRows: 1,
889
+ })));
890
+ if (results.some(r => !r.Success))
891
+ return null;
892
+ return results.some(r => r.Results.length > 0);
893
+ }
894
+ /**
895
+ * `(childPk = v1) OR (childPk = v2)` against one child entity, built from the parent's key
896
+ * VALUES and the child's own key COLUMN NAMES, paired positionally because an IS-A child shares
897
+ * its parent's key by design. Null when the child's key arity differs, which means it does not
898
+ * share the key and cannot hold a subtype row for these records.
899
+ */
900
+ buildChildKeyFilter(child, keys) {
901
+ const childPkFields = child.PrimaryKeys;
902
+ if (childPkFields.length === 0 || keys.some(k => k.KeyValuePairs.length !== childPkFields.length)) {
903
+ return null;
904
+ }
905
+ const clauses = keys.map(key => childPkFields
906
+ .map((field, i) => `[${field.Name}] = '${String(key.KeyValuePairs[i].Value ?? '').replace(/'/g, "''")}'`)
907
+ .join(' AND '));
908
+ return clauses.map(c => `(${c})`).join(' OR ');
909
+ }
910
+ /**
911
+ * How many linked records will move to the survivor — the thing users most want to know.
912
+ *
913
+ * Indexes `counts[0]` / `counts[1]` directly: `onMergeRequested` refuses anything other than
914
+ * exactly two records before reaching here, and the merge panel's config is two-sided by
915
+ * construction, so this is only ever called with a pair. Widen both together.
916
+ */
917
+ async describeDependencies(entity, keys) {
918
+ try {
919
+ const counts = await Promise.all(keys.map(key => this.ProviderToUse.GetRecordDependencies(entity.Name, key)));
920
+ return `Linked records that would move to the survivor: ${counts[0].length} on the left, ${counts[1].length} on the right.`;
921
+ }
922
+ catch (err) {
923
+ LogError(`Could not load merge dependencies for ${entity.Name}: ${err instanceof Error ? err.message : String(err)}`);
924
+ return null;
925
+ }
926
+ }
927
+ /** A human label for a row — its name field when it has one, else its key. */
928
+ labelFor(row, entity, key) {
929
+ const nameField = entity.NameField?.Name;
930
+ const name = nameField ? String(row[nameField] ?? '') : '';
931
+ return name || key.ToCompactURLSegment();
932
+ }
444
933
  // ================================================================
445
934
  // Helpers
446
935
  // ================================================================
@@ -473,21 +962,27 @@ export class GridViewRendererComponent extends BaseAngularComponent {
473
962
  } if (rf & 2) {
474
963
  let _t;
475
964
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.grid = _t.first);
476
- } }, inputs: { entity: "entity", records: "records", exportDataProvider: "exportDataProvider", selectedRecordId: "selectedRecordId", filterText: "filterText", config: "config", AutoLoadEntityActions: "AutoLoadEntityActions", totalRecordCount: "totalRecordCount", page: "page", pageSize: "pageSize", isLoading: "isLoading" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", configChanged: "configChanged", dataRequest: "dataRequest", configureRequested: "configureRequested", openRelatedRecordRequested: "openRelatedRecordRequested", createRecordRequested: "createRecordRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 4, vars: 22, consts: [["grid", ""], [3, "AfterRowClick", "AfterRowDoubleClick", "AfterSort", "GridStateChanged", "SelectionChange", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "AddToListRequested", "ForeignKeyClick", "PageChange", "ManageColumnsRequested", "Provider", "Data", "ExportDataProvider", "Params", "FilterText", "GridState", "Height", "AllowLoad", "AutoLoadEntityActions", "ShowToolbar", "ShowSearch", "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) {
965
+ } }, inputs: { entity: "entity", records: "records", ExportDataProvider: "ExportDataProvider", exportDataProvider: "exportDataProvider", selectedRecordId: "selectedRecordId", filterText: "filterText", config: "config", AutoLoadEntityActions: "AutoLoadEntityActions", totalRecordCount: "totalRecordCount", page: "page", pageSize: "pageSize", isLoading: "isLoading" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", configChanged: "configChanged", dataRequest: "dataRequest", configureRequested: "configureRequested", openRelatedRecordRequested: "openRelatedRecordRequested", createRecordRequested: "createRecordRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 6, vars: 25, consts: [["grid", ""], [3, "AfterRowClick", "AfterRowDoubleClick", "AfterSort", "GridStateChanged", "SelectionChange", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "AddToListRequested", "ForeignKeyClick", "PageChange", "ManageColumnsRequested", "MergeRecordsRequested", "Provider", "Data", "ExportDataProvider", "Params", "FilterText", "GridState", "Height", "AllowLoad", "AutoLoadEntityActions", "ShowToolbar", "ShowSearch", "ToolbarConfig", "SelectionMode", "ShowAddToListButton", "ShowPager", "PageSize", "TotalRowCount", "PagerPageNumber", "ShowMergeButton"], [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"], ["Size", "lg", 3, "Visible", "Closeable"], ["role", "status", 1, "mj-ev-merge-notice"], [3, "complete", "cancel", "Provider", "visible", "config"], ["Size", "lg", 3, "Close", "Visible", "Closeable"], ["role", "radiogroup", "aria-label", "Record to keep", 1, "mj-ev-merge-survivor"], ["type", "button", "mjButton", "", "size", "sm", "role", "radio", 3, "click", "variant", "disabled"], [1, "mj-ev-merge-survivor-note"], [3, "MergeConfirmed", "MergeCancelled", "Fields", "Config", "MergeEnabled", "IsMerging"], [1, "mj-ev-merge-deps"], [1, "mj-ev-merge-notice-text"], ["type", "button", "title", "Dismiss", "aria-label", "Dismiss", 1, "mj-ev-merge-notice-dismiss", 3, "click"], [1, "fa-solid", "fa-xmark"]], template: function GridViewRendererComponent_Template(rf, ctx) { if (rf & 1) {
477
966
  i0.ɵɵelementStart(0, "mj-entity-data-grid", 1, 0);
478
- i0.ɵɵlistener("AfterRowClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowClick_0_listener($event) { return ctx.onAfterRowClick($event); })("AfterRowDoubleClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_0_listener($event) { return ctx.onAfterRowDoubleClick($event); })("AfterSort", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterSort_0_listener($event) { return ctx.onAfterSort($event); })("GridStateChanged", function GridViewRendererComponent_Template_mj_entity_data_grid_GridStateChanged_0_listener($event) { return ctx.onGridStateChanged($event); })("SelectionChange", function GridViewRendererComponent_Template_mj_entity_data_grid_SelectionChange_0_listener($event) { return ctx.onSelectionChange($event); })("NewButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_NewButtonClick_0_listener() { return ctx.onNewButtonClick(); })("RefreshButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_RefreshButtonClick_0_listener() { return ctx.onRefreshButtonClick(); })("DeleteButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_DeleteButtonClick_0_listener($event) { return ctx.onDeleteButtonClick($event); })("AddToListRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_AddToListRequested_0_listener($event) { return ctx.onAddToListRequested($event); })("ForeignKeyClick", function GridViewRendererComponent_Template_mj_entity_data_grid_ForeignKeyClick_0_listener($event) { return ctx.onForeignKeyClick($event); })("PageChange", function GridViewRendererComponent_Template_mj_entity_data_grid_PageChange_0_listener($event) { return ctx.onPageChange($event); })("ManageColumnsRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_ManageColumnsRequested_0_listener() { return ctx.configureRequested.emit(); });
967
+ i0.ɵɵlistener("AfterRowClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowClick_0_listener($event) { return ctx.onAfterRowClick($event); })("AfterRowDoubleClick", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_0_listener($event) { return ctx.onAfterRowDoubleClick($event); })("AfterSort", function GridViewRendererComponent_Template_mj_entity_data_grid_AfterSort_0_listener($event) { return ctx.onAfterSort($event); })("GridStateChanged", function GridViewRendererComponent_Template_mj_entity_data_grid_GridStateChanged_0_listener($event) { return ctx.onGridStateChanged($event); })("SelectionChange", function GridViewRendererComponent_Template_mj_entity_data_grid_SelectionChange_0_listener($event) { return ctx.onSelectionChange($event); })("NewButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_NewButtonClick_0_listener() { return ctx.onNewButtonClick(); })("RefreshButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_RefreshButtonClick_0_listener() { return ctx.onRefreshButtonClick(); })("DeleteButtonClick", function GridViewRendererComponent_Template_mj_entity_data_grid_DeleteButtonClick_0_listener($event) { return ctx.onDeleteButtonClick($event); })("AddToListRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_AddToListRequested_0_listener($event) { return ctx.onAddToListRequested($event); })("ForeignKeyClick", function GridViewRendererComponent_Template_mj_entity_data_grid_ForeignKeyClick_0_listener($event) { return ctx.onForeignKeyClick($event); })("PageChange", function GridViewRendererComponent_Template_mj_entity_data_grid_PageChange_0_listener($event) { return ctx.onPageChange($event); })("ManageColumnsRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_ManageColumnsRequested_0_listener() { return ctx.configureRequested.emit(); })("MergeRecordsRequested", function GridViewRendererComponent_Template_mj_entity_data_grid_MergeRecordsRequested_0_listener($event) { return ctx.onMergeRequested($event); });
479
968
  i0.ɵɵelementEnd();
480
969
  i0.ɵɵconditionalCreate(2, GridViewRendererComponent_Conditional_2_Template, 1, 3, "mj-list-management-dialog", 2);
481
970
  i0.ɵɵelementStart(3, "mj-ev-confirm-dialog", 3);
482
971
  i0.ɵɵlistener("Confirmed", function GridViewRendererComponent_Template_mj_ev_confirm_dialog_Confirmed_3_listener() { return ctx.onDeleteConfirmed(); })("Cancelled", function GridViewRendererComponent_Template_mj_ev_confirm_dialog_Cancelled_3_listener() { return ctx.onDeleteCancelled(); });
483
972
  i0.ɵɵelementEnd();
973
+ i0.ɵɵconditionalCreate(4, GridViewRendererComponent_Conditional_4_Template, 12, 16, "mj-dialog", 4);
974
+ i0.ɵɵconditionalCreate(5, GridViewRendererComponent_Conditional_5_Template, 5, 1, "div", 5);
484
975
  } if (rf & 2) {
485
- i0.ɵɵproperty("Provider", ctx.Provider)("Data", ctx.records)("ExportDataProvider", ctx.exportDataProvider)("Params", ctx.effectiveParams)("FilterText", ctx.filterText ?? "")("GridState", ctx.config.gridState ?? null)("Height", "auto")("AllowLoad", false)("AutoLoadEntityActions", ctx.AutoLoadEntityActions)("ShowToolbar", ctx.effectiveShowToolbar)("ShowSearch", ctx.effectiveShowSearch)("ToolbarConfig", ctx.config.toolbarConfig ?? i0.ɵɵpureFunction0(21, _c1))("SelectionMode", ctx.effectiveSelectionMode)("ShowAddToListButton", ctx.effectiveShowAddToListButton)("ShowPager", ctx.effectiveShowPager)("PageSize", ctx.effectivePageSize)("TotalRowCount", ctx.totalRecordCount ?? 0)("PagerPageNumber", ctx.page ?? 1);
976
+ i0.ɵɵproperty("Provider", ctx.Provider)("Data", ctx.records)("ExportDataProvider", ctx.exportDataProvider)("Params", ctx.effectiveParams)("FilterText", ctx.filterText ?? "")("GridState", ctx.config.gridState ?? null)("Height", "auto")("AllowLoad", false)("AutoLoadEntityActions", ctx.AutoLoadEntityActions)("ShowToolbar", ctx.effectiveShowToolbar)("ShowSearch", ctx.effectiveShowSearch)("ToolbarConfig", ctx.config.toolbarConfig ?? i0.ɵɵpureFunction0(24, _c1))("SelectionMode", ctx.effectiveSelectionMode)("ShowAddToListButton", ctx.effectiveShowAddToListButton)("ShowPager", ctx.effectiveShowPager)("PageSize", ctx.effectivePageSize)("TotalRowCount", ctx.totalRecordCount ?? 0)("PagerPageNumber", ctx.page ?? 1)("ShowMergeButton", ctx.effectiveShowMergeButton);
486
977
  i0.ɵɵadvance(2);
487
978
  i0.ɵɵconditional(ctx.listManagementConfig ? 2 : -1);
488
979
  i0.ɵɵadvance();
489
980
  i0.ɵɵproperty("IsOpen", ctx.showDeleteConfirm)("Message", ctx.deleteConfirmMessage);
490
- } }, dependencies: [i1.ListManagementDialogComponent, i2.EntityDataGridComponent, i3.ConfirmDialogComponent], styles: ["\n :host {\n display: block;\n height: 100%;\n }\n "], encapsulation: 2 });
981
+ i0.ɵɵadvance();
982
+ i0.ɵɵconditional(ctx.mergeState ? 4 : -1);
983
+ i0.ɵɵadvance();
984
+ i0.ɵɵconditional(ctx.mergeNotice ? 5 : -1);
985
+ } }, dependencies: [i1.ListManagementDialogComponent, i2.RecordMergePanelComponent, i3.MJDialogComponent, i3.MJButtonDirective, i4.EntityDataGridComponent, i5.ConfirmDialogComponent], styles: ["\n :host {\n display: block;\n height: 100%;\n }\n\n .mj-ev-merge-survivor {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-bottom: 12px;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n }\n\n .mj-ev-merge-survivor-note {\n color: var(--mj-text-muted);\n }\n\n .mj-ev-merge-deps {\n margin: 0;\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n }\n\n .mj-ev-merge-notice {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 1001;\n padding: 8px 14px;\n border: 1px solid var(--mj-border-subtle);\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-elevated);\n color: var(--mj-text-primary);\n font-size: var(--mj-text-sm);\n box-shadow: var(--mj-shadow-md);\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .mj-ev-merge-notice-dismiss {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0 2px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n line-height: 1;\n }\n\n .mj-ev-merge-notice-dismiss:hover {\n color: var(--mj-text-primary);\n }\n "], encapsulation: 2 });
491
986
  }
492
987
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(GridViewRendererComponent, [{
493
988
  type: Component,
@@ -524,6 +1019,8 @@ export class GridViewRendererComponent extends BaseAngularComponent {
524
1019
  (ForeignKeyClick)="onForeignKeyClick($event)"
525
1020
  (PageChange)="onPageChange($event)"
526
1021
  (ManageColumnsRequested)="configureRequested.emit()"
1022
+ [ShowMergeButton]="effectiveShowMergeButton"
1023
+ (MergeRecordsRequested)="onMergeRequested($event)"
527
1024
  >
528
1025
  </mj-entity-data-grid>
529
1026
 
@@ -556,11 +1053,54 @@ export class GridViewRendererComponent extends BaseAngularComponent {
556
1053
  (Cancelled)="onDeleteCancelled()"
557
1054
  >
558
1055
  </mj-ev-confirm-dialog>
559
- `, styles: ["\n :host {\n display: block;\n height: 100%;\n }\n "] }]
1056
+
1057
+ <!-- Self-contained merge panel (Generic) — owned by this wrapper, never bubbles up.
1058
+ Lives inside the @if so the panel rebuilds its comparison (ngOnInit) per request. -->
1059
+ @if (mergeState) {
1060
+ <mj-dialog [Visible]="true" Size="lg" [Closeable]="!mergeState.IsMerging" (Close)="onMergeCancelled()">
1061
+ <div class="mj-ev-merge-survivor" role="radiogroup" aria-label="Record to keep">
1062
+ <span>Keep</span>
1063
+ <button type="button" mjButton size="sm" role="radio"
1064
+ [variant]="mergeState.Config.SurvivorSide === 'left' ? 'primary' : 'secondary'"
1065
+ [attr.aria-checked]="mergeState.Config.SurvivorSide === 'left'"
1066
+ [disabled]="mergeState.IsMerging"
1067
+ (click)="onSurvivorChange('left')">{{ mergeState.Config.LeftLabel }}</button>
1068
+ <button type="button" mjButton size="sm" role="radio"
1069
+ [variant]="mergeState.Config.SurvivorSide === 'right' ? 'primary' : 'secondary'"
1070
+ [attr.aria-checked]="mergeState.Config.SurvivorSide === 'right'"
1071
+ [disabled]="mergeState.IsMerging"
1072
+ (click)="onSurvivorChange('right')">{{ mergeState.Config.RightLabel }}</button>
1073
+ <span class="mj-ev-merge-survivor-note">{{ mergeLoserLabel }} will be deleted; its linked records move to the record you keep.</span>
1074
+ </div>
1075
+ <mj-record-merge-panel
1076
+ [Fields]="mergeState.Fields"
1077
+ [Config]="mergeState.Config"
1078
+ [MergeEnabled]="!mergeState.IsMerging"
1079
+ [IsMerging]="mergeState.IsMerging"
1080
+ (MergeConfirmed)="onMergeConfirmed($event)"
1081
+ (MergeCancelled)="onMergeCancelled()"
1082
+ >
1083
+ </mj-record-merge-panel>
1084
+ @if (mergeState.DependencyNote) {
1085
+ <p class="mj-ev-merge-deps">{{ mergeState.DependencyNote }}</p>
1086
+ }
1087
+ </mj-dialog>
1088
+ }
1089
+ @if (mergeNotice) {
1090
+ <div class="mj-ev-merge-notice" role="status">
1091
+ <span class="mj-ev-merge-notice-text">{{ mergeNotice }}</span>
1092
+ <button type="button" class="mj-ev-merge-notice-dismiss" (click)="onDismissMergeNotice()" title="Dismiss" aria-label="Dismiss">
1093
+ <i class="fa-solid fa-xmark"></i>
1094
+ </button>
1095
+ </div>
1096
+ }
1097
+ `, styles: ["\n :host {\n display: block;\n height: 100%;\n }\n\n .mj-ev-merge-survivor {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-bottom: 12px;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n }\n\n .mj-ev-merge-survivor-note {\n color: var(--mj-text-muted);\n }\n\n .mj-ev-merge-deps {\n margin: 0;\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n }\n\n .mj-ev-merge-notice {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 1001;\n padding: 8px 14px;\n border: 1px solid var(--mj-border-subtle);\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-elevated);\n color: var(--mj-text-primary);\n font-size: var(--mj-text-sm);\n box-shadow: var(--mj-shadow-md);\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .mj-ev-merge-notice-dismiss {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0 2px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n line-height: 1;\n }\n\n .mj-ev-merge-notice-dismiss:hover {\n color: var(--mj-text-primary);\n }\n "] }]
560
1098
  }], null, { entity: [{
561
1099
  type: Input
562
1100
  }], records: [{
563
1101
  type: Input
1102
+ }], ExportDataProvider: [{
1103
+ type: Input
564
1104
  }], exportDataProvider: [{
565
1105
  type: Input
566
1106
  }], selectedRecordId: [{
@@ -597,7 +1137,7 @@ export class GridViewRendererComponent extends BaseAngularComponent {
597
1137
  type: ViewChild,
598
1138
  args: ['grid']
599
1139
  }] }); })();
600
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(GridViewRendererComponent, { className: "GridViewRendererComponent", filePath: "src/lib/view-types/renderers/grid-view-renderer.component.ts", lineNumber: 187 }); })();
1140
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(GridViewRendererComponent, { className: "GridViewRendererComponent", filePath: "src/lib/view-types/renderers/grid-view-renderer.component.ts", lineNumber: 289 }); })();
601
1141
  /**
602
1142
  * Tree-shaking guard. Force-references this renderer so bundlers (ESBuild/Vite) don't drop the
603
1143
  * component in builds that only mount it dynamically via the ClassFactory/descriptor. Mirrors the