@memberjunction/ng-base-forms 5.37.0 → 5.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/base-form-component.d.ts +28 -0
- package/dist/lib/base-form-component.d.ts.map +1 -1
- package/dist/lib/base-form-component.js +37 -0
- package/dist/lib/base-form-component.js.map +1 -1
- package/dist/lib/container/record-form-container.component.d.ts +63 -1
- package/dist/lib/container/record-form-container.component.d.ts.map +1 -1
- package/dist/lib/container/record-form-container.component.js +134 -42
- package/dist/lib/container/record-form-container.component.js.map +1 -1
- package/dist/lib/interactive-form/interactive-form.component.d.ts +181 -0
- package/dist/lib/interactive-form/interactive-form.component.d.ts.map +1 -0
- package/dist/lib/interactive-form/interactive-form.component.js +560 -0
- package/dist/lib/interactive-form/interactive-form.component.js.map +1 -0
- package/dist/lib/panel-slot/base-form-panel.d.ts +97 -0
- package/dist/lib/panel-slot/base-form-panel.d.ts.map +1 -0
- package/dist/lib/panel-slot/base-form-panel.js +64 -0
- package/dist/lib/panel-slot/base-form-panel.js.map +1 -0
- package/dist/lib/panel-slot/form-panel-slot.component.d.ts +78 -0
- package/dist/lib/panel-slot/form-panel-slot.component.d.ts.map +1 -0
- package/dist/lib/panel-slot/form-panel-slot.component.js +227 -0
- package/dist/lib/panel-slot/form-panel-slot.component.js.map +1 -0
- package/dist/lib/panel-slot/form-slot-coordinator.service.d.ts +57 -0
- package/dist/lib/panel-slot/form-slot-coordinator.service.d.ts.map +1 -0
- package/dist/lib/panel-slot/form-slot-coordinator.service.js +104 -0
- package/dist/lib/panel-slot/form-slot-coordinator.service.js.map +1 -0
- package/dist/lib/toolbar/form-toolbar.component.d.ts +56 -1
- package/dist/lib/toolbar/form-toolbar.component.d.ts.map +1 -1
- package/dist/lib/toolbar/form-toolbar.component.js +294 -136
- package/dist/lib/toolbar/form-toolbar.component.js.map +1 -1
- package/dist/lib/types/navigation-events.d.ts +19 -1
- package/dist/lib/types/navigation-events.d.ts.map +1 -1
- package/dist/lib/types/navigation-events.js.map +1 -1
- package/dist/lib/types/toolbar-config.d.ts +11 -0
- package/dist/lib/types/toolbar-config.d.ts.map +1 -1
- package/dist/lib/types/toolbar-config.js +3 -0
- package/dist/lib/types/toolbar-config.js.map +1 -1
- package/dist/module.d.ts +10 -7
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +21 -7
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +4 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +7 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +12 -10
- package/dist/__tests__/sanity.test.d.ts +0 -2
- package/dist/__tests__/sanity.test.d.ts.map +0 -1
- package/dist/__tests__/sanity.test.js +0 -12
- package/dist/__tests__/sanity.test.js.map +0 -1
- package/dist/__tests__/tag-count-badge.test.d.ts +0 -2
- package/dist/__tests__/tag-count-badge.test.d.ts.map +0 -1
- package/dist/__tests__/tag-count-badge.test.js +0 -70
- package/dist/__tests__/tag-count-badge.test.js.map +0 -1
- package/dist/lib/__tests__/base-forms.test.d.ts +0 -2
- package/dist/lib/__tests__/base-forms.test.d.ts.map +0 -1
- package/dist/lib/__tests__/base-forms.test.js +0 -256
- package/dist/lib/__tests__/base-forms.test.js.map +0 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild, inject } from '@angular/core';
|
|
2
|
+
import { LogError, RunView } from '@memberjunction/core';
|
|
3
|
+
import { InteractiveFormsEngine } from '@memberjunction/core-entities';
|
|
4
|
+
import { FormEventNames, FormMethodNames, isFormRole, } from '@memberjunction/interactive-component-types/forms';
|
|
5
|
+
import { SimpleEntityFieldInfo } from '@memberjunction/interactive-component-types';
|
|
6
|
+
import { ReactBridgeService } from '@memberjunction/ng-react';
|
|
7
|
+
import { BaseFormComponent } from '../base-form-component';
|
|
8
|
+
import * as i0 from "@angular/core";
|
|
9
|
+
import * as i1 from "@memberjunction/ng-react";
|
|
10
|
+
import * as i2 from "../container/record-form-container.component";
|
|
11
|
+
const _c0 = ["reactComponent"];
|
|
12
|
+
function InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
13
|
+
i0.ɵɵelementStart(0, "div", 2);
|
|
14
|
+
i0.ɵɵelement(1, "i", 5);
|
|
15
|
+
i0.ɵɵelementStart(2, "span");
|
|
16
|
+
i0.ɵɵtext(3);
|
|
17
|
+
i0.ɵɵelementEnd()();
|
|
18
|
+
} if (rf & 2) {
|
|
19
|
+
const ctx_r0 = i0.ɵɵnextContext(3);
|
|
20
|
+
i0.ɵɵadvance(3);
|
|
21
|
+
i0.ɵɵtextInterpolate(ctx_r0.loadError);
|
|
22
|
+
} }
|
|
23
|
+
function InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
24
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
25
|
+
i0.ɵɵelementStart(0, "mj-react-component", 6, 0);
|
|
26
|
+
i0.ɵɵlistener("componentEvent", function InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_1_Template_mj_react_component_componentEvent_0_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnReactComponentEvent($event)); });
|
|
27
|
+
i0.ɵɵelementEnd();
|
|
28
|
+
} if (rf & 2) {
|
|
29
|
+
const ctx_r0 = i0.ɵɵnextContext(3);
|
|
30
|
+
i0.ɵɵproperty("component", ctx_r0.componentSpec)("componentProps", ctx_r0.formHostProps);
|
|
31
|
+
} }
|
|
32
|
+
function InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
33
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
34
|
+
i0.ɵɵelement(1, "i", 7);
|
|
35
|
+
i0.ɵɵelementStart(2, "span");
|
|
36
|
+
i0.ɵɵtext(3, "Loading form\u2026");
|
|
37
|
+
i0.ɵɵelementEnd()();
|
|
38
|
+
} }
|
|
39
|
+
function InteractiveFormComponent_Conditional_0_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
40
|
+
i0.ɵɵconditionalCreate(0, InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_0_Template, 4, 1, "div", 2)(1, InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_1_Template, 2, 2, "mj-react-component", 3)(2, InteractiveFormComponent_Conditional_0_Conditional_0_Conditional_2_Template, 4, 0, "div", 4);
|
|
41
|
+
} if (rf & 2) {
|
|
42
|
+
const ctx_r0 = i0.ɵɵnextContext(2);
|
|
43
|
+
i0.ɵɵconditional(ctx_r0.loadError ? 0 : ctx_r0.componentSpec && ctx_r0.formHostProps ? 1 : 2);
|
|
44
|
+
} }
|
|
45
|
+
function InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
46
|
+
i0.ɵɵelementStart(0, "div", 2);
|
|
47
|
+
i0.ɵɵelement(1, "i", 5);
|
|
48
|
+
i0.ɵɵelementStart(2, "span");
|
|
49
|
+
i0.ɵɵtext(3);
|
|
50
|
+
i0.ɵɵelementEnd()();
|
|
51
|
+
} if (rf & 2) {
|
|
52
|
+
const ctx_r0 = i0.ɵɵnextContext(3);
|
|
53
|
+
i0.ɵɵadvance(3);
|
|
54
|
+
i0.ɵɵtextInterpolate(ctx_r0.loadError);
|
|
55
|
+
} }
|
|
56
|
+
function InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
57
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
58
|
+
i0.ɵɵelementStart(0, "mj-react-component", 6, 0);
|
|
59
|
+
i0.ɵɵlistener("componentEvent", function InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_2_Template_mj_react_component_componentEvent_0_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnReactComponentEvent($event)); });
|
|
60
|
+
i0.ɵɵelementEnd();
|
|
61
|
+
} if (rf & 2) {
|
|
62
|
+
const ctx_r0 = i0.ɵɵnextContext(3);
|
|
63
|
+
i0.ɵɵproperty("component", ctx_r0.componentSpec)("componentProps", ctx_r0.formHostProps);
|
|
64
|
+
} }
|
|
65
|
+
function InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
66
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
67
|
+
i0.ɵɵelement(1, "i", 7);
|
|
68
|
+
i0.ɵɵelementStart(2, "span");
|
|
69
|
+
i0.ɵɵtext(3, "Loading form\u2026");
|
|
70
|
+
i0.ɵɵelementEnd()();
|
|
71
|
+
} }
|
|
72
|
+
function InteractiveFormComponent_Conditional_0_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
73
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
74
|
+
i0.ɵɵelementStart(0, "mj-record-form-container", 8);
|
|
75
|
+
i0.ɵɵlistener("Navigate", function InteractiveFormComponent_Conditional_0_Conditional_1_Template_mj_record_form_container_Navigate_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnFormNavigate($event)); })("DeleteRequested", function InteractiveFormComponent_Conditional_0_Conditional_1_Template_mj_record_form_container_DeleteRequested_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnDeleteRequested()); })("FavoriteToggled", function InteractiveFormComponent_Conditional_0_Conditional_1_Template_mj_record_form_container_FavoriteToggled_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnFavoriteToggled()); })("HistoryRequested", function InteractiveFormComponent_Conditional_0_Conditional_1_Template_mj_record_form_container_HistoryRequested_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnHistoryRequested()); })("ListManagementRequested", function InteractiveFormComponent_Conditional_0_Conditional_1_Template_mj_record_form_container_ListManagementRequested_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnListManagementRequested()); });
|
|
76
|
+
i0.ɵɵconditionalCreate(1, InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_1_Template, 4, 1, "div", 2)(2, InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_2_Template, 2, 2, "mj-react-component", 3)(3, InteractiveFormComponent_Conditional_0_Conditional_1_Conditional_3_Template, 4, 0, "div", 4);
|
|
77
|
+
i0.ɵɵelementEnd();
|
|
78
|
+
} if (rf & 2) {
|
|
79
|
+
const ctx_r0 = i0.ɵɵnextContext(2);
|
|
80
|
+
i0.ɵɵproperty("Record", ctx_r0.record)("FormComponent", ctx_r0);
|
|
81
|
+
i0.ɵɵadvance();
|
|
82
|
+
i0.ɵɵconditional(ctx_r0.loadError ? 1 : ctx_r0.componentSpec && ctx_r0.formHostProps ? 2 : 3);
|
|
83
|
+
} }
|
|
84
|
+
function InteractiveFormComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
85
|
+
i0.ɵɵconditionalCreate(0, InteractiveFormComponent_Conditional_0_Conditional_0_Template, 3, 1)(1, InteractiveFormComponent_Conditional_0_Conditional_1_Template, 4, 3, "mj-record-form-container", 1);
|
|
86
|
+
} if (rf & 2) {
|
|
87
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
88
|
+
i0.ɵɵconditional(ctx_r0.previewMode ? 0 : 1);
|
|
89
|
+
} }
|
|
90
|
+
/**
|
|
91
|
+
* Wraps a Component (the IC kind — declared `componentRole: 'form'`) so it can
|
|
92
|
+
* stand in for an Angular form on any entity.
|
|
93
|
+
*
|
|
94
|
+
* **Layering invariant:** the React component never touches BaseEntity. This
|
|
95
|
+
* wrapper owns the entity lifecycle (`record.Save`, `record.Delete`, mode
|
|
96
|
+
* transitions). The React component sees only the snapshot in
|
|
97
|
+
* {@link FormHostProps} and communicates back via `BeforeSave` /
|
|
98
|
+
* `BeforeDelete` / `EditModeChangeRequested` events plus its dirty-field diff.
|
|
99
|
+
*
|
|
100
|
+
* Instantiated by `SingleRecordComponent.LoadForm` when the resolver finds an
|
|
101
|
+
* `EntityFormOverride` matching the user's scope. Behaves exactly like the
|
|
102
|
+
* generated Angular form from the host's perspective: same `record` setter,
|
|
103
|
+
* same `EditMode` toggle, same outputs.
|
|
104
|
+
*/
|
|
105
|
+
export class InteractiveFormComponent extends BaseFormComponent {
|
|
106
|
+
/**
|
|
107
|
+
* The bound record. Backed by a setter so swapping records *after*
|
|
108
|
+
* the component has mounted (e.g. Form Builder cockpit preview record
|
|
109
|
+
* picker) triggers a fresh `originalSnapshot` capture + a
|
|
110
|
+
* `formHostProps` rebuild — without this, the React form keeps
|
|
111
|
+
* rendering the original record's values even after the input
|
|
112
|
+
* changes. Guarded on `_recordInitialized` so the post-mount path
|
|
113
|
+
* doesn't fire during the first input assignment (handled by
|
|
114
|
+
* `ngOnInit`, which sets both `originalSnapshot` and `formHostProps`).
|
|
115
|
+
*/
|
|
116
|
+
_record;
|
|
117
|
+
_recordInitialized = false;
|
|
118
|
+
set record(value) {
|
|
119
|
+
if (this._record === value)
|
|
120
|
+
return;
|
|
121
|
+
this._record = value;
|
|
122
|
+
if (this._recordInitialized && value) {
|
|
123
|
+
this.originalSnapshot = value.GetAll();
|
|
124
|
+
this.rebuildFormHostProps();
|
|
125
|
+
this.cdr.markForCheck();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
get record() { return this._record; }
|
|
129
|
+
/**
|
|
130
|
+
* The Component row ID resolved for this (entity, user, roles) tuple. Set
|
|
131
|
+
* by `SingleRecordComponent.LoadForm` after the resolver returns an
|
|
132
|
+
* override.
|
|
133
|
+
*/
|
|
134
|
+
ComponentID = null;
|
|
135
|
+
/**
|
|
136
|
+
* Loaded ComponentSpec, parsed from the Component row's `Specification` JSON.
|
|
137
|
+
*
|
|
138
|
+
* Two ways to populate this:
|
|
139
|
+
* 1. **Via `ComponentID`** (the standard path): set the input, ngOnInit
|
|
140
|
+
* fetches the row from `MJ: Components` and assigns the parsed spec.
|
|
141
|
+
* 2. **Direct assignment** (the artifact-viewer path): the caller already
|
|
142
|
+
* has the spec in hand (parsed from an artifact JSON blob) and sets
|
|
143
|
+
* `componentSpec` directly. In that case `ComponentID` should be left
|
|
144
|
+
* null and the DB fetch is skipped.
|
|
145
|
+
*/
|
|
146
|
+
componentSpec = null;
|
|
147
|
+
/**
|
|
148
|
+
* When true, render the React component **without** the
|
|
149
|
+
* `<mj-record-form-container>` wrapper — i.e. no toolbar (no Save /
|
|
150
|
+
* Cancel / Edit / Delete / History / Tags / Lists).
|
|
151
|
+
*
|
|
152
|
+
* Used by the Form Builder cockpit's Preview tab where the toolbar's
|
|
153
|
+
* actions don't apply (Save would save the *record*, but the cockpit's
|
|
154
|
+
* own Save button saves the *spec*; History/Tags assume a real
|
|
155
|
+
* persistent record, but the preview record can be a synthetic
|
|
156
|
+
* NewRecord). Keeping just the form body produces a clean "this is
|
|
157
|
+
* what the form looks like" view.
|
|
158
|
+
*/
|
|
159
|
+
previewMode = false;
|
|
160
|
+
/** FormHostProps passed to the React component. Recomputed when record or mode changes. */
|
|
161
|
+
formHostProps = null;
|
|
162
|
+
/** Loaded-spec error (component row missing, JSON parse failure, etc.). */
|
|
163
|
+
_loadError = null;
|
|
164
|
+
/**
|
|
165
|
+
* Loaded-spec error (component row missing, JSON parse failure,
|
|
166
|
+
* React-runtime bootstrap failure, etc.). Setter auto-emits
|
|
167
|
+
* `LoadErrorChanged` so host surfaces (e.g. Form Builder cockpit
|
|
168
|
+
* Preview tab) can show their own error state — retrospective fix #10.
|
|
169
|
+
*/
|
|
170
|
+
get loadError() { return this._loadError; }
|
|
171
|
+
set loadError(v) {
|
|
172
|
+
if (v === this._loadError)
|
|
173
|
+
return;
|
|
174
|
+
this._loadError = v;
|
|
175
|
+
this.LoadErrorChanged.emit(v);
|
|
176
|
+
}
|
|
177
|
+
LoadErrorChanged = new EventEmitter();
|
|
178
|
+
/** Tracks last-known field snapshot for computing the dirty-field diff on save. */
|
|
179
|
+
originalSnapshot = {};
|
|
180
|
+
/**
|
|
181
|
+
* Reference to the mounted React component. Used by the SaveRecord /
|
|
182
|
+
* CancelEdit overrides below to invoke the component's registered
|
|
183
|
+
* `RequestSave` / `RequestCancel` methods when the host toolbar fires
|
|
184
|
+
* Save/Cancel — so the single user-visible Save lives on the toolbar
|
|
185
|
+
* (consistent with every other entity form in MJ) and not duplicated
|
|
186
|
+
* inside the form body.
|
|
187
|
+
*/
|
|
188
|
+
reactComponent;
|
|
189
|
+
/**
|
|
190
|
+
* Promise resolver populated when `SaveRecord` invokes `RequestSave`
|
|
191
|
+
* on the React component. `handleBeforeSave` resolves it after the
|
|
192
|
+
* BeforeSave-driven entity save completes, so SaveRecord can return
|
|
193
|
+
* a meaningful success boolean and honor `StopEditModeAfterSave`.
|
|
194
|
+
*/
|
|
195
|
+
pendingSaveResolver = null;
|
|
196
|
+
changeDetector = inject(ChangeDetectorRef);
|
|
197
|
+
reactBridge = inject(ReactBridgeService);
|
|
198
|
+
async ngOnInit() {
|
|
199
|
+
await super.ngOnInit();
|
|
200
|
+
// Force the React bridge to be initialized BEFORE we let our template
|
|
201
|
+
// mount the `<mj-react-component>` child. Without this, the child's
|
|
202
|
+
// ngAfterViewInit can race the adapter bootstrap and throw "React
|
|
203
|
+
// runtime not initialized" — which then prevents the form from
|
|
204
|
+
// rendering and silently breaks the save flow.
|
|
205
|
+
try {
|
|
206
|
+
await this.reactBridge.getReactContext();
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
LogError(`InteractiveFormComponent: React bridge bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
210
|
+
this.loadError = 'React runtime failed to load. Try a hard refresh.';
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (this.record) {
|
|
214
|
+
this.originalSnapshot = this.record.GetAll();
|
|
215
|
+
this.rebuildFormHostProps();
|
|
216
|
+
}
|
|
217
|
+
// Mark init complete so the `record` setter rebuilds on subsequent
|
|
218
|
+
// input changes (post-mount record swaps from the Form Builder
|
|
219
|
+
// cockpit preview picker, etc.). Before this flag flips, the setter
|
|
220
|
+
// intentionally does nothing because ngOnInit already handles the
|
|
221
|
+
// initial assignment.
|
|
222
|
+
this._recordInitialized = true;
|
|
223
|
+
// Direct-spec path (artifact viewer) takes precedence; DB fetch only
|
|
224
|
+
// when no spec was supplied and we have an ID to look up.
|
|
225
|
+
if (!this.componentSpec && this.ComponentID) {
|
|
226
|
+
await this.loadComponentSpec();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Called by the React component via `mj-react-component`'s `componentEvent`
|
|
231
|
+
* @Output. Maps the form-role event names to BaseEntity operations.
|
|
232
|
+
*/
|
|
233
|
+
async OnReactComponentEvent(event) {
|
|
234
|
+
switch (event.type) {
|
|
235
|
+
case FormEventNames.BeforeSave:
|
|
236
|
+
await this.handleBeforeSave(event.payload);
|
|
237
|
+
break;
|
|
238
|
+
case FormEventNames.BeforeDelete:
|
|
239
|
+
await this.handleBeforeDelete(event.payload);
|
|
240
|
+
break;
|
|
241
|
+
case FormEventNames.EditModeChangeRequested:
|
|
242
|
+
this.handleEditModeChangeRequested(event.payload);
|
|
243
|
+
break;
|
|
244
|
+
// FieldChanged / DirtyStateChanged / ValidationChanged are read-only signals;
|
|
245
|
+
// the wrapper doesn't need to react beyond what the component does internally.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Apply the React component's dirty-field diff to the BaseEntity, save,
|
|
250
|
+
* and refresh the snapshot. Errors surface via `LatestResult.CompleteMessage`
|
|
251
|
+
* per the BaseEntity Save contract.
|
|
252
|
+
*
|
|
253
|
+
* Resolves `pendingSaveResolver` if {@link SaveRecord} initiated this
|
|
254
|
+
* flow via the toolbar — so the toolbar can return a meaningful boolean
|
|
255
|
+
* and honor `StopEditModeAfterSave`.
|
|
256
|
+
*/
|
|
257
|
+
async handleBeforeSave(args) {
|
|
258
|
+
if (args.cancel) {
|
|
259
|
+
this.resolvePendingSave(false);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!this.record) {
|
|
263
|
+
LogError('InteractiveFormComponent.handleBeforeSave: no record loaded');
|
|
264
|
+
this.resolvePendingSave(false);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const dirtyEntries = Object.entries(args.dirtyFields ?? {});
|
|
268
|
+
if (dirtyEntries.length === 0) {
|
|
269
|
+
// No-op save — most likely cause is the React draft didn't reach
|
|
270
|
+
// the host before RequestSave fired. Surface it so the user sees
|
|
271
|
+
// why their edits didn't land, instead of silently "succeeding".
|
|
272
|
+
console.warn('[InteractiveFormComponent] BeforeSave fired with no dirtyFields. ' +
|
|
273
|
+
'Either the user clicked Save without changing anything, or the ' +
|
|
274
|
+
'React draft did not propagate before save fired.');
|
|
275
|
+
}
|
|
276
|
+
for (const [fieldName, value] of dirtyEntries) {
|
|
277
|
+
const field = this.record.Fields.find(f => f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());
|
|
278
|
+
if (field) {
|
|
279
|
+
// Use the canonical field name from the lookup so case
|
|
280
|
+
// mismatches between the React draft and the entity schema
|
|
281
|
+
// don't silently drop the value.
|
|
282
|
+
this.record.Set(field.Name, value);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.warn(`[InteractiveFormComponent] Unknown field "${fieldName}" in dirtyFields ` +
|
|
286
|
+
`for entity ${this.record.EntityInfo.Name}. Value not applied.`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const saved = await this.record.Save();
|
|
290
|
+
if (saved) {
|
|
291
|
+
this.originalSnapshot = this.record.GetAll();
|
|
292
|
+
this.rebuildFormHostProps();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
LogError(`InteractiveFormComponent: save failed for ${this.record.EntityInfo.Name}: ${this.record.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
296
|
+
}
|
|
297
|
+
this.resolvePendingSave(saved);
|
|
298
|
+
}
|
|
299
|
+
resolvePendingSave(success) {
|
|
300
|
+
if (this.pendingSaveResolver) {
|
|
301
|
+
this.pendingSaveResolver(success);
|
|
302
|
+
this.pendingSaveResolver = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async handleBeforeDelete(args) {
|
|
306
|
+
if (args.cancel) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!this.record) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const deleted = await this.record.Delete();
|
|
313
|
+
if (!deleted) {
|
|
314
|
+
LogError(`InteractiveFormComponent: delete failed for ${this.record.EntityInfo.Name}: ${this.record.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
handleEditModeChangeRequested(args) {
|
|
318
|
+
if (args.cancel) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (args.requestedMode === 'edit') {
|
|
322
|
+
this.StartEditMode();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this.EndEditMode();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Propagate Angular-toolbar-driven mode changes into the React component's
|
|
330
|
+
* `FormHostProps.mode`. Without these overrides, clicking Edit on the
|
|
331
|
+
* `<mj-record-form-container>` toolbar flips `this.EditMode` but the React
|
|
332
|
+
* side still sees `mode: 'view'`.
|
|
333
|
+
*/
|
|
334
|
+
StartEditMode() {
|
|
335
|
+
super.StartEditMode();
|
|
336
|
+
this.rebuildFormHostProps();
|
|
337
|
+
}
|
|
338
|
+
EndEditMode() {
|
|
339
|
+
super.EndEditMode();
|
|
340
|
+
this.rebuildFormHostProps();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Toolbar **Save** override. Routes the click through the React
|
|
344
|
+
* component's registered `RequestSave` method so the React layer can
|
|
345
|
+
* (a) run its own validation, (b) flush its draft state, (c) emit
|
|
346
|
+
* `BeforeSave` with the dirty-field diff — exactly the flow the
|
|
347
|
+
* in-form Save button would trigger. The wrapper then awaits the
|
|
348
|
+
* BeforeSave handler via `pendingSaveResolver`, gets a real
|
|
349
|
+
* success/failure boolean, and honors `StopEditModeAfterSave`.
|
|
350
|
+
*
|
|
351
|
+
* Falls back to the base implementation for components that haven't
|
|
352
|
+
* registered `RequestSave` — preserves correctness for forms that
|
|
353
|
+
* keep their own in-form Save button (older sample, hand-authored
|
|
354
|
+
* forms that pre-date this contract).
|
|
355
|
+
*/
|
|
356
|
+
async SaveRecord(StopEditModeAfterSave) {
|
|
357
|
+
if (!this.reactComponent?.hasMethod?.(FormMethodNames.RequestSave)) {
|
|
358
|
+
return super.SaveRecord(StopEditModeAfterSave);
|
|
359
|
+
}
|
|
360
|
+
const completion = new Promise(resolve => {
|
|
361
|
+
this.pendingSaveResolver = resolve;
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
this.reactComponent.invokeMethod(FormMethodNames.RequestSave);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
this.pendingSaveResolver = null;
|
|
368
|
+
LogError(`InteractiveFormComponent.SaveRecord: RequestSave threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
const success = await completion;
|
|
372
|
+
if (success && StopEditModeAfterSave) {
|
|
373
|
+
this.EndEditMode();
|
|
374
|
+
}
|
|
375
|
+
return success;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Toolbar **Cancel** override. Asks the React component to discard
|
|
379
|
+
* its draft via `RequestCancel`, then flips out of edit mode the
|
|
380
|
+
* same way the base implementation would. The React component is
|
|
381
|
+
* expected to (a) clear its local draft state and (b) emit
|
|
382
|
+
* `EditModeChangeRequested({ requestedMode: 'view' })` — though we
|
|
383
|
+
* call `EndEditMode` here directly anyway, so even a no-op handler
|
|
384
|
+
* leaves the form in a coherent state.
|
|
385
|
+
*/
|
|
386
|
+
CancelEdit() {
|
|
387
|
+
if (this.reactComponent?.hasMethod?.(FormMethodNames.RequestCancel)) {
|
|
388
|
+
try {
|
|
389
|
+
this.reactComponent.invokeMethod(FormMethodNames.RequestCancel);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
LogError(`InteractiveFormComponent.CancelEdit: RequestCancel threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
393
|
+
}
|
|
394
|
+
// Don't call super.CancelEdit() — base reverts the BaseEntity,
|
|
395
|
+
// but for interactive forms the BaseEntity never received the
|
|
396
|
+
// React draft in the first place, so there's nothing to revert.
|
|
397
|
+
// Just flip mode.
|
|
398
|
+
this.EndEditMode();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
super.CancelEdit();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the Component row, parse its `Specification`, and surface either
|
|
405
|
+
* the spec or an error. Backed by `InteractiveFormsEngine` (in-memory
|
|
406
|
+
* cache, BaseEntity event-driven invalidation) so this is a sub-ms JS
|
|
407
|
+
* lookup once the engine has loaded — the engine handles freshness
|
|
408
|
+
* via remote-invalidate events, replacing the prior RunView's
|
|
409
|
+
* `BypassCache: true` escape hatch. Falls through to a one-shot
|
|
410
|
+
* RunView only on engine-cache miss (defensive — the engine should
|
|
411
|
+
* always have a Type='Form' Component, but if a brand-new Component
|
|
412
|
+
* was just created on the server and the invalidation event hasn't
|
|
413
|
+
* propagated yet, we still want to render rather than blank-screen).
|
|
414
|
+
*/
|
|
415
|
+
async loadComponentSpec() {
|
|
416
|
+
if (!this.ComponentID) {
|
|
417
|
+
this.loadError = 'No ComponentID provided to InteractiveFormComponent.';
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const provider = this.ProviderToUse;
|
|
422
|
+
const user = provider.CurrentUser;
|
|
423
|
+
// Lazy-load the form-metadata cache. No-op when already loaded.
|
|
424
|
+
try {
|
|
425
|
+
await InteractiveFormsEngine.Instance.Config(false, user, provider);
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
LogError(`InteractiveFormComponent: InteractiveFormsEngine.Config failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
429
|
+
// Fall through to RunView path below — engine miss isn't fatal.
|
|
430
|
+
}
|
|
431
|
+
let specJson = null;
|
|
432
|
+
let nameForError;
|
|
433
|
+
const cached = InteractiveFormsEngine.Instance.FindFormByID(this.ComponentID);
|
|
434
|
+
if (cached) {
|
|
435
|
+
specJson = cached.Specification;
|
|
436
|
+
nameForError = cached.Name;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Engine miss — one-shot RunView fallback. This covers the
|
|
440
|
+
// narrow window where a freshly-created Component hasn't
|
|
441
|
+
// propagated to the engine yet (cross-process remote-invalidate
|
|
442
|
+
// is debounced ~1.5s). After the propagation lands subsequent
|
|
443
|
+
// form opens hit the cache.
|
|
444
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
445
|
+
const result = await rv.RunView({
|
|
446
|
+
EntityName: 'MJ: Components',
|
|
447
|
+
Fields: ['ID', 'Name', 'Specification'],
|
|
448
|
+
ExtraFilter: `ID='${this.ComponentID}'`,
|
|
449
|
+
MaxRows: 1,
|
|
450
|
+
ResultType: 'simple',
|
|
451
|
+
BypassCache: true,
|
|
452
|
+
}, user);
|
|
453
|
+
if (!result.Success || !result.Results?.length) {
|
|
454
|
+
this.loadError = `Component ${this.ComponentID} not found.`;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
specJson = result.Results[0].Specification;
|
|
458
|
+
nameForError = result.Results[0].Name;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
this.componentSpec = JSON.parse(specJson ?? 'null');
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
this.loadError = `Component ${nameForError ?? this.ComponentID} has invalid Specification JSON: ${err instanceof Error ? err.message : String(err)}`;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!this.componentSpec) {
|
|
468
|
+
this.loadError = `Component ${nameForError ?? this.ComponentID} has an empty Specification.`;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Validate the spec commits to the form-role contract. We use the
|
|
472
|
+
// helper (whose signature is structural via Pick<>) so this stays
|
|
473
|
+
// robust across ComponentSpec field additions.
|
|
474
|
+
if (!isFormRole(this.componentSpec)) {
|
|
475
|
+
this.loadError = `Component ${nameForError ?? this.ComponentID} does not declare componentRole='form'.`;
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
this.changeDetector.markForCheck();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Build `FormHostProps` from the current record + permissions + edit mode.
|
|
485
|
+
* Called whenever the record loads, mode changes, or save completes.
|
|
486
|
+
*/
|
|
487
|
+
rebuildFormHostProps() {
|
|
488
|
+
if (!this.record) {
|
|
489
|
+
this.formHostProps = null;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const fields = this.record.Fields.map(f => SimpleEntityFieldInfo.FromEntityFieldInfo(f.EntityFieldInfo));
|
|
493
|
+
const pk = this.record.PrimaryKey;
|
|
494
|
+
const primaryKey = (pk && pk.HasValue)
|
|
495
|
+
? this.primaryKeyToPlain(pk)
|
|
496
|
+
: null;
|
|
497
|
+
this.formHostProps = {
|
|
498
|
+
entityName: this.record.EntityInfo.Name,
|
|
499
|
+
primaryKey,
|
|
500
|
+
record: this.record.GetAll(),
|
|
501
|
+
entityMetadata: {
|
|
502
|
+
fields,
|
|
503
|
+
displayName: this.record.EntityInfo.DisplayName ?? this.record.EntityInfo.Name,
|
|
504
|
+
nameField: this.record.EntityInfo.NameField?.Name,
|
|
505
|
+
},
|
|
506
|
+
mode: this.EditMode ? 'edit' : (pk && pk.HasValue ? 'view' : 'create'),
|
|
507
|
+
canEdit: this.UserCanEdit,
|
|
508
|
+
canDelete: this.UserCanDelete,
|
|
509
|
+
canCreate: this.UserCanCreate,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
primaryKeyToPlain(pk) {
|
|
513
|
+
const obj = {};
|
|
514
|
+
for (const kvp of pk.KeyValuePairs ?? []) {
|
|
515
|
+
obj[kvp.FieldName] = kvp.Value;
|
|
516
|
+
}
|
|
517
|
+
return obj;
|
|
518
|
+
}
|
|
519
|
+
static ɵfac = /*@__PURE__*/ (() => { let ɵInteractiveFormComponent_BaseFactory; return function InteractiveFormComponent_Factory(__ngFactoryType__) { return (ɵInteractiveFormComponent_BaseFactory || (ɵInteractiveFormComponent_BaseFactory = i0.ɵɵgetInheritedFactory(InteractiveFormComponent)))(__ngFactoryType__ || InteractiveFormComponent); }; })();
|
|
520
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: InteractiveFormComponent, selectors: [["mj-interactive-form"]], viewQuery: function InteractiveFormComponent_Query(rf, ctx) { if (rf & 1) {
|
|
521
|
+
i0.ɵɵviewQuery(_c0, 5);
|
|
522
|
+
} if (rf & 2) {
|
|
523
|
+
let _t;
|
|
524
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.reactComponent = _t.first);
|
|
525
|
+
} }, inputs: { record: "record", ComponentID: "ComponentID", componentSpec: "componentSpec", previewMode: "previewMode" }, outputs: { LoadErrorChanged: "LoadErrorChanged" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 1, vars: 1, consts: [["reactComponent", ""], [3, "Record", "FormComponent"], [1, "mj-error-banner"], [3, "component", "componentProps"], [1, "mj-loading-state"], [1, "fa-solid", "fa-triangle-exclamation"], [3, "componentEvent", "component", "componentProps"], [1, "fa-solid", "fa-spinner", "fa-spin"], [3, "Navigate", "DeleteRequested", "FavoriteToggled", "HistoryRequested", "ListManagementRequested", "Record", "FormComponent"]], template: function InteractiveFormComponent_Template(rf, ctx) { if (rf & 1) {
|
|
526
|
+
i0.ɵɵconditionalCreate(0, InteractiveFormComponent_Conditional_0_Template, 2, 1);
|
|
527
|
+
} if (rf & 2) {
|
|
528
|
+
i0.ɵɵconditional(ctx.record ? 0 : -1);
|
|
529
|
+
} }, dependencies: [i1.MJReactComponent, i2.MjRecordFormContainerComponent], encapsulation: 2 });
|
|
530
|
+
}
|
|
531
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(InteractiveFormComponent, [{
|
|
532
|
+
type: Component,
|
|
533
|
+
args: [{ standalone: false, selector: 'mj-interactive-form', template: "@if (record) {\n@if (previewMode) {\n <!-- Preview mode \u2014 no toolbar wrap. Just the form body for clean\n \"what the form looks like\" previews (Form Builder cockpit). -->\n @if (loadError) {\n <div class=\"mj-error-banner\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ loadError }}</span>\n </div>\n } @else if (componentSpec && formHostProps) {\n <mj-react-component\n #reactComponent\n [component]=\"componentSpec\"\n [componentProps]=\"formHostProps\"\n (componentEvent)=\"OnReactComponentEvent($event)\">\n </mj-react-component>\n } @else {\n <div class=\"mj-loading-state\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Loading form\u2026</span>\n </div>\n }\n} @else {\n<mj-record-form-container\n [Record]=\"record\"\n [FormComponent]=\"this\"\n (Navigate)=\"OnFormNavigate($event)\"\n (DeleteRequested)=\"OnDeleteRequested()\"\n (FavoriteToggled)=\"OnFavoriteToggled()\"\n (HistoryRequested)=\"OnHistoryRequested()\"\n (ListManagementRequested)=\"OnListManagementRequested()\">\n\n @if (loadError) {\n <div class=\"mj-error-banner\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ loadError }}</span>\n </div>\n } @else if (componentSpec && formHostProps) {\n <mj-react-component\n #reactComponent\n [component]=\"componentSpec\"\n [componentProps]=\"formHostProps\"\n (componentEvent)=\"OnReactComponentEvent($event)\">\n </mj-react-component>\n } @else {\n <div class=\"mj-loading-state\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Loading form\u2026</span>\n </div>\n }\n\n</mj-record-form-container>\n}\n}\n" }]
|
|
534
|
+
}], null, { record: [{
|
|
535
|
+
type: Input
|
|
536
|
+
}], ComponentID: [{
|
|
537
|
+
type: Input
|
|
538
|
+
}], componentSpec: [{
|
|
539
|
+
type: Input
|
|
540
|
+
}], previewMode: [{
|
|
541
|
+
type: Input
|
|
542
|
+
}], LoadErrorChanged: [{
|
|
543
|
+
type: Output
|
|
544
|
+
}], reactComponent: [{
|
|
545
|
+
type: ViewChild,
|
|
546
|
+
args: ['reactComponent']
|
|
547
|
+
}] }); })();
|
|
548
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(InteractiveFormComponent, { className: "InteractiveFormComponent", filePath: "src/lib/interactive-form/interactive-form.component.ts", lineNumber: 38 }); })();
|
|
549
|
+
/**
|
|
550
|
+
* Tree-shaking guard. Imported and called from public-api.ts so bundlers don't
|
|
551
|
+
* drop the component when no consumer references it by name.
|
|
552
|
+
*/
|
|
553
|
+
export function LoadInteractiveFormComponent() {
|
|
554
|
+
// Force the class reference to stay live.
|
|
555
|
+
if (false) {
|
|
556
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
557
|
+
const _ = InteractiveFormComponent;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//# sourceMappingURL=interactive-form.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive-form.component.js","sourceRoot":"","sources":["../../../src/lib/interactive-form/interactive-form.component.ts","../../../src/lib/interactive-form/interactive-form.component.html"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, inject } from '@angular/core';\nimport { BaseEntity, CompositeKey, LogError, RunView } from '@memberjunction/core';\nimport { InteractiveFormsEngine } from '@memberjunction/core-entities';\nimport { ComponentSpec } from '@memberjunction/interactive-component-types';\nimport {\n FormHostProps,\n FormEventNames,\n FormMethodNames,\n FormBeforeSaveArgs,\n FormBeforeDeleteArgs,\n FormEditModeChangeRequestedArgs,\n isFormRole,\n} from '@memberjunction/interactive-component-types/forms';\nimport { SimpleEntityFieldInfo } from '@memberjunction/interactive-component-types';\nimport { MJReactComponent, ReactBridgeService, ReactComponentEvent } from '@memberjunction/ng-react';\nimport { BaseFormComponent } from '../base-form-component';\n\n/**\n * Wraps a Component (the IC kind — declared `componentRole: 'form'`) so it can\n * stand in for an Angular form on any entity.\n *\n * **Layering invariant:** the React component never touches BaseEntity. This\n * wrapper owns the entity lifecycle (`record.Save`, `record.Delete`, mode\n * transitions). The React component sees only the snapshot in\n * {@link FormHostProps} and communicates back via `BeforeSave` /\n * `BeforeDelete` / `EditModeChangeRequested` events plus its dirty-field diff.\n *\n * Instantiated by `SingleRecordComponent.LoadForm` when the resolver finds an\n * `EntityFormOverride` matching the user's scope. Behaves exactly like the\n * generated Angular form from the host's perspective: same `record` setter,\n * same `EditMode` toggle, same outputs.\n */\n@Component({\n standalone: false,\n selector: 'mj-interactive-form',\n templateUrl: './interactive-form.component.html',\n})\nexport class InteractiveFormComponent extends BaseFormComponent implements OnInit, OnDestroy {\n\n /**\n * The bound record. Backed by a setter so swapping records *after*\n * the component has mounted (e.g. Form Builder cockpit preview record\n * picker) triggers a fresh `originalSnapshot` capture + a\n * `formHostProps` rebuild — without this, the React form keeps\n * rendering the original record's values even after the input\n * changes. Guarded on `_recordInitialized` so the post-mount path\n * doesn't fire during the first input assignment (handled by\n * `ngOnInit`, which sets both `originalSnapshot` and `formHostProps`).\n */\n private _record!: BaseEntity;\n private _recordInitialized = false;\n\n @Input()\n public override set record(value: BaseEntity) {\n if (this._record === value) return;\n this._record = value;\n if (this._recordInitialized && value) {\n this.originalSnapshot = value.GetAll();\n this.rebuildFormHostProps();\n this.cdr.markForCheck();\n }\n }\n public override get record(): BaseEntity { return this._record; }\n\n /**\n * The Component row ID resolved for this (entity, user, roles) tuple. Set\n * by `SingleRecordComponent.LoadForm` after the resolver returns an\n * override.\n */\n @Input() public ComponentID: string | null = null;\n\n /**\n * Loaded ComponentSpec, parsed from the Component row's `Specification` JSON.\n *\n * Two ways to populate this:\n * 1. **Via `ComponentID`** (the standard path): set the input, ngOnInit\n * fetches the row from `MJ: Components` and assigns the parsed spec.\n * 2. **Direct assignment** (the artifact-viewer path): the caller already\n * has the spec in hand (parsed from an artifact JSON blob) and sets\n * `componentSpec` directly. In that case `ComponentID` should be left\n * null and the DB fetch is skipped.\n */\n @Input() public componentSpec: ComponentSpec | null = null;\n\n /**\n * When true, render the React component **without** the\n * `<mj-record-form-container>` wrapper — i.e. no toolbar (no Save /\n * Cancel / Edit / Delete / History / Tags / Lists).\n *\n * Used by the Form Builder cockpit's Preview tab where the toolbar's\n * actions don't apply (Save would save the *record*, but the cockpit's\n * own Save button saves the *spec*; History/Tags assume a real\n * persistent record, but the preview record can be a synthetic\n * NewRecord). Keeping just the form body produces a clean \"this is\n * what the form looks like\" view.\n */\n @Input() public previewMode = false;\n\n /** FormHostProps passed to the React component. Recomputed when record or mode changes. */\n public formHostProps: FormHostProps | null = null;\n\n /** Loaded-spec error (component row missing, JSON parse failure, etc.). */\n private _loadError: string | null = null;\n\n /**\n * Loaded-spec error (component row missing, JSON parse failure,\n * React-runtime bootstrap failure, etc.). Setter auto-emits\n * `LoadErrorChanged` so host surfaces (e.g. Form Builder cockpit\n * Preview tab) can show their own error state — retrospective fix #10.\n */\n public get loadError(): string | null { return this._loadError; }\n public set loadError(v: string | null) {\n if (v === this._loadError) return;\n this._loadError = v;\n this.LoadErrorChanged.emit(v);\n }\n\n @Output() public LoadErrorChanged = new EventEmitter<string | null>();\n\n /** Tracks last-known field snapshot for computing the dirty-field diff on save. */\n private originalSnapshot: Record<string, unknown> = {};\n\n /**\n * Reference to the mounted React component. Used by the SaveRecord /\n * CancelEdit overrides below to invoke the component's registered\n * `RequestSave` / `RequestCancel` methods when the host toolbar fires\n * Save/Cancel — so the single user-visible Save lives on the toolbar\n * (consistent with every other entity form in MJ) and not duplicated\n * inside the form body.\n */\n @ViewChild('reactComponent') public reactComponent?: MJReactComponent;\n\n /**\n * Promise resolver populated when `SaveRecord` invokes `RequestSave`\n * on the React component. `handleBeforeSave` resolves it after the\n * BeforeSave-driven entity save completes, so SaveRecord can return\n * a meaningful success boolean and honor `StopEditModeAfterSave`.\n */\n private pendingSaveResolver: ((success: boolean) => void) | null = null;\n\n private readonly changeDetector = inject(ChangeDetectorRef);\n private readonly reactBridge = inject(ReactBridgeService);\n\n public override async ngOnInit(): Promise<void> {\n await super.ngOnInit();\n // Force the React bridge to be initialized BEFORE we let our template\n // mount the `<mj-react-component>` child. Without this, the child's\n // ngAfterViewInit can race the adapter bootstrap and throw \"React\n // runtime not initialized\" — which then prevents the form from\n // rendering and silently breaks the save flow.\n try {\n await this.reactBridge.getReactContext();\n } catch (err) {\n LogError(`InteractiveFormComponent: React bridge bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);\n this.loadError ='React runtime failed to load. Try a hard refresh.';\n return;\n }\n if (this.record) {\n this.originalSnapshot = this.record.GetAll();\n this.rebuildFormHostProps();\n }\n // Mark init complete so the `record` setter rebuilds on subsequent\n // input changes (post-mount record swaps from the Form Builder\n // cockpit preview picker, etc.). Before this flag flips, the setter\n // intentionally does nothing because ngOnInit already handles the\n // initial assignment.\n this._recordInitialized = true;\n // Direct-spec path (artifact viewer) takes precedence; DB fetch only\n // when no spec was supplied and we have an ID to look up.\n if (!this.componentSpec && this.ComponentID) {\n await this.loadComponentSpec();\n }\n }\n\n /**\n * Called by the React component via `mj-react-component`'s `componentEvent`\n * @Output. Maps the form-role event names to BaseEntity operations.\n */\n public async OnReactComponentEvent(event: ReactComponentEvent): Promise<void> {\n switch (event.type) {\n case FormEventNames.BeforeSave:\n await this.handleBeforeSave(event.payload as FormBeforeSaveArgs);\n break;\n case FormEventNames.BeforeDelete:\n await this.handleBeforeDelete(event.payload as FormBeforeDeleteArgs);\n break;\n case FormEventNames.EditModeChangeRequested:\n this.handleEditModeChangeRequested(event.payload as FormEditModeChangeRequestedArgs);\n break;\n // FieldChanged / DirtyStateChanged / ValidationChanged are read-only signals;\n // the wrapper doesn't need to react beyond what the component does internally.\n }\n }\n\n /**\n * Apply the React component's dirty-field diff to the BaseEntity, save,\n * and refresh the snapshot. Errors surface via `LatestResult.CompleteMessage`\n * per the BaseEntity Save contract.\n *\n * Resolves `pendingSaveResolver` if {@link SaveRecord} initiated this\n * flow via the toolbar — so the toolbar can return a meaningful boolean\n * and honor `StopEditModeAfterSave`.\n */\n private async handleBeforeSave(args: FormBeforeSaveArgs): Promise<void> {\n if (args.cancel) {\n this.resolvePendingSave(false);\n return;\n }\n if (!this.record) {\n LogError('InteractiveFormComponent.handleBeforeSave: no record loaded');\n this.resolvePendingSave(false);\n return;\n }\n\n const dirtyEntries = Object.entries(args.dirtyFields ?? {});\n if (dirtyEntries.length === 0) {\n // No-op save — most likely cause is the React draft didn't reach\n // the host before RequestSave fired. Surface it so the user sees\n // why their edits didn't land, instead of silently \"succeeding\".\n console.warn(\n '[InteractiveFormComponent] BeforeSave fired with no dirtyFields. ' +\n 'Either the user clicked Save without changing anything, or the ' +\n 'React draft did not propagate before save fired.'\n );\n }\n for (const [fieldName, value] of dirtyEntries) {\n const field = this.record.Fields.find(f =>\n f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());\n if (field) {\n // Use the canonical field name from the lookup so case\n // mismatches between the React draft and the entity schema\n // don't silently drop the value.\n this.record.Set(field.Name, value);\n } else {\n console.warn(\n `[InteractiveFormComponent] Unknown field \"${fieldName}\" in dirtyFields ` +\n `for entity ${this.record.EntityInfo.Name}. Value not applied.`\n );\n }\n }\n\n const saved = await this.record.Save();\n if (saved) {\n this.originalSnapshot = this.record.GetAll();\n this.rebuildFormHostProps();\n } else {\n LogError(`InteractiveFormComponent: save failed for ${this.record.EntityInfo.Name}: ${this.record.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n this.resolvePendingSave(saved);\n }\n\n private resolvePendingSave(success: boolean): void {\n if (this.pendingSaveResolver) {\n this.pendingSaveResolver(success);\n this.pendingSaveResolver = null;\n }\n }\n\n private async handleBeforeDelete(args: FormBeforeDeleteArgs): Promise<void> {\n if (args.cancel) {\n return;\n }\n if (!this.record) {\n return;\n }\n const deleted = await this.record.Delete();\n if (!deleted) {\n LogError(`InteractiveFormComponent: delete failed for ${this.record.EntityInfo.Name}: ${this.record.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n }\n\n private handleEditModeChangeRequested(args: FormEditModeChangeRequestedArgs): void {\n if (args.cancel) {\n return;\n }\n if (args.requestedMode === 'edit') {\n this.StartEditMode();\n } else {\n this.EndEditMode();\n }\n }\n\n /**\n * Propagate Angular-toolbar-driven mode changes into the React component's\n * `FormHostProps.mode`. Without these overrides, clicking Edit on the\n * `<mj-record-form-container>` toolbar flips `this.EditMode` but the React\n * side still sees `mode: 'view'`.\n */\n public override StartEditMode(): void {\n super.StartEditMode();\n this.rebuildFormHostProps();\n }\n\n public override EndEditMode(): void {\n super.EndEditMode();\n this.rebuildFormHostProps();\n }\n\n /**\n * Toolbar **Save** override. Routes the click through the React\n * component's registered `RequestSave` method so the React layer can\n * (a) run its own validation, (b) flush its draft state, (c) emit\n * `BeforeSave` with the dirty-field diff — exactly the flow the\n * in-form Save button would trigger. The wrapper then awaits the\n * BeforeSave handler via `pendingSaveResolver`, gets a real\n * success/failure boolean, and honors `StopEditModeAfterSave`.\n *\n * Falls back to the base implementation for components that haven't\n * registered `RequestSave` — preserves correctness for forms that\n * keep their own in-form Save button (older sample, hand-authored\n * forms that pre-date this contract).\n */\n public override async SaveRecord(StopEditModeAfterSave: boolean): Promise<boolean> {\n if (!this.reactComponent?.hasMethod?.(FormMethodNames.RequestSave)) {\n return super.SaveRecord(StopEditModeAfterSave);\n }\n const completion = new Promise<boolean>(resolve => {\n this.pendingSaveResolver = resolve;\n });\n try {\n this.reactComponent.invokeMethod(FormMethodNames.RequestSave);\n } catch (err) {\n this.pendingSaveResolver = null;\n LogError(`InteractiveFormComponent.SaveRecord: RequestSave threw: ${err instanceof Error ? err.message : String(err)}`);\n return false;\n }\n const success = await completion;\n if (success && StopEditModeAfterSave) {\n this.EndEditMode();\n }\n return success;\n }\n\n /**\n * Toolbar **Cancel** override. Asks the React component to discard\n * its draft via `RequestCancel`, then flips out of edit mode the\n * same way the base implementation would. The React component is\n * expected to (a) clear its local draft state and (b) emit\n * `EditModeChangeRequested({ requestedMode: 'view' })` — though we\n * call `EndEditMode` here directly anyway, so even a no-op handler\n * leaves the form in a coherent state.\n */\n public override CancelEdit(): void {\n if (this.reactComponent?.hasMethod?.(FormMethodNames.RequestCancel)) {\n try {\n this.reactComponent.invokeMethod(FormMethodNames.RequestCancel);\n } catch (err) {\n LogError(`InteractiveFormComponent.CancelEdit: RequestCancel threw: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Don't call super.CancelEdit() — base reverts the BaseEntity,\n // but for interactive forms the BaseEntity never received the\n // React draft in the first place, so there's nothing to revert.\n // Just flip mode.\n this.EndEditMode();\n return;\n }\n super.CancelEdit();\n }\n\n /**\n * Resolve the Component row, parse its `Specification`, and surface either\n * the spec or an error. Backed by `InteractiveFormsEngine` (in-memory\n * cache, BaseEntity event-driven invalidation) so this is a sub-ms JS\n * lookup once the engine has loaded — the engine handles freshness\n * via remote-invalidate events, replacing the prior RunView's\n * `BypassCache: true` escape hatch. Falls through to a one-shot\n * RunView only on engine-cache miss (defensive — the engine should\n * always have a Type='Form' Component, but if a brand-new Component\n * was just created on the server and the invalidation event hasn't\n * propagated yet, we still want to render rather than blank-screen).\n */\n private async loadComponentSpec(): Promise<void> {\n if (!this.ComponentID) {\n this.loadError ='No ComponentID provided to InteractiveFormComponent.';\n return;\n }\n try {\n const provider = this.ProviderToUse;\n const user = provider.CurrentUser;\n // Lazy-load the form-metadata cache. No-op when already loaded.\n try {\n await InteractiveFormsEngine.Instance.Config(false, user, provider);\n } catch (err) {\n LogError(`InteractiveFormComponent: InteractiveFormsEngine.Config failed: ${err instanceof Error ? err.message : String(err)}`);\n // Fall through to RunView path below — engine miss isn't fatal.\n }\n\n let specJson: string | null = null;\n let nameForError: string | undefined;\n\n const cached = InteractiveFormsEngine.Instance.FindFormByID(this.ComponentID);\n if (cached) {\n specJson = cached.Specification;\n nameForError = cached.Name;\n } else {\n // Engine miss — one-shot RunView fallback. This covers the\n // narrow window where a freshly-created Component hasn't\n // propagated to the engine yet (cross-process remote-invalidate\n // is debounced ~1.5s). After the propagation lands subsequent\n // form opens hit the cache.\n const rv = RunView.FromMetadataProvider(provider);\n const result = await rv.RunView<{ ID: string; Specification: string; Name: string }>({\n EntityName: 'MJ: Components',\n Fields: ['ID', 'Name', 'Specification'],\n ExtraFilter: `ID='${this.ComponentID}'`,\n MaxRows: 1,\n ResultType: 'simple',\n BypassCache: true,\n }, user);\n if (!result.Success || !result.Results?.length) {\n this.loadError =`Component ${this.ComponentID} not found.`;\n return;\n }\n specJson = result.Results[0].Specification;\n nameForError = result.Results[0].Name;\n }\n\n try {\n this.componentSpec = JSON.parse(specJson ?? 'null') as ComponentSpec;\n } catch (err) {\n this.loadError =`Component ${nameForError ?? this.ComponentID} has invalid Specification JSON: ${err instanceof Error ? err.message : String(err)}`;\n return;\n }\n if (!this.componentSpec) {\n this.loadError =`Component ${nameForError ?? this.ComponentID} has an empty Specification.`;\n return;\n }\n // Validate the spec commits to the form-role contract. We use the\n // helper (whose signature is structural via Pick<>) so this stays\n // robust across ComponentSpec field additions.\n if (!isFormRole(this.componentSpec)) {\n this.loadError =`Component ${nameForError ?? this.ComponentID} does not declare componentRole='form'.`;\n return;\n }\n } finally {\n this.changeDetector.markForCheck();\n }\n }\n\n /**\n * Build `FormHostProps` from the current record + permissions + edit mode.\n * Called whenever the record loads, mode changes, or save completes.\n */\n private rebuildFormHostProps(): void {\n if (!this.record) {\n this.formHostProps = null;\n return;\n }\n const fields: SimpleEntityFieldInfo[] = this.record.Fields.map(f =>\n SimpleEntityFieldInfo.FromEntityFieldInfo(f.EntityFieldInfo));\n\n const pk = this.record.PrimaryKey;\n const primaryKey: Record<string, unknown> | null = (pk && pk.HasValue)\n ? this.primaryKeyToPlain(pk)\n : null;\n\n this.formHostProps = {\n entityName: this.record.EntityInfo.Name,\n primaryKey,\n record: this.record.GetAll(),\n entityMetadata: {\n fields,\n displayName: this.record.EntityInfo.DisplayName ?? this.record.EntityInfo.Name,\n nameField: this.record.EntityInfo.NameField?.Name,\n },\n mode: this.EditMode ? 'edit' : (pk && pk.HasValue ? 'view' : 'create'),\n canEdit: this.UserCanEdit,\n canDelete: this.UserCanDelete,\n canCreate: this.UserCanCreate,\n };\n }\n\n private primaryKeyToPlain(pk: CompositeKey): Record<string, unknown> {\n const obj: Record<string, unknown> = {};\n for (const kvp of pk.KeyValuePairs ?? []) {\n obj[kvp.FieldName] = kvp.Value;\n }\n return obj;\n }\n}\n\n/**\n * Tree-shaking guard. Imported and called from public-api.ts so bundlers don't\n * drop the component when no consumer references it by name.\n */\nexport function LoadInteractiveFormComponent(): void {\n // Force the class reference to stay live.\n if (false as boolean) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const _: unknown = InteractiveFormComponent;\n }\n}\n","@if (record) {\n@if (previewMode) {\n <!-- Preview mode — no toolbar wrap. Just the form body for clean\n \"what the form looks like\" previews (Form Builder cockpit). -->\n @if (loadError) {\n <div class=\"mj-error-banner\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ loadError }}</span>\n </div>\n } @else if (componentSpec && formHostProps) {\n <mj-react-component\n #reactComponent\n [component]=\"componentSpec\"\n [componentProps]=\"formHostProps\"\n (componentEvent)=\"OnReactComponentEvent($event)\">\n </mj-react-component>\n } @else {\n <div class=\"mj-loading-state\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Loading form…</span>\n </div>\n }\n} @else {\n<mj-record-form-container\n [Record]=\"record\"\n [FormComponent]=\"this\"\n (Navigate)=\"OnFormNavigate($event)\"\n (DeleteRequested)=\"OnDeleteRequested()\"\n (FavoriteToggled)=\"OnFavoriteToggled()\"\n (HistoryRequested)=\"OnHistoryRequested()\"\n (ListManagementRequested)=\"OnListManagementRequested()\">\n\n @if (loadError) {\n <div class=\"mj-error-banner\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ loadError }}</span>\n </div>\n } @else if (componentSpec && formHostProps) {\n <mj-react-component\n #reactComponent\n [component]=\"componentSpec\"\n [componentProps]=\"formHostProps\"\n (componentEvent)=\"OnReactComponentEvent($event)\">\n </mj-react-component>\n } @else {\n <div class=\"mj-loading-state\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Loading form…</span>\n </div>\n }\n\n</mj-record-form-container>\n}\n}\n"]}
|