@memberjunction/ng-record-clone 0.0.1-seed.1 → 6.2.0-edge.3

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 (55) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +241 -3
  3. package/dist/lib/clone-lineage-chip.component.d.ts +50 -0
  4. package/dist/lib/clone-lineage-chip.component.d.ts.map +1 -0
  5. package/dist/lib/clone-lineage-chip.component.js +448 -0
  6. package/dist/lib/clone-lineage-chip.component.js.map +1 -0
  7. package/dist/lib/clone-plan-tree.component.d.ts +51 -0
  8. package/dist/lib/clone-plan-tree.component.d.ts.map +1 -0
  9. package/dist/lib/clone-plan-tree.component.js +634 -0
  10. package/dist/lib/clone-plan-tree.component.js.map +1 -0
  11. package/dist/lib/clone-progress.component.d.ts +12 -0
  12. package/dist/lib/clone-progress.component.d.ts.map +1 -0
  13. package/dist/lib/clone-progress.component.js +151 -0
  14. package/dist/lib/clone-progress.component.js.map +1 -0
  15. package/dist/lib/clone-result.component.d.ts +40 -0
  16. package/dist/lib/clone-result.component.d.ts.map +1 -0
  17. package/dist/lib/clone-result.component.js +357 -0
  18. package/dist/lib/clone-result.component.js.map +1 -0
  19. package/dist/lib/clone-review.component.d.ts +58 -0
  20. package/dist/lib/clone-review.component.d.ts.map +1 -0
  21. package/dist/lib/clone-review.component.js +581 -0
  22. package/dist/lib/clone-review.component.js.map +1 -0
  23. package/dist/lib/clone-scope-controls.component.d.ts +81 -0
  24. package/dist/lib/clone-scope-controls.component.d.ts.map +1 -0
  25. package/dist/lib/clone-scope-controls.component.js +394 -0
  26. package/dist/lib/clone-scope-controls.component.js.map +1 -0
  27. package/dist/lib/clone-values.component.d.ts +55 -0
  28. package/dist/lib/clone-values.component.d.ts.map +1 -0
  29. package/dist/lib/clone-values.component.js +493 -0
  30. package/dist/lib/clone-values.component.js.map +1 -0
  31. package/dist/lib/record-clone-panel.component.d.ts +247 -0
  32. package/dist/lib/record-clone-panel.component.d.ts.map +1 -0
  33. package/dist/lib/record-clone-panel.component.js +1252 -0
  34. package/dist/lib/record-clone-panel.component.js.map +1 -0
  35. package/dist/lib/record-clone-slide-in.component.d.ts +83 -0
  36. package/dist/lib/record-clone-slide-in.component.d.ts.map +1 -0
  37. package/dist/lib/record-clone-slide-in.component.js +206 -0
  38. package/dist/lib/record-clone-slide-in.component.js.map +1 -0
  39. package/dist/lib/record-clone-types.d.ts +89 -0
  40. package/dist/lib/record-clone-types.d.ts.map +1 -0
  41. package/dist/lib/record-clone-types.js +29 -0
  42. package/dist/lib/record-clone-types.js.map +1 -0
  43. package/dist/lib/record-clone.module.d.ts +16 -0
  44. package/dist/lib/record-clone.module.d.ts.map +1 -0
  45. package/dist/lib/record-clone.module.js +69 -0
  46. package/dist/lib/record-clone.module.js.map +1 -0
  47. package/dist/lib/record-clone.service.d.ts +42 -0
  48. package/dist/lib/record-clone.service.d.ts.map +1 -0
  49. package/dist/lib/record-clone.service.js +123 -0
  50. package/dist/lib/record-clone.service.js.map +1 -0
  51. package/dist/public-api.d.ts +16 -0
  52. package/dist/public-api.d.ts.map +1 -0
  53. package/dist/public-api.js +16 -0
  54. package/dist/public-api.js.map +1 -0
  55. package/package.json +47 -5
