@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.
Files changed (56) hide show
  1. package/dist/lib/base-form-component.d.ts +28 -0
  2. package/dist/lib/base-form-component.d.ts.map +1 -1
  3. package/dist/lib/base-form-component.js +37 -0
  4. package/dist/lib/base-form-component.js.map +1 -1
  5. package/dist/lib/container/record-form-container.component.d.ts +63 -1
  6. package/dist/lib/container/record-form-container.component.d.ts.map +1 -1
  7. package/dist/lib/container/record-form-container.component.js +134 -42
  8. package/dist/lib/container/record-form-container.component.js.map +1 -1
  9. package/dist/lib/interactive-form/interactive-form.component.d.ts +181 -0
  10. package/dist/lib/interactive-form/interactive-form.component.d.ts.map +1 -0
  11. package/dist/lib/interactive-form/interactive-form.component.js +560 -0
  12. package/dist/lib/interactive-form/interactive-form.component.js.map +1 -0
  13. package/dist/lib/panel-slot/base-form-panel.d.ts +97 -0
  14. package/dist/lib/panel-slot/base-form-panel.d.ts.map +1 -0
  15. package/dist/lib/panel-slot/base-form-panel.js +64 -0
  16. package/dist/lib/panel-slot/base-form-panel.js.map +1 -0
  17. package/dist/lib/panel-slot/form-panel-slot.component.d.ts +78 -0
  18. package/dist/lib/panel-slot/form-panel-slot.component.d.ts.map +1 -0
  19. package/dist/lib/panel-slot/form-panel-slot.component.js +227 -0
  20. package/dist/lib/panel-slot/form-panel-slot.component.js.map +1 -0
  21. package/dist/lib/panel-slot/form-slot-coordinator.service.d.ts +57 -0
  22. package/dist/lib/panel-slot/form-slot-coordinator.service.d.ts.map +1 -0
  23. package/dist/lib/panel-slot/form-slot-coordinator.service.js +104 -0
  24. package/dist/lib/panel-slot/form-slot-coordinator.service.js.map +1 -0
  25. package/dist/lib/toolbar/form-toolbar.component.d.ts +56 -1
  26. package/dist/lib/toolbar/form-toolbar.component.d.ts.map +1 -1
  27. package/dist/lib/toolbar/form-toolbar.component.js +294 -136
  28. package/dist/lib/toolbar/form-toolbar.component.js.map +1 -1
  29. package/dist/lib/types/navigation-events.d.ts +19 -1
  30. package/dist/lib/types/navigation-events.d.ts.map +1 -1
  31. package/dist/lib/types/navigation-events.js.map +1 -1
  32. package/dist/lib/types/toolbar-config.d.ts +11 -0
  33. package/dist/lib/types/toolbar-config.d.ts.map +1 -1
  34. package/dist/lib/types/toolbar-config.js +3 -0
  35. package/dist/lib/types/toolbar-config.js.map +1 -1
  36. package/dist/module.d.ts +10 -7
  37. package/dist/module.d.ts.map +1 -1
  38. package/dist/module.js +21 -7
  39. package/dist/module.js.map +1 -1
  40. package/dist/public-api.d.ts +4 -0
  41. package/dist/public-api.d.ts.map +1 -1
  42. package/dist/public-api.js +7 -0
  43. package/dist/public-api.js.map +1 -1
  44. package/package.json +12 -10
  45. package/dist/__tests__/sanity.test.d.ts +0 -2
  46. package/dist/__tests__/sanity.test.d.ts.map +0 -1
  47. package/dist/__tests__/sanity.test.js +0 -12
  48. package/dist/__tests__/sanity.test.js.map +0 -1
  49. package/dist/__tests__/tag-count-badge.test.d.ts +0 -2
  50. package/dist/__tests__/tag-count-badge.test.d.ts.map +0 -1
  51. package/dist/__tests__/tag-count-badge.test.js +0 -70
  52. package/dist/__tests__/tag-count-badge.test.js.map +0 -1
  53. package/dist/lib/__tests__/base-forms.test.d.ts +0 -2
  54. package/dist/lib/__tests__/base-forms.test.d.ts.map +0 -1
  55. package/dist/lib/__tests__/base-forms.test.js +0 -256
  56. 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"]}