@@ -0,0 +1,1252 @@
1
+ /**
2
+ * @fileoverview Embeddable record clone panel.
3
+ *
4
+ * Implements §12.2 and §12.3 of the record cloning plan. Hosts the full clone wizard
5
+ * (loading, scope, values, review, executing, done, failed, not cloneable) inline, with no
6
+ * overlay of its own, so it can sit in a slide-in, a dialog, a dashboard or a test harness.
7
+ * {@link RecordCloneSlideInComponent} wraps it in MJ's generic slide panel.
8
+ *
9
+ * The panel never navigates. It reports what happened through outputs and the host decides:
10
+ * `NavigateToRecord` carries the record to open, `CloseRequested` asks the host to dismiss it.
11
+ */
12
+ import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, inject, } from '@angular/core';
13
+ import { CommonModule } from '@angular/common';
14
+ import { BaseEntity, CompositeKey } from '@memberjunction/core';
15
+ import { MJGlobal, MJEventType } from '@memberjunction/global';
16
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
17
+ import { SharedGenericModule } from '@memberjunction/ng-shared-generic';
18
+ import { NormalizeClonePresets } from '@memberjunction/record-cloning-base';
19
+ import { MJButtonDirective, MJTabNavComponent, } from '@memberjunction/ng-ui-components';
20
+ import { RecordCloneService } from './record-clone.service';
21
+ import { CloneScopeControlsComponent } from './clone-scope-controls.component';
22
+ import { ClonePlanTreeComponent } from './clone-plan-tree.component';
23
+ import { CloneValuesComponent } from './clone-values.component';
24
+ import { CloneReviewComponent } from './clone-review.component';
25
+ import { CloneProgressComponent } from './clone-progress.component';
26
+ import { CloneResultComponent } from './clone-result.component';
27
+ import { EntityToRecordCloneKey, CompositeKeyToRecordCloneKey, } from './record-clone-types';
28
+ import * as i0 from "@angular/core";
29
+ import * as i1 from "@memberjunction/ng-shared-generic";
30
+ const _forTrack0 = ($index, $item) => $item.RelationshipID;
31
+ function RecordClonePanelComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
32
+ const _r1 = i0.ɵɵgetCurrentView();
33
+ i0.ɵɵelementStart(0, "div", 1)(1, "mj-tab-nav", 8);
34
+ i0.ɵɵlistener("TabChange", function RecordClonePanelComponent_Conditional_1_Template_mj_tab_nav_TabChange_1_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnStepChange($event)); });
35
+ i0.ɵɵelementEnd()();
36
+ } if (rf & 2) {
37
+ const ctx_r1 = i0.ɵɵnextContext();
38
+ i0.ɵɵadvance();
39
+ i0.ɵɵproperty("Tabs", ctx_r1.WizardTabs)("ActiveKey", ctx_r1.CurrentStep);
40
+ } }
41
+ function RecordClonePanelComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
42
+ i0.ɵɵelementStart(0, "div", 3);
43
+ i0.ɵɵelement(1, "mj-loading", 9);
44
+ i0.ɵɵelementEnd();
45
+ } if (rf & 2) {
46
+ const ctx_r1 = i0.ɵɵnextContext();
47
+ i0.ɵɵadvance();
48
+ i0.ɵɵproperty("Text", ctx_r1.LoadingMessage);
49
+ } }
50
+ function RecordClonePanelComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
51
+ const _r3 = i0.ɵɵgetCurrentView();
52
+ i0.ɵɵelementStart(0, "div", 4);
53
+ i0.ɵɵelement(1, "i", 10);
54
+ i0.ɵɵelementStart(2, "h3", 11);
55
+ i0.ɵɵtext(3, "Record Cannot Be Cloned");
56
+ i0.ɵɵelementEnd();
57
+ i0.ɵɵelementStart(4, "p", 12);
58
+ i0.ɵɵtext(5);
59
+ i0.ɵɵelementEnd();
60
+ i0.ɵɵelementStart(6, "button", 13);
61
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_4_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnClose()); });
62
+ i0.ɵɵtext(7, " Close ");
63
+ i0.ɵɵelementEnd()();
64
+ } if (rf & 2) {
65
+ const ctx_r1 = i0.ɵɵnextContext();
66
+ i0.ɵɵadvance(5);
67
+ i0.ɵɵtextInterpolate(ctx_r1.NotCloneableReason);
68
+ } }
69
+ function RecordClonePanelComponent_Conditional_5_Conditional_6_For_2_Template(rf, ctx) { if (rf & 1) {
70
+ const _r5 = i0.ɵɵgetCurrentView();
71
+ i0.ɵɵelementStart(0, "span", 23);
72
+ i0.ɵɵtext(1);
73
+ i0.ɵɵelementStart(2, "button", 24);
74
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_5_Conditional_6_For_2_Template_button_click_2_listener() { const o_r6 = i0.ɵɵrestoreView(_r5).$implicit; const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.OnUndoEdgeOverride(o_r6.RelationshipID)); });
75
+ i0.ɵɵtext(3, "Undo");
76
+ i0.ɵɵelementEnd()();
77
+ } if (rf & 2) {
78
+ const o_r6 = ctx.$implicit;
79
+ i0.ɵɵadvance();
80
+ i0.ɵɵtextInterpolate2(" ", o_r6.Policy === "Skip" ? "Skipping" : "Copying", " ", o_r6.RelatedEntityName || "a relationship", " ");
81
+ i0.ɵɵadvance();
82
+ i0.ɵɵattribute("aria-label", "Undo: " + (o_r6.Policy === "Skip" ? "copy " : "skip ") + (o_r6.RelatedEntityName || "this relationship") + " again");
83
+ } }
84
+ function RecordClonePanelComponent_Conditional_5_Conditional_6_Template(rf, ctx) { if (rf & 1) {
85
+ i0.ɵɵelementStart(0, "div", 18);
86
+ i0.ɵɵrepeaterCreate(1, RecordClonePanelComponent_Conditional_5_Conditional_6_For_2_Template, 4, 3, "span", 23, _forTrack0);
87
+ i0.ɵɵelementEnd();
88
+ } if (rf & 2) {
89
+ const ctx_r1 = i0.ɵɵnextContext(2);
90
+ i0.ɵɵadvance();
91
+ i0.ɵɵrepeater(ctx_r1.EdgeOverrides);
92
+ } }
93
+ function RecordClonePanelComponent_Conditional_5_Conditional_7_Template(rf, ctx) { if (rf & 1) {
94
+ i0.ɵɵelementStart(0, "p", 19);
95
+ i0.ɵɵtext(1);
96
+ i0.ɵɵelementEnd();
97
+ } if (rf & 2) {
98
+ const ctx_r1 = i0.ɵɵnextContext(2);
99
+ i0.ɵɵadvance();
100
+ i0.ɵɵtextInterpolate1("Click Deep on a branch to skip all of its rows.", ctx_r1.CanOverrideScope ? " Click Skip to copy a branch the configuration leaves out." : "");
101
+ } }
102
+ function RecordClonePanelComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
103
+ const _r4 = i0.ɵɵgetCurrentView();
104
+ i0.ɵɵelementStart(0, "div", 6)(1, "mj-clone-scope-controls", 14);
105
+ i0.ɵɵlistener("ScopeChanged", function RecordClonePanelComponent_Conditional_5_Template_mj_clone_scope_controls_ScopeChanged_1_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnScopeOptionsChanged($event)); })("ResetToDefaults", function RecordClonePanelComponent_Conditional_5_Template_mj_clone_scope_controls_ResetToDefaults_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnResetScopeToDefaults()); });
106
+ i0.ɵɵelementEnd();
107
+ i0.ɵɵelementStart(2, "div", 15)(3, "h4", 16);
108
+ i0.ɵɵtext(4, "Planned Record Graph");
109
+ i0.ɵɵelementEnd();
110
+ i0.ɵɵelementStart(5, "mj-clone-plan-tree", 17);
111
+ i0.ɵɵlistener("NodeSelected", function RecordClonePanelComponent_Conditional_5_Template_mj_clone_plan_tree_NodeSelected_5_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnNodeSelected($event)); })("EdgePolicyChanged", function RecordClonePanelComponent_Conditional_5_Template_mj_clone_plan_tree_EdgePolicyChanged_5_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnEdgePolicyChanged($event)); });
112
+ i0.ɵɵelementEnd();
113
+ i0.ɵɵconditionalCreate(6, RecordClonePanelComponent_Conditional_5_Conditional_6_Template, 3, 0, "div", 18);
114
+ i0.ɵɵconditionalCreate(7, RecordClonePanelComponent_Conditional_5_Conditional_7_Template, 2, 1, "p", 19);
115
+ i0.ɵɵelementEnd();
116
+ i0.ɵɵelementStart(8, "div", 20);
117
+ i0.ɵɵelement(9, "div");
118
+ i0.ɵɵelementStart(10, "button", 21);
119
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_5_Template_button_click_10_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.GoToStep("values")); });
120
+ i0.ɵɵtext(11, " Next: Values & Retarget ");
121
+ i0.ɵɵelement(12, "i", 22);
122
+ i0.ɵɵelementEnd()()();
123
+ } if (rf & 2) {
124
+ const ctx_r1 = i0.ɵɵnextContext();
125
+ i0.ɵɵclassProp("step-hidden", ctx_r1.CurrentStep !== "scope");
126
+ i0.ɵɵadvance();
127
+ i0.ɵɵproperty("Presets", ctx_r1.AvailablePresets)("PresetLabels", ctx_r1.PresetLabels)("SelectedPreset", ctx_r1.SelectedPreset)("MaxDepth", ctx_r1.DisplayedScope.MaxDepth)("Subtypes", ctx_r1.DisplayedScope.Subtypes)("Hierarchy", ctx_r1.DisplayedScope.Hierarchy)("SoftLinks", ctx_r1.DisplayedScope.SoftLinks)("EntityActions", ctx_r1.DisplayedScope.EntityActions)("MaxRecords", ctx_r1.DisplayedScope.MaxRecords)("ConfiguredScope", ctx_r1.ConfiguredScope)("EntityName", ctx_r1.EffectiveEntityName)("CanOverrideScope", ctx_r1.CanOverrideScope);
128
+ i0.ɵɵadvance(4);
129
+ i0.ɵɵproperty("Plan", ctx_r1.ActivePlan)("AllowNarrowing", ctx_r1.CanNarrowScope)("AllowWidening", ctx_r1.CanOverrideScope);
130
+ i0.ɵɵadvance();
131
+ i0.ɵɵconditional(ctx_r1.EdgeOverrides.length > 0 ? 6 : -1);
132
+ i0.ɵɵadvance();
133
+ i0.ɵɵconditional(ctx_r1.CanNarrowScope ? 7 : -1);
134
+ i0.ɵɵadvance(3);
135
+ i0.ɵɵproperty("disabled", !ctx_r1.ActivePlan);
136
+ } }
137
+ function RecordClonePanelComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
138
+ const _r7 = i0.ɵɵgetCurrentView();
139
+ i0.ɵɵelementStart(0, "div", 6)(1, "mj-clone-values", 25);
140
+ i0.ɵɵlistener("RootNameChange", function RecordClonePanelComponent_Conditional_6_Template_mj_clone_values_RootNameChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnRootNameChange($event)); })("PromptedValuesChange", function RecordClonePanelComponent_Conditional_6_Template_mj_clone_values_PromptedValuesChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnPromptedValuesChange($event)); })("RetargetFieldsChange", function RecordClonePanelComponent_Conditional_6_Template_mj_clone_values_RetargetFieldsChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnRetargetFieldsChange($event)); })("ReasonChange", function RecordClonePanelComponent_Conditional_6_Template_mj_clone_values_ReasonChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnReasonChange($event)); })("ValidityChange", function RecordClonePanelComponent_Conditional_6_Template_mj_clone_values_ValidityChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnValuesValidityChange($event)); });
141
+ i0.ɵɵelementEnd();
142
+ i0.ɵɵelementStart(2, "div", 20)(3, "button", 13);
143
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_6_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.GoToStep("scope")); });
144
+ i0.ɵɵelement(4, "i", 26);
145
+ i0.ɵɵtext(5, " Back to Scope ");
146
+ i0.ɵɵelementEnd();
147
+ i0.ɵɵelementStart(6, "button", 21);
148
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_6_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.GoToStep("review")); });
149
+ i0.ɵɵtext(7, " Next: Review & Execute ");
150
+ i0.ɵɵelement(8, "i", 22);
151
+ i0.ɵɵelementEnd()()();
152
+ } if (rf & 2) {
153
+ const ctx_r1 = i0.ɵɵnextContext();
154
+ i0.ɵɵclassProp("step-hidden", ctx_r1.CurrentStep !== "values");
155
+ i0.ɵɵadvance();
156
+ i0.ɵɵproperty("EntityName", ctx_r1.EffectiveEntityName)("RootName", ctx_r1.RootRecordName)("RootNameReadOnly", ctx_r1.RootNameReadOnly)("NamingStrategyReason", ctx_r1.NamingStrategyReason)("PromptedFields", ctx_r1.PromptedFields)("PromptedValues", ctx_r1.PromptedValues)("RetargetFields", ctx_r1.RetargetFields)("Reason", ctx_r1.CloneReason);
157
+ i0.ɵɵadvance(5);
158
+ i0.ɵɵproperty("disabled", !ctx_r1.IsValuesValid);
159
+ } }
160
+ function RecordClonePanelComponent_Conditional_7_Conditional_1_Template(rf, ctx) { if (rf & 1) {
161
+ i0.ɵɵelementStart(0, "div", 27);
162
+ i0.ɵɵelement(1, "i", 29);
163
+ i0.ɵɵelementStart(2, "span");
164
+ i0.ɵɵtext(3);
165
+ i0.ɵɵelementEnd()();
166
+ } if (rf & 2) {
167
+ const ctx_r1 = i0.ɵɵnextContext(2);
168
+ i0.ɵɵadvance(3);
169
+ i0.ɵɵtextInterpolate(ctx_r1.PlanChangedNotice);
170
+ } }
171
+ function RecordClonePanelComponent_Conditional_7_Conditional_3_Template(rf, ctx) { if (rf & 1) {
172
+ const _r9 = i0.ɵɵgetCurrentView();
173
+ i0.ɵɵelementStart(0, "div", 20)(1, "button", 13);
174
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_7_Conditional_3_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r9); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.GoToStep("values")); });
175
+ i0.ɵɵelement(2, "i", 26);
176
+ i0.ɵɵtext(3, " Back to Values ");
177
+ i0.ɵɵelementEnd()();
178
+ } }
179
+ function RecordClonePanelComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
180
+ const _r8 = i0.ɵɵgetCurrentView();
181
+ i0.ɵɵelementStart(0, "div", 6);
182
+ i0.ɵɵconditionalCreate(1, RecordClonePanelComponent_Conditional_7_Conditional_1_Template, 4, 1, "div", 27);
183
+ i0.ɵɵelementStart(2, "mj-clone-review", 28);
184
+ i0.ɵɵlistener("Confirm", function RecordClonePanelComponent_Conditional_7_Template_mj_clone_review_Confirm_2_listener() { i0.ɵɵrestoreView(_r8); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.ExecuteClone()); })("Cancel", function RecordClonePanelComponent_Conditional_7_Template_mj_clone_review_Cancel_2_listener() { i0.ɵɵrestoreView(_r8); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnClose()); })("NodeClicked", function RecordClonePanelComponent_Conditional_7_Template_mj_clone_review_NodeClicked_2_listener($event) { i0.ɵɵrestoreView(_r8); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnReviewNodeClicked($event)); });
185
+ i0.ɵɵelementEnd();
186
+ i0.ɵɵconditionalCreate(3, RecordClonePanelComponent_Conditional_7_Conditional_3_Template, 4, 0, "div", 20);
187
+ i0.ɵɵelementEnd();
188
+ } if (rf & 2) {
189
+ const ctx_r1 = i0.ɵɵnextContext();
190
+ i0.ɵɵclassProp("step-hidden", ctx_r1.CurrentStep !== "review");
191
+ i0.ɵɵadvance();
192
+ i0.ɵɵconditional(ctx_r1.PlanChangedNotice ? 1 : -1);
193
+ i0.ɵɵadvance();
194
+ i0.ɵɵproperty("Plan", ctx_r1.ActivePlan)("RootName", ctx_r1.RootRecordName)("Reason", ctx_r1.CloneReason)("IsExecuting", ctx_r1.CurrentState === "executing")("IsPlanning", ctx_r1.IsReplanning);
195
+ i0.ɵɵadvance();
196
+ i0.ɵɵconditional(ctx_r1.CurrentState !== "executing" ? 3 : -1);
197
+ } }
198
+ function RecordClonePanelComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
199
+ i0.ɵɵelementStart(0, "div", 6);
200
+ i0.ɵɵelement(1, "mj-clone-progress", 30);
201
+ i0.ɵɵelementEnd();
202
+ } if (rf & 2) {
203
+ const ctx_r1 = i0.ɵɵnextContext();
204
+ i0.ɵɵadvance();
205
+ i0.ɵɵproperty("Progress", ctx_r1.ExecutionProgress);
206
+ } }
207
+ function RecordClonePanelComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
208
+ const _r10 = i0.ɵɵgetCurrentView();
209
+ i0.ɵɵelementStart(0, "div", 6)(1, "mj-clone-result", 31);
210
+ i0.ɵɵlistener("CloneAnother", function RecordClonePanelComponent_Conditional_9_Template_mj_clone_result_CloneAnother_1_listener() { i0.ɵɵrestoreView(_r10); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.Reset()); })("Close", function RecordClonePanelComponent_Conditional_9_Template_mj_clone_result_Close_1_listener() { i0.ɵɵrestoreView(_r10); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnClose()); })("NavigateToRecord", function RecordClonePanelComponent_Conditional_9_Template_mj_clone_result_NavigateToRecord_1_listener($event) { i0.ɵɵrestoreView(_r10); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnNavigateToRecord($event)); });
211
+ i0.ɵɵelementEnd()();
212
+ } if (rf & 2) {
213
+ const ctx_r1 = i0.ɵɵnextContext();
214
+ i0.ɵɵadvance();
215
+ i0.ɵɵproperty("Result", ctx_r1.ExecutionResult)("EntityName", ctx_r1.EffectiveEntityName)("TargetKey", ctx_r1.TargetRecordKey)("RootRecordName", ctx_r1.RootRecordName);
216
+ } }
217
+ function RecordClonePanelComponent_Conditional_10_Template(rf, ctx) { if (rf & 1) {
218
+ const _r11 = i0.ɵɵgetCurrentView();
219
+ i0.ɵɵelementStart(0, "div", 7);
220
+ i0.ɵɵelement(1, "i", 32);
221
+ i0.ɵɵelementStart(2, "h3", 33);
222
+ i0.ɵɵtext(3, "Execution Error");
223
+ i0.ɵɵelementEnd();
224
+ i0.ɵɵelementStart(4, "p", 12);
225
+ i0.ɵɵtext(5);
226
+ i0.ɵɵelementEnd();
227
+ i0.ɵɵelementStart(6, "div", 34)(7, "button", 35);
228
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_10_Template_button_click_7_listener() { i0.ɵɵrestoreView(_r11); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.BackToReview()); });
229
+ i0.ɵɵtext(8, " Back to Review ");
230
+ i0.ɵɵelementEnd();
231
+ i0.ɵɵelementStart(9, "button", 13);
232
+ i0.ɵɵlistener("click", function RecordClonePanelComponent_Conditional_10_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r11); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnClose()); });
233
+ i0.ɵɵtext(10, " Close ");
234
+ i0.ɵɵelementEnd()()();
235
+ } if (rf & 2) {
236
+ const ctx_r1 = i0.ɵɵnextContext();
237
+ i0.ɵɵadvance(5);
238
+ i0.ɵɵtextInterpolate(ctx_r1.ExecutionErrorMessage);
239
+ } }
240
+ const SCOPE_OPTION_KEYS = ['MaxDepth', 'MaxRecords', 'Subtypes', 'Hierarchy', 'SoftLinks', 'EntityActions'];
241
+ /**
242
+ * Embeddable clone wizard for one source record.
243
+ *
244
+ * Give it a source with either {@link Record} or {@link EntityName} plus {@link RecordKey}.
245
+ * With {@link AutoStart} on (the default) it describes the record and computes the first plan
246
+ * as soon as it has a source; otherwise call {@link Start}.
247
+ *
248
+ * @example
249
+ * ```html
250
+ * <mj-record-clone-panel
251
+ * [Record]="record"
252
+ * (CloneCompleted)="OnCloned($event)"
253
+ * (NavigateToRecord)="OpenRecord($event)"
254
+ * (CloseRequested)="HidePanel()">
255
+ * </mj-record-clone-panel>
256
+ * ```
257
+ */
258
+ export class RecordClonePanelComponent extends BaseAngularComponent {
259
+ cloneService = inject(RecordCloneService);
260
+ cdr = inject(ChangeDetectorRef);
261
+ // ── Inputs ───────────────────────────────────────────────────────────
262
+ /**
263
+ * The record to clone. Takes precedence for the key when {@link RecordKey} is not set,
264
+ * and supplies the entity name when {@link EntityName} is not set.
265
+ */
266
+ set Record(value) {
267
+ this._record = value;
268
+ this.queueAutoStart();
269
+ }
270
+ get Record() {
271
+ return this._record;
272
+ }
273
+ _record = null;
274
+ /** Entity of the source record, e.g. `'MJ: Users'`. Needed when {@link Record} is not set. */
275
+ set EntityName(value) {
276
+ this._entityName = value;
277
+ this.queueAutoStart();
278
+ }
279
+ get EntityName() {
280
+ return this._entityName;
281
+ }
282
+ _entityName;
283
+ /**
284
+ * Key of the source record: a {@link RecordCloneKey}, or a record-id string in the
285
+ * compact URL-segment form (`'<value>'` for a single-column key, `'F1|V1||F2|V2'` for a
286
+ * composite one), parsed with `CompositeKey.FromURLSegment` against the entity's metadata.
287
+ */
288
+ set RecordKey(value) {
289
+ this._recordKey = value;
290
+ this.queueAutoStart();
291
+ }
292
+ get RecordKey() {
293
+ return this._recordKey;
294
+ }
295
+ _recordKey;
296
+ /** Start describing and planning as soon as a source is set. Default true. */
297
+ AutoStart = true;
298
+ /** Show the Scope / Values / Review step tabs. Default true. */
299
+ ShowStepTabs = true;
300
+ // ── Outputs ──────────────────────────────────────────────────────────
301
+ /** Fires whenever {@link CurrentState} changes. */
302
+ StateChange = new EventEmitter();
303
+ /** Fires whenever the active wizard step changes. */
304
+ StepChange = new EventEmitter();
305
+ /** Fires after every successful plan computation with the fresh plan. */
306
+ PlanChanged = new EventEmitter();
307
+ /** Fires once the clone commits. The panel also raises one `remote-invalidate` event per written entity so open views refresh. */
308
+ CloneCompleted = new EventEmitter();
309
+ /** Fires when describe, plan or execute fails, with the message the panel shows. */
310
+ CloneFailed = new EventEmitter();
311
+ /** Asks the host to open a record (the new clone, or a record linked from the result). The panel never routes itself. */
312
+ NavigateToRecord = new EventEmitter();
313
+ /** Asks the host to dismiss the panel (Close, Cancel, or after a navigation request). */
314
+ CloseRequested = new EventEmitter();
315
+ // ── Public state (read by the template and by hosts) ─────────────────
316
+ /** Current wizard state. Setting it emits {@link StateChange}. */
317
+ get CurrentState() {
318
+ return this._currentState;
319
+ }
320
+ set CurrentState(value) {
321
+ if (value === this._currentState)
322
+ return;
323
+ this._currentState = value;
324
+ this.StateChange.emit(value);
325
+ }
326
+ _currentState = 'loading';
327
+ /** Active authoring step. Setting it emits {@link StepChange}. */
328
+ get CurrentStep() {
329
+ return this._currentStep;
330
+ }
331
+ set CurrentStep(value) {
332
+ if (value === this._currentStep)
333
+ return;
334
+ this._currentStep = value;
335
+ this.StepChange.emit(value);
336
+ }
337
+ _currentStep = 'scope';
338
+ /** True while the panel is describing, planning for the first time, or executing. Hosts use it to block closing. */
339
+ get IsBusy() {
340
+ return this.CurrentState === 'loading' || this.CurrentState === 'executing';
341
+ }
342
+ LoadingMessage = 'Analyzing record and dependencies...';
343
+ NotCloneableReason = 'This entity or record type does not permit cloning.';
344
+ ExecutionErrorMessage = '';
345
+ /** Describe answer for the source entity, once loaded. */
346
+ DescribeDetails = null;
347
+ /** Latest plan computed by the server. */
348
+ ActivePlan = null;
349
+ /** Result of the last execute call. */
350
+ ExecutionResult = null;
351
+ ExecutionProgress = null;
352
+ /**
353
+ * Plan options sent with every plan and execute request. Holds only what the user changed (plus
354
+ * the preset and entered values); everything else comes from the entity's clone configuration.
355
+ */
356
+ ScopeOptions = {};
357
+ AvailablePresets = [];
358
+ /** How each preset is shown in the picker, from the entity's clone configuration. */
359
+ PresetLabels = {};
360
+ SelectedPreset;
361
+ /**
362
+ * Whether the developer scope overrides are offered: `RecordClone.Describe` reports whether the
363
+ * user holds `Clone Records: Override Scope`. The server applies the same rule to every request.
364
+ */
365
+ CanOverrideScope = false;
366
+ /** Branch policies the user changed in the plan tree, sent with every plan and execute. */
367
+ EdgeOverrides = [];
368
+ /** Whether the user may skip branches: the entity's UserEditable allows scope changes, or they hold Override Scope. */
369
+ get CanNarrowScope() {
370
+ const editable = this.DescribeDetails?.UserEditable ?? 'all';
371
+ return editable === 'scope' || editable === 'all' || this.CanOverrideScope;
372
+ }
373
+ /** The entity's configured scope (its `Configuration.Clone`, else the engine defaults), for the summary. */
374
+ get ConfiguredScope() {
375
+ const entityCfg = this.ProviderToUse?.EntityByName(this.EffectiveEntityName)?.CloneConfig;
376
+ const presetOptions = RecordClonePanelComponent.presetOptions(entityCfg?.Presets, this.SelectedPreset);
377
+ const cfg = { ...(entityCfg ?? {}), ...presetOptions };
378
+ const builtIn = RecordClonePanelComponent.builtInScope();
379
+ return {
380
+ MaxDepth: cfg?.MaxDepth ?? builtIn.MaxDepth,
381
+ MaxRecords: cfg?.MaxRecords ?? builtIn.MaxRecords,
382
+ Subtypes: cfg?.Subtypes ?? builtIn.Subtypes,
383
+ Hierarchy: cfg?.Hierarchy ?? builtIn.Hierarchy,
384
+ SoftLinks: cfg?.SoftLinks ?? builtIn.SoftLinks,
385
+ EntityActions: cfg?.Hooks?.EntityActions ?? builtIn.EntityActions,
386
+ };
387
+ }
388
+ RootRecordName = '';
389
+ NamingStrategyReason;
390
+ PromptedFields = [];
391
+ PromptedValues = {};
392
+ RetargetFields = [];
393
+ CloneReason = '';
394
+ IsValuesValid = true;
395
+ HasUserEditedRootName = false;
396
+ WizardTabs = [
397
+ { key: 'scope', label: '1. Scope & Graph', icon: 'fa-solid fa-diagram-project' },
398
+ { key: 'values', label: '2. Values & Retarget', icon: 'fa-solid fa-pen-to-square' },
399
+ { key: 'review', label: '3. Review & Execute', icon: 'fa-solid fa-clipboard-check' },
400
+ ];
401
+ /** The promise of the most recent {@link Start}; tests and hosts can await it. */
402
+ InitializationPromise;
403
+ /** Entity name the panel is cloning, from {@link EntityName} or {@link Record}. */
404
+ get EffectiveEntityName() {
405
+ return this.EntityName || this.Record?.EntityInfo?.Name || 'Record';
406
+ }
407
+ /** Source key resolved against the entity's primary key columns. */
408
+ get EffectiveRecordKey() {
409
+ if (this.RecordKey) {
410
+ if (typeof this.RecordKey !== 'string')
411
+ return this.RecordKey;
412
+ const entityInfo = this.ProviderToUse?.EntityByName(this.EffectiveEntityName);
413
+ return CompositeKeyToRecordCloneKey(CompositeKey.FromURLSegment(entityInfo, this.RecordKey));
414
+ }
415
+ if (this.Record) {
416
+ return EntityToRecordCloneKey(this.Record);
417
+ }
418
+ return undefined;
419
+ }
420
+ /** Default title for hosts that show one, e.g. `Clone MJ: Users`. */
421
+ get PanelTitle() {
422
+ return `Clone ${this.EffectiveEntityName}`;
423
+ }
424
+ get ShowWizardTabs() {
425
+ return (this.ShowStepTabs &&
426
+ (this.CurrentState === 'scope' ||
427
+ this.CurrentState === 'values' ||
428
+ this.CurrentState === 'review' ||
429
+ this.CurrentState === 'plan_changed'));
430
+ }
431
+ // ── Public methods ───────────────────────────────────────────────────
432
+ /**
433
+ * Describes the source, computes the first plan and lands on the Scope step.
434
+ * Safe to call again; each call starts the wizard over.
435
+ */
436
+ Start() {
437
+ this.InitializationPromise = this.initialize();
438
+ return this.InitializationPromise;
439
+ }
440
+ /** Clears the values entered so far and starts over (used by "Clone another"). */
441
+ Reset() {
442
+ // Values folded into the options on the way to Review belong to the previous run.
443
+ this.ScopeOptions = {};
444
+ this.EdgeOverrides = [];
445
+ this.SelectedPreset = undefined;
446
+ this.ActivePlan = null;
447
+ this.ExecutionResult = null;
448
+ this.ExecutionProgress = null;
449
+ this.PlanChangedNotice = null;
450
+ this.PromptedValues = {};
451
+ this.PromptedFields = [];
452
+ this.RetargetFields = [];
453
+ this.CloneReason = '';
454
+ this.RootRecordName = '';
455
+ this.HasUserEditedRootName = false;
456
+ return this.Start();
457
+ }
458
+ /** True while a re-plan is in flight; Review disables Confirm so the user only confirms a plan they have seen. */
459
+ IsReplanning = false;
460
+ replanRequestId = 0;
461
+ /**
462
+ * Re-runs the plan with the current {@link ScopeOptions}, e.g. after a host changes them. When
463
+ * several re-plans overlap, only the latest request's answer is kept.
464
+ */
465
+ async Replan() {
466
+ const requestId = ++this.replanRequestId;
467
+ this.IsReplanning = true;
468
+ this.cdr.markForCheck();
469
+ try {
470
+ const planOutput = await this.cloneService.PlanClone({
471
+ EntityName: this.EffectiveEntityName,
472
+ SourceRecordKey: this.EffectiveRecordKey,
473
+ Options: this.ScopeOptions,
474
+ EdgeOverrides: this.wireEdgeOverrides(),
475
+ ExpectedPlanHash: this.ActivePlan?.Hash,
476
+ }, this.ProviderToUse);
477
+ if (requestId !== this.replanRequestId)
478
+ return; // a newer re-plan superseded this one
479
+ this.ActivePlan = planOutput.Plan;
480
+ if (planOutput.Plan) {
481
+ // The name the server will apply can move with the plan (a rule derives it from a
482
+ // prompted value, a rename avoids a newly taken one); follow it unless the user typed one.
483
+ if (this.RootNameReadOnly || !this.HasUserEditedRootName) {
484
+ const change = this.rootNameChange();
485
+ if (change) {
486
+ this.RootRecordName = String(change.NewValue);
487
+ // The note under the name says how it was made, which can change too (a rule instead of a suffix).
488
+ this.NamingStrategyReason = change.Reason || this.NamingStrategyReason;
489
+ }
490
+ }
491
+ this.PlanChanged.emit(planOutput.Plan);
492
+ }
493
+ }
494
+ catch (err) {
495
+ // A superseded request's failure says nothing about the plan on screen.
496
+ if (requestId === this.replanRequestId)
497
+ throw err;
498
+ }
499
+ finally {
500
+ if (requestId === this.replanRequestId)
501
+ this.IsReplanning = false;
502
+ this.cdr.markForCheck();
503
+ }
504
+ }
505
+ /**
506
+ * Moves to a wizard step. Review is refused while prompted values are invalid; entering
507
+ * Review folds the entered values into the options and re-plans so the diff is current.
508
+ */
509
+ GoToStep(step) {
510
+ if (step === 'review' && !this.IsValuesValid) {
511
+ return;
512
+ }
513
+ this.CurrentStep = step;
514
+ this.CurrentState = step;
515
+ this.PlanChangedNotice = null;
516
+ this.cdr.markForCheck();
517
+ if (step === 'review') {
518
+ // Re-plan whenever the values differ from the ones the shown plan was built with,
519
+ // including a value the user cleared or a retarget set back to the original.
520
+ const next = this.buildOptionsWithValues();
521
+ if (this.planValuesKey(next) !== this.planValuesKey(this.ScopeOptions)) {
522
+ this.ScopeOptions = next;
523
+ void this.replanOrFail();
524
+ }
525
+ }
526
+ }
527
+ /** Executes the reviewed plan. The server refuses with `PLAN_CHANGED` if the plan hash moved. */
528
+ async ExecuteClone() {
529
+ // Also refuse re-entry: the button is disabled only by binding, so a second call before
530
+ // change detection (a host, a key repeat) would start a second clone.
531
+ if (!this.ActivePlan || this.ActivePlan.Blocked || this.IsReplanning || this.CurrentState === 'executing')
532
+ return;
533
+ this.PlanChangedNotice = null;
534
+ this.CurrentState = 'executing';
535
+ // One transaction on the server, with no per-row progress channel: an indeterminate state with the row count.
536
+ const rows = this.ActivePlan.Counts.Create;
537
+ this.ExecutionProgress = {
538
+ Message: `Creating ${rows} record${rows === 1 ? '' : 's'} in one transaction...`,
539
+ Total: rows,
540
+ };
541
+ this.cdr.markForCheck();
542
+ try {
543
+ const result = await this.cloneService.ExecuteClone({
544
+ EntityName: this.EffectiveEntityName,
545
+ SourceRecordKey: this.EffectiveRecordKey,
546
+ ExpectedPlanHash: this.ActivePlan.Hash,
547
+ Options: this.buildOptionsWithValues(),
548
+ EdgeOverrides: this.wireEdgeOverrides(),
549
+ }, this.ProviderToUse);
550
+ this.ExecutionResult = result;
551
+ if (result.Success) {
552
+ this.CurrentState = 'done';
553
+ const completed = {
554
+ EntityName: this.EffectiveEntityName,
555
+ TargetKey: this.TargetRecordKey || '',
556
+ CloneLogID: result.CloneLogID,
557
+ CreatedRecordsCount: result.Created?.length || result.Counts?.Create || 1,
558
+ Result: result,
559
+ };
560
+ this.raiseRecordCreatedEvent(completed);
561
+ this.CloneCompleted.emit(completed);
562
+ }
563
+ else if (result.ResultCode === 'PLAN_CHANGED') {
564
+ // The records changed since review: show the fresh plan and ask for a new confirmation.
565
+ this.PlanChangedNotice = 'The records changed since you reviewed this plan. Review the updated plan and confirm again.';
566
+ this.CurrentStep = 'review';
567
+ this.CurrentState = 'plan_changed';
568
+ if (result.Plan) {
569
+ this.ActivePlan = result.Plan;
570
+ this.PlanChanged.emit(result.Plan);
571
+ }
572
+ else {
573
+ await this.replanOrFail();
574
+ }
575
+ }
576
+ else {
577
+ this.fail(result.ErrorMessage || 'Clone execution failed.', result.ResultCode);
578
+ }
579
+ }
580
+ catch (err) {
581
+ this.fail(err instanceof Error ? err.message : String(err));
582
+ }
583
+ finally {
584
+ this.cdr.markForCheck();
585
+ }
586
+ }
587
+ /** Shown on Review after an Execute found the plan out of date. Cleared by the next Execute or step change. */
588
+ PlanChangedNotice = null;
589
+ /** From the Failed state: back to Review with a fresh plan, since the old one may be why it failed. */
590
+ BackToReview() {
591
+ this.PlanChangedNotice = null;
592
+ this.CurrentStep = 'review';
593
+ this.CurrentState = 'review';
594
+ this.ScopeOptions = this.buildOptionsWithValues();
595
+ this.cdr.markForCheck();
596
+ void this.replanOrFail();
597
+ }
598
+ /** Key of the root clone once execution succeeded, as a record-id string. */
599
+ get TargetRecordKey() {
600
+ return this.ExecutionResult?.Roots?.[0]?.TargetKey || null;
601
+ }
602
+ // ── Template handlers ────────────────────────────────────────────────
603
+ /** Scope values the controls show: the server's effective options once a plan exists. */
604
+ get DisplayedScope() {
605
+ return { ...RecordClonePanelComponent.builtInScope(), ...(this.ActivePlan?.EffectiveOptions ?? {}) };
606
+ }
607
+ /**
608
+ * Keeps only the scope values the user changed from what is displayed, so the entity's
609
+ * configured defaults (and a preset's options) are not overwritten by values nobody chose.
610
+ * Picking a preset clears earlier scope changes so the preset's options apply.
611
+ */
612
+ OnScopeOptionsChanged(options) {
613
+ const next = this.scopeWithCurrentValues();
614
+ if (options.Preset !== this.SelectedPreset) {
615
+ for (const k of SCOPE_OPTION_KEYS)
616
+ delete next[k];
617
+ next.Preset = options.Preset;
618
+ }
619
+ else {
620
+ const shown = this.DisplayedScope;
621
+ for (const k of SCOPE_OPTION_KEYS) {
622
+ if (options[k] !== undefined && options[k] !== shown[k]) {
623
+ next[k] = options[k];
624
+ }
625
+ }
626
+ }
627
+ this.ScopeOptions = next;
628
+ this.SelectedPreset = options.Preset;
629
+ void this.replanOrFail();
630
+ }
631
+ /**
632
+ * A branch was switched in the plan tree. Switching a branch the user already changed drops that
633
+ * override (back to the configured policy); otherwise the change is remembered. Then re-plan.
634
+ */
635
+ OnEdgePolicyChanged(change) {
636
+ const others = this.EdgeOverrides.filter((o) => o.RelationshipID !== change.RelationshipID);
637
+ const hadOverride = others.length !== this.EdgeOverrides.length;
638
+ this.EdgeOverrides = hadOverride ? others : [...others, change];
639
+ void this.replanOrFail();
640
+ }
641
+ /** Drops one branch override so that relationship follows its configured policy again. */
642
+ OnUndoEdgeOverride(relationshipID) {
643
+ this.EdgeOverrides = this.EdgeOverrides.filter((o) => o.RelationshipID !== relationshipID);
644
+ void this.replanOrFail();
645
+ }
646
+ /** Drops every scope and branch override so the entity's configured scope applies again. */
647
+ OnResetScopeToDefaults() {
648
+ const next = this.scopeWithCurrentValues();
649
+ for (const k of SCOPE_OPTION_KEYS)
650
+ delete next[k];
651
+ this.ScopeOptions = next;
652
+ this.EdgeOverrides = [];
653
+ void this.replanOrFail();
654
+ }
655
+ OnStepChange(stepKey) {
656
+ this.GoToStep(stepKey);
657
+ }
658
+ OnRootNameChange(name) {
659
+ this.RootRecordName = name;
660
+ this.HasUserEditedRootName = true;
661
+ }
662
+ /**
663
+ * Keeps the root name in step with a prompted value while the user has not typed a name.
664
+ * Which prompted field drives it comes from the entity's `Clone.Naming.Fields` (for MJ: Users
665
+ * that is Email), falling back to the entity's name field.
666
+ */
667
+ OnPromptedValuesChange(values) {
668
+ this.PromptedValues = values;
669
+ if (this.HasUserEditedRootName)
670
+ return;
671
+ const drivingField = this.namingDrivenFields().find((f) => {
672
+ const v = values[f];
673
+ return typeof v === 'string' && v.trim().length > 0;
674
+ });
675
+ if (drivingField) {
676
+ this.RootRecordName = values[drivingField];
677
+ }
678
+ }
679
+ OnRetargetFieldsChange(fields) {
680
+ this.RetargetFields = fields;
681
+ }
682
+ OnReasonChange(reason) {
683
+ this.CloneReason = reason;
684
+ }
685
+ OnValuesValidityChange(isValid) {
686
+ this.IsValuesValid = isValid;
687
+ this.cdr.markForCheck();
688
+ }
689
+ OnNodeSelected(_node) {
690
+ // Reserved for a node detail pane.
691
+ }
692
+ OnReviewNodeClicked(_nodeKey) {
693
+ this.GoToStep('scope');
694
+ }
695
+ OnNavigateToRecord(event) {
696
+ this.NavigateToRecord.emit(event);
697
+ this.CloseRequested.emit();
698
+ }
699
+ OnClose() {
700
+ this.CloseRequested.emit();
701
+ }
702
+ // ── Internals ────────────────────────────────────────────────────────
703
+ /** Label and description of each preset, by key, read the way the engine reads `Clone.Presets`. */
704
+ static presetLabels(presets) {
705
+ return Object.fromEntries(NormalizeClonePresets(presets).map((p) => [p.Key, { Label: p.Label, Description: p.Description }]));
706
+ }
707
+ /** Options of the named preset, read the way the engine reads `Clone.Presets`. */
708
+ static presetOptions(presets, key) {
709
+ if (!key)
710
+ return {};
711
+ return NormalizeClonePresets(presets).find((p) => p.Key === key)?.Options ?? {};
712
+ }
713
+ /** Branch overrides in the shape the operation accepts (labels stripped), or undefined when there are none. */
714
+ wireEdgeOverrides() {
715
+ return this.EdgeOverrides.length > 0 ? this.EdgeOverrides.map(({ RelationshipID, Policy }) => ({ RelationshipID, Policy })) : undefined;
716
+ }
717
+ /** What the controls show before the first plan arrives; the server's effective options replace it. */
718
+ static builtInScope() {
719
+ return {
720
+ MaxDepth: 3,
721
+ MaxRecords: 500,
722
+ Subtypes: 'include',
723
+ Hierarchy: 'subtree',
724
+ SoftLinks: 'skip',
725
+ EntityActions: 'fire',
726
+ };
727
+ }
728
+ /** Re-plan from a UI event; a failure moves the wizard to Failed instead of leaving a stale plan up. */
729
+ async replanOrFail() {
730
+ try {
731
+ await this.Replan();
732
+ }
733
+ catch (err) {
734
+ this.fail(err instanceof Error ? err.message : String(err));
735
+ this.cdr.markForCheck();
736
+ }
737
+ }
738
+ autoStartQueued = false;
739
+ /** Coalesces the Record / EntityName / RecordKey setters into one start per change batch. */
740
+ queueAutoStart() {
741
+ if (!this.AutoStart || this.autoStartQueued)
742
+ return;
743
+ this.autoStartQueued = true;
744
+ queueMicrotask(() => {
745
+ this.autoStartQueued = false;
746
+ if (this.AutoStart && this.EffectiveRecordKey) {
747
+ void this.Start();
748
+ }
749
+ });
750
+ }
751
+ async initialize() {
752
+ this.HasUserEditedRootName = false;
753
+ this.CurrentState = 'loading';
754
+ this.CurrentStep = 'scope';
755
+ this.LoadingMessage = 'Inspecting entity cloning policy...';
756
+ this.cdr.markForCheck();
757
+ try {
758
+ const describe = await this.cloneService.DescribeRecord({
759
+ EntityName: this.EffectiveEntityName,
760
+ Key: this.EffectiveRecordKey,
761
+ }, this.ProviderToUse);
762
+ this.DescribeDetails = describe;
763
+ if (!describe.CanClone) {
764
+ this.CurrentState = 'not_cloneable';
765
+ this.NotCloneableReason = describe.Reason || 'Cloning is disabled for this entity.';
766
+ this.cdr.markForCheck();
767
+ return;
768
+ }
769
+ this.AvailablePresets = describe.Presets || [];
770
+ this.PresetLabels = RecordClonePanelComponent.presetLabels(this.ProviderToUse?.EntityByName(this.EffectiveEntityName)?.CloneConfig?.Presets);
771
+ this.CanOverrideScope = describe.CanOverrideScope === true;
772
+ this.LoadingMessage = 'Computing dependency graph and plan...';
773
+ this.cdr.markForCheck();
774
+ await this.Replan();
775
+ this.setupInitialValues();
776
+ this.CurrentState = 'scope';
777
+ this.cdr.markForCheck();
778
+ }
779
+ catch (err) {
780
+ this.fail(err instanceof Error ? err.message : String(err));
781
+ this.cdr.markForCheck();
782
+ }
783
+ }
784
+ /**
785
+ * Whether the configuration, not the user, decides the root's name: a rule derives it (a user's
786
+ * Name from the prompted Email), a reset stamps it, or UserEditable doesn't allow field edits.
787
+ * The server ignores a typed name then, so the box is read-only.
788
+ */
789
+ get RootNameReadOnly() {
790
+ const entInfo = this.ProviderToUse?.EntityByName(this.EffectiveEntityName);
791
+ const cfg = entInfo?.CloneConfig;
792
+ const nameField = (entInfo?.NameField?.Name ?? 'Name').toLowerCase();
793
+ const ruleTargets = cfg?.Fields?.Rules?.Rules ?? [];
794
+ const governed = ruleTargets.some((r) => (r.TargetField || r.Field || '').toLowerCase() === nameField)
795
+ || Object.keys(cfg?.Fields?.Reset ?? {}).some((k) => k.toLowerCase() === nameField);
796
+ const editable = cfg?.UserEditable ?? 'all';
797
+ return governed || editable === 'none' || editable === 'scope';
798
+ }
799
+ /** The last change on the root's name field in the current plan: the value the server applies. */
800
+ rootNameChange() {
801
+ const rootNode = this.ActivePlan?.Nodes.find((n) => n.Depth === 0 || n.ParentKey === null);
802
+ if (!rootNode)
803
+ return undefined;
804
+ const nameField = (this.ProviderToUse?.EntityByName(this.EffectiveEntityName)?.NameField?.Name ?? 'Name').toLowerCase();
805
+ return [...rootNode.FieldChanges].reverse().find((fc) => fc.Field.toLowerCase() === nameField);
806
+ }
807
+ setupInitialValues() {
808
+ if (!this.ActivePlan)
809
+ return;
810
+ const rootNode = this.ActivePlan.Nodes.find((n) => n.Depth === 0 || n.ParentKey === null);
811
+ if (rootNode) {
812
+ // The value the server will apply is the last change on the name field (its rename, when
813
+ // the naming strategy made one), not the first, which is the plain copy of the source.
814
+ const nameChange = this.rootNameChange();
815
+ if (nameChange) {
816
+ this.RootRecordName = String(nameChange.NewValue);
817
+ this.NamingStrategyReason = nameChange.Reason || 'Suggested by naming strategy';
818
+ }
819
+ else {
820
+ this.RootRecordName = rootNode.DisplayName ? `${rootNode.DisplayName} (Copy)` : 'Cloned Record';
821
+ }
822
+ }
823
+ const entInfo = this.ProviderToUse?.EntityByName(this.EffectiveEntityName);
824
+ const cloneCfg = entInfo?.CloneConfig;
825
+ const promptFor = cloneCfg?.Fields?.PromptFor;
826
+ if (Array.isArray(promptFor) && promptFor.length > 0) {
827
+ this.PromptedFields = promptFor.map((fName) => {
828
+ const fieldInfo = entInfo?.Fields.find((f) => f.Name === fName);
829
+ const fc = rootNode?.FieldChanges.find((c) => c.Field === fName);
830
+ // Pre-fill only a value the engine proposed. The source row's own value is exactly
831
+ // what a prompted field (a unique Email, say) must not keep, so it is never offered.
832
+ const proposed = fc && fc.NewValue !== fc.OldValue ? fc.NewValue : null;
833
+ return {
834
+ FieldName: fName,
835
+ DisplayName: fieldInfo?.DisplayName || fName,
836
+ Type: fieldInfo?.Type || 'string',
837
+ // The plan blocks without every PromptFor value (the copy can't reuse the source's).
838
+ IsRequired: true,
839
+ Description: fieldInfo?.Description,
840
+ DefaultValue: (proposed ?? null),
841
+ };
842
+ });
843
+ for (const pf of this.PromptedFields) {
844
+ if (this.PromptedValues[pf.FieldName] === undefined && pf.DefaultValue !== null && pf.DefaultValue !== undefined) {
845
+ this.PromptedValues[pf.FieldName] = pf.DefaultValue;
846
+ }
847
+ }
848
+ }
849
+ const retargetFieldNames = cloneCfg?.UI?.RetargetFields;
850
+ if (Array.isArray(retargetFieldNames) && retargetFieldNames.length > 0) {
851
+ this.RetargetFields = retargetFieldNames
852
+ .map((fName) => {
853
+ const fieldInfo = entInfo?.Fields.find((f) => f.Name === fName);
854
+ if (!fieldInfo)
855
+ return null;
856
+ const fc = rootNode?.FieldChanges.find((c) => c.Field === fName);
857
+ const currentVal = (fc?.OldValue ?? null);
858
+ return {
859
+ FieldName: fName,
860
+ DisplayName: fieldInfo.DisplayName || fName,
861
+ RelatedEntity: fieldInfo.RelatedEntity || '',
862
+ CurrentValue: currentVal,
863
+ NewValue: currentVal,
864
+ };
865
+ })
866
+ .filter((item) => item !== null);
867
+ }
868
+ }
869
+ /** Prompted fields whose value names the clone: `Clone.Naming.Fields` that are prompted, else the name field. */
870
+ namingDrivenFields() {
871
+ const entInfo = this.ProviderToUse?.EntityByName(this.EffectiveEntityName);
872
+ const prompted = new Set(this.PromptedFields.map((f) => f.FieldName.toLowerCase()));
873
+ const configured = (entInfo?.CloneConfig?.Naming?.Fields ?? []).filter((f) => prompted.has(f.toLowerCase()));
874
+ if (configured.length > 0)
875
+ return configured;
876
+ const nameField = entInfo?.NameField?.Name;
877
+ return nameField && prompted.has(nameField.toLowerCase()) ? [nameField] : [];
878
+ }
879
+ /** Current options plus the root name, prompted values and reason the user entered. */
880
+ /** Retarget pickers the user pointed at a different record, in the `Options.Retarget` shape. */
881
+ retargets() {
882
+ return this.RetargetFields
883
+ .filter((r) => r.NewValue && r.NewValue !== r.CurrentValue)
884
+ .map((r) => ({ EntityName: this.EffectiveEntityName, Field: r.FieldName, Value: r.NewValue }));
885
+ }
886
+ /**
887
+ * The scope options plus the Values step's current values, leaving out the ones that are empty:
888
+ * a Scope-step re-plan then carries no stale name or retarget, and nothing the user hasn't set.
889
+ */
890
+ scopeWithCurrentValues() {
891
+ const next = this.buildOptionsWithValues();
892
+ if (Object.keys(next.FieldOverrides ?? {}).length === 0)
893
+ delete next.FieldOverrides;
894
+ if (Object.keys(next.PromptedValues ?? {}).length === 0)
895
+ delete next.PromptedValues;
896
+ if (!next.Reason)
897
+ delete next.Reason;
898
+ return next;
899
+ }
900
+ buildOptionsWithValues() {
901
+ const entInfo = this.ProviderToUse?.EntityByName(this.EffectiveEntityName);
902
+ const nameField = entInfo?.NameField?.Name || 'Name';
903
+ const retarget = this.retargets();
904
+ // Values come from the current Values step every time, never from an earlier request's
905
+ // options: a retarget set back, or a typed name that was cleared, must not linger.
906
+ const { FieldOverrides: _fields, PromptedValues: _prompts, Retarget: _retarget, Reason: _reason, ...scope } = this.ScopeOptions;
907
+ return {
908
+ ...scope,
909
+ // Only a name the user typed overrides the server's naming, which avoids names already taken.
910
+ FieldOverrides: this.HasUserEditedRootName && this.RootRecordName.trim() ? { [nameField]: this.RootRecordName } : {},
911
+ ...(retarget.length > 0 ? { Retarget: retarget } : {}),
912
+ PromptedValues: { ...this.PromptedValues },
913
+ Reason: this.CloneReason,
914
+ };
915
+ }
916
+ /** The parts of the options that the Values step sets and that change the plan. */
917
+ planValuesKey(options) {
918
+ return JSON.stringify([options.FieldOverrides ?? {}, options.PromptedValues ?? {}, options.Retarget ?? []]);
919
+ }
920
+ fail(message, resultCode) {
921
+ this.CurrentState = 'failed';
922
+ this.ExecutionErrorMessage = message;
923
+ this.CloneFailed.emit({ EntityName: this.EffectiveEntityName, Message: message, ResultCode: resultCode });
924
+ }
925
+ /**
926
+ * The clone's rows were saved on the server, so no client-side BaseEntity raised a save.
927
+ * Announce each written entity the way the provider announces a server-side write
928
+ * (`remote-invalidate`, no `baseEntity`), so cached engines and open views reload it.
929
+ */
930
+ raiseRecordCreatedEvent(completed) {
931
+ const written = new Set([completed.EntityName, ...(completed.Result?.Created ?? []).map((c) => c.EntityName)]);
932
+ const timestamp = new Date().toISOString();
933
+ for (const entityName of written) {
934
+ const args = {
935
+ type: 'remote-invalidate',
936
+ entityName,
937
+ baseEntity: null,
938
+ provider: this.ProviderToUse,
939
+ payload: { primaryKeyValues: null, action: 'save', sourceServerId: '', timestamp },
940
+ };
941
+ MJGlobal.Instance.RaiseEvent({ component: this, event: MJEventType.ComponentEvent, eventCode: BaseEntity.BaseEventCode, args });
942
+ }
943
+ }
944
+ static ɵfac = /*@__PURE__*/ (() => { let ɵRecordClonePanelComponent_BaseFactory; return function RecordClonePanelComponent_Factory(__ngFactoryType__) { return (ɵRecordClonePanelComponent_BaseFactory || (ɵRecordClonePanelComponent_BaseFactory = i0.ɵɵgetInheritedFactory(RecordClonePanelComponent)))(__ngFactoryType__ || RecordClonePanelComponent); }; })();
945
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecordClonePanelComponent, selectors: [["mj-record-clone-panel"]], inputs: { Record: "Record", EntityName: "EntityName", RecordKey: "RecordKey", AutoStart: "AutoStart", ShowStepTabs: "ShowStepTabs" }, outputs: { StateChange: "StateChange", StepChange: "StepChange", PlanChanged: "PlanChanged", CloneCompleted: "CloneCompleted", CloneFailed: "CloneFailed", NavigateToRecord: "NavigateToRecord", CloseRequested: "CloseRequested" }, features: [i0.ɵɵInheritDefinitionFeature], decls: 11, vars: 9, consts: [[1, "clone-panel-content"], [1, "panel-tabs-wrapper"], [1, "panel-body"], [1, "panel-state-center"], [1, "panel-state-center", "not-cloneable-card"], [1, "step-container", 3, "step-hidden"], [1, "step-container"], [1, "panel-state-center", "failed-card"], [3, "TabChange", "Tabs", "ActiveKey"], ["Size", "medium", 3, "Text"], [1, "fa-solid", "fa-ban", "not-cloneable-icon"], [1, "state-title"], [1, "state-description"], ["type", "button", "mjButton", "", "variant", "outline", 3, "click"], [3, "ScopeChanged", "ResetToDefaults", "Presets", "PresetLabels", "SelectedPreset", "MaxDepth", "Subtypes", "Hierarchy", "SoftLinks", "EntityActions", "MaxRecords", "ConfiguredScope", "EntityName", "CanOverrideScope"], [1, "tree-section"], [1, "section-heading"], [3, "NodeSelected", "EdgePolicyChanged", "Plan", "AllowNarrowing", "AllowWidening"], [1, "branch-overrides"], [1, "tree-hint"], [1, "step-footer"], ["type", "button", "mjButton", "", "variant", "primary", 3, "click", "disabled"], [1, "fa-solid", "fa-arrow-right"], [1, "branch-override"], ["type", "button", 1, "undo-btn", 3, "click"], [3, "RootNameChange", "PromptedValuesChange", "RetargetFieldsChange", "ReasonChange", "ValidityChange", "EntityName", "RootName", "RootNameReadOnly", "NamingStrategyReason", "PromptedFields", "PromptedValues", "RetargetFields", "Reason"], [1, "fa-solid", "fa-arrow-left"], ["role", "status", 1, "plan-changed-notice"], [3, "Confirm", "Cancel", "NodeClicked", "Plan", "RootName", "Reason", "IsExecuting", "IsPlanning"], [1, "fa-solid", "fa-rotate"], [3, "Progress"], [3, "CloneAnother", "Close", "NavigateToRecord", "Result", "EntityName", "TargetKey", "RootRecordName"], [1, "fa-solid", "fa-circle-exclamation", "failed-icon"], [1, "state-title", "failed-text"], [1, "state-actions"], ["type", "button", "mjButton", "", "variant", "primary", 3, "click"]], template: function RecordClonePanelComponent_Template(rf, ctx) { if (rf & 1) {
946
+ i0.ɵɵelementStart(0, "div", 0);
947
+ i0.ɵɵconditionalCreate(1, RecordClonePanelComponent_Conditional_1_Template, 2, 2, "div", 1);
948
+ i0.ɵɵelementStart(2, "div", 2);
949
+ i0.ɵɵconditionalCreate(3, RecordClonePanelComponent_Conditional_3_Template, 2, 1, "div", 3);
950
+ i0.ɵɵconditionalCreate(4, RecordClonePanelComponent_Conditional_4_Template, 8, 1, "div", 4);
951
+ i0.ɵɵconditionalCreate(5, RecordClonePanelComponent_Conditional_5_Template, 13, 20, "div", 5);
952
+ i0.ɵɵconditionalCreate(6, RecordClonePanelComponent_Conditional_6_Template, 9, 11, "div", 5);
953
+ i0.ɵɵconditionalCreate(7, RecordClonePanelComponent_Conditional_7_Template, 4, 9, "div", 5);
954
+ i0.ɵɵconditionalCreate(8, RecordClonePanelComponent_Conditional_8_Template, 2, 1, "div", 6);
955
+ i0.ɵɵconditionalCreate(9, RecordClonePanelComponent_Conditional_9_Template, 2, 4, "div", 6);
956
+ i0.ɵɵconditionalCreate(10, RecordClonePanelComponent_Conditional_10_Template, 11, 1, "div", 7);
957
+ i0.ɵɵelementEnd()();
958
+ } if (rf & 2) {
959
+ i0.ɵɵadvance();
960
+ i0.ɵɵconditional(ctx.ShowWizardTabs ? 1 : -1);
961
+ i0.ɵɵadvance(2);
962
+ i0.ɵɵconditional(ctx.CurrentState === "loading" ? 3 : -1);
963
+ i0.ɵɵadvance();
964
+ i0.ɵɵconditional(ctx.CurrentState === "not_cloneable" ? 4 : -1);
965
+ i0.ɵɵadvance();
966
+ i0.ɵɵconditional(ctx.ShowWizardTabs && (ctx.CurrentState === "scope" || ctx.CurrentStep === "scope") ? 5 : -1);
967
+ i0.ɵɵadvance();
968
+ i0.ɵɵconditional(ctx.ShowWizardTabs && (ctx.CurrentState === "values" || ctx.CurrentStep === "values") ? 6 : -1);
969
+ i0.ɵɵadvance();
970
+ i0.ɵɵconditional((ctx.ShowWizardTabs || ctx.CurrentState === "executing") && (ctx.CurrentState === "review" || ctx.CurrentStep === "review") ? 7 : -1);
971
+ i0.ɵɵadvance();
972
+ i0.ɵɵconditional(ctx.CurrentState === "executing" ? 8 : -1);
973
+ i0.ɵɵadvance();
974
+ i0.ɵɵconditional(ctx.CurrentState === "done" ? 9 : -1);
975
+ i0.ɵɵadvance();
976
+ i0.ɵɵconditional(ctx.CurrentState === "failed" ? 10 : -1);
977
+ } }, dependencies: [CommonModule,
978
+ SharedGenericModule, i1.LoadingComponent, MJButtonDirective,
979
+ MJTabNavComponent,
980
+ CloneScopeControlsComponent,
981
+ ClonePlanTreeComponent,
982
+ CloneValuesComponent,
983
+ CloneReviewComponent,
984
+ CloneProgressComponent,
985
+ CloneResultComponent], styles: [".plan-changed-notice[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-bottom: var(--mj-space-3);\n padding: var(--mj-space-2) var(--mj-space-3);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-md);\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n }\n .clone-panel-content[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n }\n\n .panel-tabs-wrapper[_ngcontent-%COMP%] {\n padding: var(--mj-space-2) var(--mj-space-4);\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n }\n\n .panel-body[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4);\n display: flex;\n flex-direction: column;\n }\n\n .step-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-4);\n }\n\n .step-hidden[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .tree-section[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-1-5);\n margin-top: var(--mj-space-1);\n }\n\n .branch-overrides[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: var(--mj-space-2);\n }\n\n .branch-override[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: 2px 4px 2px var(--mj-space-2-5);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-full);\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n font-size: var(--mj-text-xs);\n }\n\n .undo-btn[_ngcontent-%COMP%] {\n border: 0;\n border-radius: var(--mj-radius-full);\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-family: inherit;\n font-size: var(--mj-text-xs);\n padding: 1px var(--mj-space-2);\n cursor: pointer;\n }\n\n .undo-btn[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n }\n\n .tree-hint[_ngcontent-%COMP%] {\n margin: 0;\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n }\n\n .section-heading[_ngcontent-%COMP%] {\n margin: 0;\n font-size: var(--mj-text-sm);\n font-weight: 600;\n color: var(--mj-text-primary);\n }\n\n .step-footer[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: var(--mj-space-4);\n border-top: 1px solid var(--mj-border-default);\n margin-top: var(--mj-space-4);\n }\n\n .panel-state-center[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: var(--mj-space-12) var(--mj-space-6);\n gap: var(--mj-space-3);\n }\n\n .not-cloneable-icon[_ngcontent-%COMP%] {\n font-size: 36px;\n color: var(--mj-status-warning-text);\n }\n\n .failed-icon[_ngcontent-%COMP%] {\n font-size: 36px;\n color: var(--mj-status-error-text);\n }\n\n .state-title[_ngcontent-%COMP%] {\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: 700;\n color: var(--mj-text-primary);\n }\n\n .failed-text[_ngcontent-%COMP%] {\n color: var(--mj-status-error-text);\n }\n\n .state-description[_ngcontent-%COMP%] {\n margin: 0;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n max-width: 400px;\n }\n\n .state-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: var(--mj-space-2);\n margin-top: var(--mj-space-2);\n }"], changeDetection: 0 });
986
+ }
987
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RecordClonePanelComponent, [{
988
+ type: Component,
989
+ args: [{ standalone: true, selector: 'mj-record-clone-panel', template: `
990
+ <div class="clone-panel-content">
991
+ <!-- Wizard Navigation Tabs (visible during authoring steps) -->
992
+ @if (ShowWizardTabs) {
993
+ <div class="panel-tabs-wrapper">
994
+ <mj-tab-nav
995
+ [Tabs]="WizardTabs"
996
+ [ActiveKey]="CurrentStep"
997
+ (TabChange)="OnStepChange($event)">
998
+ </mj-tab-nav>
999
+ </div>
1000
+ }
1001
+
1002
+ <!-- Step Body -->
1003
+ <div class="panel-body">
1004
+ <!-- Loading State -->
1005
+ @if (CurrentState === 'loading') {
1006
+ <div class="panel-state-center">
1007
+ <mj-loading [Text]="LoadingMessage" Size="medium"></mj-loading>
1008
+ </div>
1009
+ }
1010
+
1011
+ <!-- Not Cloneable State -->
1012
+ @if (CurrentState === 'not_cloneable') {
1013
+ <div class="panel-state-center not-cloneable-card">
1014
+ <i class="fa-solid fa-ban not-cloneable-icon"></i>
1015
+ <h3 class="state-title">Record Cannot Be Cloned</h3>
1016
+ <p class="state-description">{{NotCloneableReason}}</p>
1017
+ <button
1018
+ type="button"
1019
+ mjButton
1020
+ variant="outline"
1021
+ (click)="OnClose()">
1022
+ Close
1023
+ </button>
1024
+ </div>
1025
+ }
1026
+
1027
+ <!-- Scope Step -->
1028
+ <!-- Wizard steps only while the wizard is active: after Execute the result (or failure) replaces them. -->
1029
+ @if (ShowWizardTabs && (CurrentState === 'scope' || CurrentStep === 'scope')) {
1030
+ <div class="step-container" [class.step-hidden]="CurrentStep !== 'scope'">
1031
+ <mj-clone-scope-controls
1032
+ [Presets]="AvailablePresets"
1033
+ [PresetLabels]="PresetLabels"
1034
+ [SelectedPreset]="SelectedPreset"
1035
+ [MaxDepth]="DisplayedScope.MaxDepth"
1036
+ [Subtypes]="DisplayedScope.Subtypes"
1037
+ [Hierarchy]="DisplayedScope.Hierarchy"
1038
+ [SoftLinks]="DisplayedScope.SoftLinks"
1039
+ [EntityActions]="DisplayedScope.EntityActions"
1040
+ [MaxRecords]="DisplayedScope.MaxRecords"
1041
+ [ConfiguredScope]="ConfiguredScope"
1042
+ [EntityName]="EffectiveEntityName"
1043
+ [CanOverrideScope]="CanOverrideScope"
1044
+ (ScopeChanged)="OnScopeOptionsChanged($event)"
1045
+ (ResetToDefaults)="OnResetScopeToDefaults()">
1046
+ </mj-clone-scope-controls>
1047
+
1048
+ <div class="tree-section">
1049
+ <h4 class="section-heading">Planned Record Graph</h4>
1050
+ <mj-clone-plan-tree
1051
+ [Plan]="ActivePlan"
1052
+ [AllowNarrowing]="CanNarrowScope"
1053
+ [AllowWidening]="CanOverrideScope"
1054
+ (NodeSelected)="OnNodeSelected($event)"
1055
+ (EdgePolicyChanged)="OnEdgePolicyChanged($event)">
1056
+ </mj-clone-plan-tree>
1057
+ @if (EdgeOverrides.length > 0) {
1058
+ <div class="branch-overrides">
1059
+ @for (o of EdgeOverrides; track o.RelationshipID) {
1060
+ <span class="branch-override">
1061
+ {{ o.Policy === 'Skip' ? 'Skipping' : 'Copying' }} {{ o.RelatedEntityName || 'a relationship' }}
1062
+ <button type="button" class="undo-btn" (click)="OnUndoEdgeOverride(o.RelationshipID)"
1063
+ [attr.aria-label]="'Undo: ' + (o.Policy === 'Skip' ? 'copy ' : 'skip ') + (o.RelatedEntityName || 'this relationship') + ' again'">Undo</button>
1064
+ </span>
1065
+ }
1066
+ </div>
1067
+ }
1068
+ @if (CanNarrowScope) {
1069
+ <p class="tree-hint">Click Deep on a branch to skip all of its rows.{{ CanOverrideScope ? ' Click Skip to copy a branch the configuration leaves out.' : '' }}</p>
1070
+ }
1071
+ </div>
1072
+
1073
+ <div class="step-footer">
1074
+ <div></div>
1075
+ <button
1076
+ type="button"
1077
+ mjButton
1078
+ variant="primary"
1079
+ [disabled]="!ActivePlan"
1080
+ (click)="GoToStep('values')">
1081
+ Next: Values & Retarget
1082
+ <i class="fa-solid fa-arrow-right"></i>
1083
+ </button>
1084
+ </div>
1085
+ </div>
1086
+ }
1087
+
1088
+ <!-- Values Step -->
1089
+ @if (ShowWizardTabs && (CurrentState === 'values' || CurrentStep === 'values')) {
1090
+ <div class="step-container" [class.step-hidden]="CurrentStep !== 'values'">
1091
+ <mj-clone-values
1092
+ [EntityName]="EffectiveEntityName"
1093
+ [RootName]="RootRecordName"
1094
+ [RootNameReadOnly]="RootNameReadOnly"
1095
+ [NamingStrategyReason]="NamingStrategyReason"
1096
+ [PromptedFields]="PromptedFields"
1097
+ [PromptedValues]="PromptedValues"
1098
+ [RetargetFields]="RetargetFields"
1099
+ [Reason]="CloneReason"
1100
+ (RootNameChange)="OnRootNameChange($event)"
1101
+ (PromptedValuesChange)="OnPromptedValuesChange($event)"
1102
+ (RetargetFieldsChange)="OnRetargetFieldsChange($event)"
1103
+ (ReasonChange)="OnReasonChange($event)"
1104
+ (ValidityChange)="OnValuesValidityChange($event)">
1105
+ </mj-clone-values>
1106
+
1107
+ <div class="step-footer">
1108
+ <button
1109
+ type="button"
1110
+ mjButton
1111
+ variant="outline"
1112
+ (click)="GoToStep('scope')">
1113
+ <i class="fa-solid fa-arrow-left"></i>
1114
+ Back to Scope
1115
+ </button>
1116
+ <button
1117
+ type="button"
1118
+ mjButton
1119
+ variant="primary"
1120
+ [disabled]="!IsValuesValid"
1121
+ (click)="GoToStep('review')">
1122
+ Next: Review & Execute
1123
+ <i class="fa-solid fa-arrow-right"></i>
1124
+ </button>
1125
+ </div>
1126
+ </div>
1127
+ }
1128
+
1129
+ <!-- Review Step -->
1130
+ @if ((ShowWizardTabs || CurrentState === 'executing') && (CurrentState === 'review' || CurrentStep === 'review')) {
1131
+ <div class="step-container" [class.step-hidden]="CurrentStep !== 'review'">
1132
+ @if (PlanChangedNotice) {
1133
+ <div class="plan-changed-notice" role="status">
1134
+ <i class="fa-solid fa-rotate"></i>
1135
+ <span>{{ PlanChangedNotice }}</span>
1136
+ </div>
1137
+ }
1138
+ <mj-clone-review
1139
+ [Plan]="ActivePlan"
1140
+ [RootName]="RootRecordName"
1141
+ [Reason]="CloneReason"
1142
+ [IsExecuting]="CurrentState === 'executing'"
1143
+ [IsPlanning]="IsReplanning"
1144
+ (Confirm)="ExecuteClone()"
1145
+ (Cancel)="OnClose()"
1146
+ (NodeClicked)="OnReviewNodeClicked($event)">
1147
+ </mj-clone-review>
1148
+
1149
+ @if (CurrentState !== 'executing') {
1150
+ <div class="step-footer">
1151
+ <button
1152
+ type="button"
1153
+ mjButton
1154
+ variant="outline"
1155
+ (click)="GoToStep('values')">
1156
+ <i class="fa-solid fa-arrow-left"></i>
1157
+ Back to Values
1158
+ </button>
1159
+ </div>
1160
+ }
1161
+ </div>
1162
+ }
1163
+
1164
+ <!-- Executing State -->
1165
+ @if (CurrentState === 'executing') {
1166
+ <div class="step-container">
1167
+ <mj-clone-progress
1168
+ [Progress]="ExecutionProgress">
1169
+ </mj-clone-progress>
1170
+ </div>
1171
+ }
1172
+
1173
+ <!-- Done State -->
1174
+ @if (CurrentState === 'done') {
1175
+ <div class="step-container">
1176
+ <mj-clone-result
1177
+ [Result]="ExecutionResult"
1178
+ [EntityName]="EffectiveEntityName"
1179
+ [TargetKey]="TargetRecordKey"
1180
+ [RootRecordName]="RootRecordName"
1181
+ (CloneAnother)="Reset()"
1182
+ (Close)="OnClose()"
1183
+ (NavigateToRecord)="OnNavigateToRecord($event)">
1184
+ </mj-clone-result>
1185
+ </div>
1186
+ }
1187
+
1188
+ <!-- Failed State -->
1189
+ @if (CurrentState === 'failed') {
1190
+ <div class="panel-state-center failed-card">
1191
+ <i class="fa-solid fa-circle-exclamation failed-icon"></i>
1192
+ <h3 class="state-title failed-text">Execution Error</h3>
1193
+ <p class="state-description">{{ExecutionErrorMessage}}</p>
1194
+ <div class="state-actions">
1195
+ <button
1196
+ type="button"
1197
+ mjButton
1198
+ variant="primary"
1199
+ (click)="BackToReview()">
1200
+ Back to Review
1201
+ </button>
1202
+ <button
1203
+ type="button"
1204
+ mjButton
1205
+ variant="outline"
1206
+ (click)="OnClose()">
1207
+ Close
1208
+ </button>
1209
+ </div>
1210
+ </div>
1211
+ }
1212
+ </div>
1213
+ </div>
1214
+ `, imports: [
1215
+ CommonModule,
1216
+ SharedGenericModule,
1217
+ MJButtonDirective,
1218
+ MJTabNavComponent,
1219
+ CloneScopeControlsComponent,
1220
+ ClonePlanTreeComponent,
1221
+ CloneValuesComponent,
1222
+ CloneReviewComponent,
1223
+ CloneProgressComponent,
1224
+ CloneResultComponent,
1225
+ ], changeDetection: ChangeDetectionStrategy.OnPush, styles: ["\n .plan-changed-notice {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-bottom: var(--mj-space-3);\n padding: var(--mj-space-2) var(--mj-space-3);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-md);\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n }\n .clone-panel-content {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n }\n\n .panel-tabs-wrapper {\n padding: var(--mj-space-2) var(--mj-space-4);\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n }\n\n .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4);\n display: flex;\n flex-direction: column;\n }\n\n .step-container {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-4);\n }\n\n .step-hidden {\n display: none !important;\n }\n\n .tree-section {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-1-5);\n margin-top: var(--mj-space-1);\n }\n\n .branch-overrides {\n display: flex;\n flex-wrap: wrap;\n gap: var(--mj-space-2);\n }\n\n .branch-override {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: 2px 4px 2px var(--mj-space-2-5);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-full);\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n font-size: var(--mj-text-xs);\n }\n\n .undo-btn {\n border: 0;\n border-radius: var(--mj-radius-full);\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-family: inherit;\n font-size: var(--mj-text-xs);\n padding: 1px var(--mj-space-2);\n cursor: pointer;\n }\n\n .undo-btn:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n }\n\n .tree-hint {\n margin: 0;\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n }\n\n .section-heading {\n margin: 0;\n font-size: var(--mj-text-sm);\n font-weight: 600;\n color: var(--mj-text-primary);\n }\n\n .step-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: var(--mj-space-4);\n border-top: 1px solid var(--mj-border-default);\n margin-top: var(--mj-space-4);\n }\n\n .panel-state-center {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: var(--mj-space-12) var(--mj-space-6);\n gap: var(--mj-space-3);\n }\n\n .not-cloneable-icon {\n font-size: 36px;\n color: var(--mj-status-warning-text);\n }\n\n .failed-icon {\n font-size: 36px;\n color: var(--mj-status-error-text);\n }\n\n .state-title {\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: 700;\n color: var(--mj-text-primary);\n }\n\n .failed-text {\n color: var(--mj-status-error-text);\n }\n\n .state-description {\n margin: 0;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n max-width: 400px;\n }\n\n .state-actions {\n display: flex;\n gap: var(--mj-space-2);\n margin-top: var(--mj-space-2);\n }\n "] }]
1226
+ }], null, { Record: [{
1227
+ type: Input
1228
+ }], EntityName: [{
1229
+ type: Input
1230
+ }], RecordKey: [{
1231
+ type: Input
1232
+ }], AutoStart: [{
1233
+ type: Input
1234
+ }], ShowStepTabs: [{
1235
+ type: Input
1236
+ }], StateChange: [{
1237
+ type: Output
1238
+ }], StepChange: [{
1239
+ type: Output
1240
+ }], PlanChanged: [{
1241
+ type: Output
1242
+ }], CloneCompleted: [{
1243
+ type: Output
1244
+ }], CloneFailed: [{
1245
+ type: Output
1246
+ }], NavigateToRecord: [{
1247
+ type: Output
1248
+ }], CloseRequested: [{
1249
+ type: Output
1250
+ }] }); })();
1251
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RecordClonePanelComponent, { className: "RecordClonePanelComponent", filePath: "src/lib/record-clone-panel.component.ts", lineNumber: 480 }); })();
1252
+ //# sourceMappingURL=record-clone-panel.component.js.map