@memberjunction/ng-explorer-core 5.37.0 → 5.39.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 (31) hide show
  1. package/dist/generated/lazy-feature-config.d.ts +1 -1
  2. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  3. package/dist/generated/lazy-feature-config.js +4 -2
  4. package/dist/generated/lazy-feature-config.js.map +1 -1
  5. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
  6. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
  7. package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
  8. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
  9. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  10. package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
  11. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  12. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  13. package/dist/lib/shell/components/header/app-nav.component.js +12 -0
  14. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  15. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
  16. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  17. package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
  18. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  19. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
  20. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  21. package/dist/lib/shell/components/tabs/tab-container.component.js +195 -12
  22. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  23. package/dist/lib/shell/shell.component.d.ts +5 -4
  24. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  25. package/dist/lib/shell/shell.component.js +20 -9
  26. package/dist/lib/shell/shell.component.js.map +1 -1
  27. package/dist/lib/single-record/single-record.component.d.ts +31 -37
  28. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  29. package/dist/lib/single-record/single-record.component.js +64 -306
  30. package/dist/lib/single-record/single-record.component.js.map +1 -1
  31. package/package.json +46 -45
@@ -1,49 +1,43 @@
1
- import { AfterViewInit, EventEmitter, OnDestroy, OnInit } from '@angular/core';
2
- import { ActivatedRoute } from '@angular/router';
1
+ import { EventEmitter } from '@angular/core';
3
2
  import { CompositeKey, BaseEntity } from '@memberjunction/core';
4
- import { Container } from '@memberjunction/ng-container-directives';
3
+ import { FormNavigationEvent, FormNotificationEvent } from '@memberjunction/ng-base-forms';
5
4
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
6
5
  import * as i0 from "@angular/core";
7
- export declare class SingleRecordComponent extends BaseAngularComponent implements OnInit, AfterViewInit, OnDestroy {
8
- private route;
9
- formContainer: Container;
6
+ /**
7
+ * Explorer-side host for a single entity record in the main tab area.
8
+ *
9
+ * This is now a **thin wrapper** around the Generic `<mj-entity-form-host>`
10
+ * (in `@memberjunction/ng-base-forms`), which owns all the mechanics: resolving
11
+ * the form (class / custom / interactive override + variants), loading the
12
+ * record, dynamically creating the form, binding it, and tearing it down.
13
+ *
14
+ * SingleRecordComponent's only remaining job is the **Explorer mapping**:
15
+ * translating the host's framework-agnostic events into Explorer services —
16
+ * `Navigate` → {@link NavigationService}, `Notification` → {@link SharedService},
17
+ * record loads → {@link RecentAccessService} — none of which belong in a Generic
18
+ * component.
19
+ */
20
+ export declare class SingleRecordComponent extends BaseAngularComponent {
10
21
  PrimaryKey: CompositeKey;
11
22
  entityName: string | null;
12
23
  newRecordValues: string | Record<string, unknown> | null;
13
- loadComplete: EventEmitter<any>;
24
+ loadComplete: EventEmitter<void>;
14
25
  recordSaved: EventEmitter<BaseEntity>;
15
- private recentAccessService;
26
+ /** Emitted when the hosted form asks to be dismissed (e.g. Discard on a new record). */
27
+ recordDismissed: EventEmitter<void>;
16
28
  private navigationService;
17
29
  private sharedService;
18
- private cdr;
19
- constructor(route: ActivatedRoute);
20
- appDescription: string;
21
- useGenericForm: boolean;
22
- loading: boolean;
23
- errorTitle: string | null;
24
- errorDetail: string | null;
25
- private _formComponentRef;
26
- private _currentRecord;
27
- private _eventHandlerSubscription;
28
- private _formEventSubscriptions;
29
- ngOnInit(): void;
30
- ngAfterViewInit(): void;
31
- LoadForm(primaryKey: CompositeKey, entityName: string): Promise<void>;
32
- /**
33
- * Render a user-visible error state inside the record pane AND log a structured
34
- * console.error for developers. Always emits `loadComplete` so the Explorer shell
35
- * does not hang on its first-resource-load gate.
36
- */
37
- private failWithUserError;
38
- protected SetNewRecordValues(record: BaseEntity): void;
39
- /**
40
- * Subscribe to BaseFormComponent @Output events and map them to Explorer services.
41
- */
42
- private subscribeToFormEvents;
43
- private handleNavigation;
44
- private cleanupFormSubscriptions;
45
- ngOnDestroy(): void;
30
+ private formPresenter;
31
+ private recentAccessService;
32
+ /** Unblock the shell's first-resource-load gate (success or error). */
33
+ onLoadComplete(): void;
34
+ /** Log access for existing records once the form's record is ready. */
35
+ onRecordReady(record: BaseEntity): void;
36
+ onSaved(record: BaseEntity): void;
37
+ onNotification(event: FormNotificationEvent): void;
38
+ /** Map the form's navigation requests onto Explorer's NavigationService. */
39
+ handleNavigation(event: FormNavigationEvent): void;
46
40
  static ɵfac: i0.ɵɵFactoryDeclaration<SingleRecordComponent, never>;
47
- static ɵcmp: i0.ɵɵComponentDeclaration<SingleRecordComponent, "mj-single-record", never, { "PrimaryKey": { "alias": "PrimaryKey"; "required": false; }; "entityName": { "alias": "entityName"; "required": false; }; "newRecordValues": { "alias": "newRecordValues"; "required": false; }; }, { "loadComplete": "loadComplete"; "recordSaved": "recordSaved"; }, never, never, false, never>;
41
+ static ɵcmp: i0.ɵɵComponentDeclaration<SingleRecordComponent, "mj-single-record", never, { "PrimaryKey": { "alias": "PrimaryKey"; "required": false; }; "entityName": { "alias": "entityName"; "required": false; }; "newRecordValues": { "alias": "newRecordValues"; "required": false; }; }, { "loadComplete": "loadComplete"; "recordSaved": "recordSaved"; "recordDismissed": "recordDismissed"; }, never, never, false, never>;
48
42
  }
49
43
  //# sourceMappingURL=single-record.component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"single-record.component.d.ts","sourceRoot":"","sources":["../../../src/lib/single-record/single-record.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA8C,YAAY,EAAiB,SAAS,EAAE,MAAM,EAAqB,MAAM,eAAe,CAAC;AAC7J,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAA0B,YAAY,EAAE,UAAU,EAA4D,MAAM,sBAAsB,CAAC;AAGlJ,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;;AACrE,qBAMa,qBAAsB,SAAQ,oBAAqB,YAAW,MAAM,EAAE,aAAa,EAAE,SAAS;IAc5F,OAAO,CAAC,KAAK;IAbY,aAAa,EAAG,SAAS,CAAC;IAChD,UAAU,EAAE,YAAY,CAAsB;IAC9C,UAAU,EAAE,MAAM,GAAG,IAAI,CAAM;IAC/B,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAM;IAE7D,YAAY,EAAE,YAAY,CAAC,GAAG,CAAC,CAA2B;IAC1D,WAAW,EAAE,YAAY,CAAC,UAAU,CAAC,CAAkC;IAExF,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,GAAG,CAA6B;gBAEnB,KAAK,EAAE,cAAc;IAKnC,cAAc,EAAE,MAAM,CAAK;IAC3B,cAAc,EAAE,OAAO,CAAS;IAChC,OAAO,EAAE,OAAO,CAAQ;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAGzC,OAAO,CAAC,iBAAiB,CAAgD;IACzE,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,yBAAyB,CAA6B;IAC9D,OAAO,CAAC,uBAAuB,CAAsB;IAErD,QAAQ,IAAI,IAAI;IAGhB,eAAe;IAIF,QAAQ,CAAC,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM;IA2GlE;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAyBzB,SAAS,CAAC,kBAAkB,CAAC,MAAM,EAAE,UAAU;IA8E/C;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,gBAAgB;IA6BxB,OAAO,CAAC,wBAAwB;IAOhC,WAAW,IAAI,IAAI;yCA/SR,qBAAqB;2CAArB,qBAAqB;CA6UjC"}
1
+ {"version":3,"file":"single-record.component.d.ts","sourceRoot":"","sources":["../../../src/lib/single-record/single-record.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAyB,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAA0B,MAAM,+BAA+B,CAAC;AAEnH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;;AAErE;;;;;;;;;;;;;GAaG;AACH,qBAMa,qBAAsB,SAAQ,oBAAoB;IAC7C,UAAU,EAAE,YAAY,CAAsB;IAC9C,UAAU,EAAE,MAAM,GAAG,IAAI,CAAM;IAC/B,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAM;IAE7D,YAAY,EAAE,YAAY,CAAC,IAAI,CAAC,CAA4B;IAC5D,WAAW,EAAE,YAAY,CAAC,UAAU,CAAC,CAAkC;IACxF,wFAAwF;IACvE,eAAe,EAAE,YAAY,CAAC,IAAI,CAAC,CAA4B;IAEhF,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,mBAAmB,CAA6B;IAExD,uEAAuE;IACvE,cAAc,IAAI,IAAI;IAItB,uEAAuE;IACvE,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAMvC,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIjC,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI;IAIlD,4EAA4E;IAC5E,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI;yCApCvC,qBAAqB;2CAArB,qBAAqB;CA4EjC"}
@@ -1,280 +1,61 @@
1
- import { ChangeDetectorRef, Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
2
- import { CompositeKey, FieldValueCollection, EntityFieldTSType } from '@memberjunction/core';
3
- import { MJGlobal } from '@memberjunction/global';
4
- import { Container } from '@memberjunction/ng-container-directives';
5
- import { BaseFormComponent } from '@memberjunction/ng-base-forms';
1
+ import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
2
+ import { CompositeKey } from '@memberjunction/core';
3
+ import { MJFormPresenterService } from '@memberjunction/ng-base-forms';
6
4
  import { NavigationService, RecentAccessService, SharedService } from '@memberjunction/ng-shared';
7
5
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
8
6
  import * as i0 from "@angular/core";
9
- import * as i1 from "@angular/router";
10
- import * as i2 from "@memberjunction/ng-container-directives";
11
- import * as i3 from "@memberjunction/ng-shared-generic";
12
- function SingleRecordComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
13
- i0.ɵɵelement(0, "mj-loading", 0);
14
- } if (rf & 2) {
15
- i0.ɵɵproperty("showText", false);
16
- } }
17
- function SingleRecordComponent_Conditional_1_Conditional_5_Template(rf, ctx) { if (rf & 1) {
18
- i0.ɵɵelementStart(0, "p", 6);
19
- i0.ɵɵtext(1);
20
- i0.ɵɵelementEnd();
21
- } if (rf & 2) {
22
- const ctx_r0 = i0.ɵɵnextContext(2);
23
- i0.ɵɵadvance();
24
- i0.ɵɵtextInterpolate(ctx_r0.errorDetail);
25
- } }
26
- function SingleRecordComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
27
- i0.ɵɵelementStart(0, "div", 1)(1, "div", 3);
28
- i0.ɵɵelement(2, "i", 4);
29
- i0.ɵɵelementEnd();
30
- i0.ɵɵelementStart(3, "h2", 5);
31
- i0.ɵɵtext(4);
32
- i0.ɵɵelementEnd();
33
- i0.ɵɵconditionalCreate(5, SingleRecordComponent_Conditional_1_Conditional_5_Template, 2, 1, "p", 6);
34
- i0.ɵɵelementStart(6, "p", 7);
35
- i0.ɵɵtext(7, "See browser console for technical details.");
36
- i0.ɵɵelementEnd()();
37
- } if (rf & 2) {
38
- const ctx_r0 = i0.ɵɵnextContext();
39
- i0.ɵɵadvance(4);
40
- i0.ɵɵtextInterpolate(ctx_r0.errorTitle);
41
- i0.ɵɵadvance();
42
- i0.ɵɵconditional(ctx_r0.errorDetail ? 5 : -1);
43
- } }
44
- function SingleRecordComponent_ng_template_2_Template(rf, ctx) { }
7
+ import * as i1 from "@memberjunction/ng-base-forms";
8
+ /**
9
+ * Explorer-side host for a single entity record in the main tab area.
10
+ *
11
+ * This is now a **thin wrapper** around the Generic `<mj-entity-form-host>`
12
+ * (in `@memberjunction/ng-base-forms`), which owns all the mechanics: resolving
13
+ * the form (class / custom / interactive override + variants), loading the
14
+ * record, dynamically creating the form, binding it, and tearing it down.
15
+ *
16
+ * SingleRecordComponent's only remaining job is the **Explorer mapping**:
17
+ * translating the host's framework-agnostic events into Explorer services —
18
+ * `Navigate` → {@link NavigationService}, `Notification` → {@link SharedService},
19
+ * record loads → {@link RecentAccessService} none of which belong in a Generic
20
+ * component.
21
+ */
45
22
  export class SingleRecordComponent extends BaseAngularComponent {
46
- route;
47
- formContainer;
48
23
  PrimaryKey = new CompositeKey();
49
24
  entityName = '';
50
25
  newRecordValues = '';
51
26
  loadComplete = new EventEmitter();
52
27
  recordSaved = new EventEmitter();
53
- recentAccessService;
28
+ /** Emitted when the hosted form asks to be dismissed (e.g. Discard on a new record). */
29
+ recordDismissed = new EventEmitter();
54
30
  navigationService = inject(NavigationService);
55
31
  sharedService = inject(SharedService);
56
- cdr = inject(ChangeDetectorRef);
57
- constructor(route) {
58
- super();
59
- this.route = route;
60
- this.recentAccessService = new RecentAccessService();
61
- }
62
- appDescription = '';
63
- useGenericForm = false;
64
- loading = true;
65
- errorTitle = null;
66
- errorDetail = null;
67
- // Track dynamically created components and entities for cleanup
68
- _formComponentRef = null;
69
- _currentRecord = null;
70
- _eventHandlerSubscription = null;
71
- _formEventSubscriptions = [];
72
- ngOnInit() {
73
- }
74
- ngAfterViewInit() {
75
- this.LoadForm(this.PrimaryKey, this.entityName);
76
- }
77
- async LoadForm(primaryKey, entityName) {
78
- // Perform any necessary actions with the ViewID, such as fetching data
79
- if (!entityName || entityName.trim().length === 0) {
80
- // No entity yet — caller will re-invoke once it has one. Don't emit loadComplete;
81
- // it would race the real load. The shell's recovery timer will surface the
82
- // "taking longer than expected" reset if this is the terminal state.
83
- return;
84
- }
85
- this.entityName = entityName;
86
- if (primaryKey.HasValue) {
87
- // we have an existing record to load up
88
- this.PrimaryKey = primaryKey;
89
- }
90
- else {
91
- // new record, no existing primary key
92
- this.PrimaryKey = new CompositeKey();
93
- }
94
- const formReg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseFormComponent, entityName);
95
- const md = this.ProviderToUse;
96
- const entity = md.EntityByName(entityName);
97
- const permissions = entity?.GetUserPermisions(md.CurrentUser);
98
- try {
99
- if (!entity) {
100
- this.failWithUserError(`Entity not found: "${entityName}"`, `This MemberJunction instance has no metadata for entity "${entityName}". Check that the entity name in the URL matches a row in __mj.Entity, and that the metadata cache is current.`);
101
- return;
102
- }
103
- if (!formReg) {
104
- this.failWithUserError(`No form is registered for "${entityName}".`, `MemberJunction could not find an Angular form component registered against BaseFormComponent for entity "${entityName}". This usually means CodeGen has not generated a form for this entity in the running build (forms live under packages/MJExplorer or your app's entity-form package). Run CodeGen, ensure the generated module is imported, or register a custom form via @RegisterClass(BaseFormComponent, '${entityName}').`, { entityId: entity.ID, recordKey: primaryKey?.ToString?.() ?? '(none)' });
105
- return;
106
- }
107
- const record = await md.GetEntityObject(entityName);
108
- if (!record) {
109
- throw new Error(`Unable to instantiate entity ${entityName} with primary key values: ${primaryKey.ToString()}`);
110
- }
111
- if (primaryKey.HasValue) {
112
- const loadOk = await record.InnerLoad(primaryKey);
113
- if (!loadOk) {
114
- this.failWithUserError(`Could not load ${entityName} record.`, record.LatestResult?.Message
115
- ? `Server error: ${record.LatestResult.Message}`
116
- : `InnerLoad returned false for entity "${entityName}" with key ${primaryKey.ToString()}. The record may not exist, you may lack permission to view it, or the load may have been blocked server-side.`, { recordKey: primaryKey.ToString() });
117
- return;
118
- }
119
- // Log access to existing record (fire-and-forget, don't await)
120
- this.recentAccessService.logAccess(entityName, primaryKey, 'record');
121
- }
122
- else {
123
- record.NewRecord();
124
- this.SetNewRecordValues(record);
125
- }
126
- // CRITICAL: Track the event handler subscription for cleanup
127
- this._eventHandlerSubscription = record.RegisterEventHandler((eventType) => {
128
- if (eventType.type === 'save')
129
- this.recordSaved.emit(record);
130
- });
131
- const viewContainerRef = this.formContainer.viewContainerRef;
132
- viewContainerRef.clear();
133
- const componentRef = viewContainerRef.createComponent(formReg.SubClass);
134
- // Track component and record for cleanup
135
- this._formComponentRef = componentRef;
136
- this._currentRecord = record;
137
- componentRef.instance.record = record;
138
- componentRef.instance.userPermissions = permissions;
139
- componentRef.instance.EditMode = !primaryKey.HasValue; // for new records go direct into edit mode
140
- // Subscribe to form @Output events and map them to Explorer services
141
- this.subscribeToFormEvents(componentRef.instance);
142
- this.useGenericForm = false;
143
- this.errorTitle = null;
144
- this.errorDetail = null;
145
- this.loadComplete.emit();
146
- }
147
- catch (err) {
148
- const msg = err instanceof Error ? err.message : String(err);
149
- this.failWithUserError(`Failed to load ${entityName} record.`, `An unexpected error occurred while loading this record: ${msg}`, { error: err });
150
- }
151
- this.loading = false;
152
- this.cdr.detectChanges();
153
- }
154
- /**
155
- * Render a user-visible error state inside the record pane AND log a structured
156
- * console.error for developers. Always emits `loadComplete` so the Explorer shell
157
- * does not hang on its first-resource-load gate.
158
- */
159
- failWithUserError(title, detail, context) {
160
- this.errorTitle = title;
161
- this.errorDetail = detail;
162
- this.loading = false;
163
- // Single structured console.error for devs — easy to grep, easy to read.
164
- console.error(`[SingleRecord] ${title}\n${detail}` +
165
- (context ? `\nContext: ${JSON.stringify(context, null, 2)}` : ''));
166
- // Clear any prior form/component so the error UI is what shows.
167
- if (this._formComponentRef) {
168
- try {
169
- this._formComponentRef.destroy();
170
- }
171
- catch { /* noop */ }
172
- this._formComponentRef = null;
173
- }
174
- if (this.formContainer?.viewContainerRef) {
175
- this.formContainer.viewContainerRef.clear();
176
- }
177
- // Always unblock the shell.
32
+ formPresenter = inject(MJFormPresenterService);
33
+ recentAccessService = new RecentAccessService();
34
+ /** Unblock the shell's first-resource-load gate (success or error). */
35
+ onLoadComplete() {
178
36
  this.loadComplete.emit();
179
- this.cdr.detectChanges();
180
37
  }
181
- SetNewRecordValues(record) {
182
- if (!this.newRecordValues) {
183
- return;
184
- }
185
- // Handle both object and string (URL segment) formats
186
- if (typeof this.newRecordValues === 'string') {
187
- if (this.newRecordValues.length === 0) {
188
- return;
189
- }
190
- // we have a URL segment string format: "field1|value1||field2|value2"
191
- const fv = new FieldValueCollection();
192
- fv.SimpleLoadFromURLSegment(this.newRecordValues);
193
- // now apply the values to the record
194
- fv.KeyValuePairs.filter(kvp => kvp.Value !== null && kvp.Value !== undefined).forEach(kvp => {
195
- const f = record.Fields.find(f => f.Name.trim().toLowerCase() === kvp.FieldName.trim().toLowerCase());
196
- if (f) {
197
- // make sure we set the value to the right type based on the f.TSType property
198
- switch (f.EntityFieldInfo.TSType) {
199
- case EntityFieldTSType.String:
200
- record.Set(kvp.FieldName, kvp.Value);
201
- break;
202
- case EntityFieldTSType.Number:
203
- record.Set(kvp.FieldName, parseFloat(kvp.Value));
204
- break;
205
- case EntityFieldTSType.Boolean:
206
- if (kvp.Value === 'false' || kvp.Value === '0' || kvp.Value.toString().trim().length === 0)
207
- record.Set(kvp.FieldName, false);
208
- else
209
- record.Set(kvp.FieldName, true);
210
- break;
211
- case EntityFieldTSType.Date:
212
- record.Set(kvp.FieldName, new Date(kvp.Value));
213
- break;
214
- }
215
- }
216
- });
217
- }
218
- else {
219
- // we have a plain object format: { field1: value1, field2: value2 }
220
- const recordValues = this.newRecordValues;
221
- Object.keys(recordValues)
222
- .filter(key => recordValues[key] !== null && recordValues[key] !== undefined)
223
- .forEach(key => {
224
- const f = record.Fields.find(f => f.Name.trim().toLowerCase() === key.trim().toLowerCase());
225
- if (f) {
226
- const value = recordValues[key];
227
- // Set the value with proper type conversion
228
- switch (f.EntityFieldInfo.TSType) {
229
- case EntityFieldTSType.String:
230
- record.Set(key, value?.toString() || '');
231
- break;
232
- case EntityFieldTSType.Number:
233
- record.Set(key, typeof value === 'number' ? value : parseFloat(value?.toString() || '0'));
234
- break;
235
- case EntityFieldTSType.Boolean:
236
- if (typeof value === 'boolean') {
237
- record.Set(key, value);
238
- }
239
- else if (typeof value === 'string') {
240
- record.Set(key, value !== 'false' && value !== '0' && value.trim().length > 0);
241
- }
242
- else {
243
- record.Set(key, !!value);
244
- }
245
- break;
246
- case EntityFieldTSType.Date:
247
- record.Set(key, value instanceof Date ? value : new Date(value?.toString() || ''));
248
- break;
249
- default:
250
- record.Set(key, value);
251
- break;
252
- }
253
- }
254
- });
38
+ /** Log access for existing records once the form's record is ready. */
39
+ onRecordReady(record) {
40
+ if (record?.IsSaved) {
41
+ this.recentAccessService.logAccess(record.EntityInfo.Name, record.PrimaryKey, 'record');
255
42
  }
256
43
  }
257
- /**
258
- * Subscribe to BaseFormComponent @Output events and map them to Explorer services.
259
- */
260
- subscribeToFormEvents(form) {
261
- this.cleanupFormSubscriptions();
262
- this._formEventSubscriptions.push(form.Navigate.subscribe((event) => this.handleNavigation(event)), form.Notification.subscribe((event) => {
263
- this.sharedService.CreateSimpleNotification(event.Message, event.Type, event.Duration);
264
- }));
44
+ onSaved(record) {
45
+ this.recordSaved.emit(record);
265
46
  }
47
+ onNotification(event) {
48
+ this.sharedService.CreateSimpleNotification(event.Message, event.Type, event.Duration);
49
+ }
50
+ /** Map the form's navigation requests onto Explorer's NavigationService. */
266
51
  handleNavigation(event) {
267
52
  switch (event.Kind) {
268
53
  case 'record':
269
54
  this.navigationService.OpenEntityRecord(event.EntityName, event.PrimaryKey, { forceNewTab: event.OpenInNewTab });
270
55
  break;
271
56
  case 'new-record':
272
- // Creating a new related record from inside an open record form (e.g. + New
273
- // on a related-entity grid). Force a new tab so the parent record stays
274
- // intact — otherwise the new-record form silently replaces the parent in
275
- // single-resource mode and the user loses their context. This is the
276
- // original intent of dea32401ff, now stated explicitly at the call site
277
- // instead of as a global navigation heuristic.
57
+ // Creating a related record from inside an open form: force a new tab so the
58
+ // parent record stays intact in single-resource mode.
278
59
  this.navigationService.OpenNewEntityRecord(event.EntityName, {
279
60
  newRecordValues: event.DefaultValues,
280
61
  forceNewTab: true,
@@ -289,62 +70,37 @@ export class SingleRecordComponent extends BaseAngularComponent {
289
70
  case 'email':
290
71
  window.open(`mailto:${event.EmailAddress}`, '_self');
291
72
  break;
73
+ case 'dismiss':
74
+ this.recordDismissed.emit();
75
+ break;
76
+ case 'create-related': {
77
+ // A FK field wants a new related record created. Open the related entity's form
78
+ // as a dialog/slide-in (prefilled), then hand the saved record back so the field
79
+ // can select it.
80
+ const ref = this.formPresenter.Open({
81
+ EntityName: event.EntityName,
82
+ Presentation: event.Presentation ?? 'dialog',
83
+ NewRecordValues: event.NewRecordValues,
84
+ Provider: event.Provider,
85
+ });
86
+ ref.AfterSaved().then(created => event.Complete(created));
87
+ break;
88
+ }
292
89
  }
293
90
  }
294
- cleanupFormSubscriptions() {
295
- for (const sub of this._formEventSubscriptions) {
296
- sub.unsubscribe();
297
- }
298
- this._formEventSubscriptions = [];
299
- }
300
- ngOnDestroy() {
301
- // CRITICAL: Clean up form event subscriptions first
302
- this.cleanupFormSubscriptions();
303
- // CRITICAL: Clean up dynamically created form component to prevent zombie components
304
- if (this._formComponentRef) {
305
- this._formComponentRef.destroy();
306
- this._formComponentRef = null;
307
- }
308
- // CRITICAL: Unsubscribe from event handler to prevent memory leaks
309
- if (this._eventHandlerSubscription) {
310
- this._eventHandlerSubscription.unsubscribe();
311
- this._eventHandlerSubscription = null;
312
- }
313
- // Clean up record reference
314
- if (this._currentRecord) {
315
- this._currentRecord = null;
316
- }
317
- // Clear the view container to ensure no lingering references
318
- if (this.formContainer?.viewContainerRef) {
319
- this.formContainer.viewContainerRef.clear();
320
- }
321
- // Reset state
322
- this.loading = true;
323
- this.useGenericForm = false;
324
- }
325
- static ɵfac = function SingleRecordComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SingleRecordComponent)(i0.ɵɵdirectiveInject(i1.ActivatedRoute)); };
326
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SingleRecordComponent, selectors: [["mj-single-record"]], viewQuery: function SingleRecordComponent_Query(rf, ctx) { if (rf & 1) {
327
- i0.ɵɵviewQuery(Container, 7);
91
+ static ɵfac = /*@__PURE__*/ (() => { let ɵSingleRecordComponent_BaseFactory; return function SingleRecordComponent_Factory(__ngFactoryType__) { return (ɵSingleRecordComponent_BaseFactory || (ɵSingleRecordComponent_BaseFactory = i0.ɵɵgetInheritedFactory(SingleRecordComponent)))(__ngFactoryType__ || SingleRecordComponent); }; })();
92
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SingleRecordComponent, selectors: [["mj-single-record"]], inputs: { PrimaryKey: "PrimaryKey", entityName: "entityName", newRecordValues: "newRecordValues" }, outputs: { loadComplete: "loadComplete", recordSaved: "recordSaved", recordDismissed: "recordDismissed" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 1, vars: 4, consts: [[3, "LoadComplete", "LoadError", "RecordReady", "Saved", "Navigate", "Notification", "Dismissed", "EntityName", "PrimaryKey", "NewRecordValues", "Provider"]], template: function SingleRecordComponent_Template(rf, ctx) { if (rf & 1) {
93
+ i0.ɵɵelementStart(0, "mj-entity-form-host", 0);
94
+ i0.ɵɵlistener("LoadComplete", function SingleRecordComponent_Template_mj_entity_form_host_LoadComplete_0_listener() { return ctx.onLoadComplete(); })("LoadError", function SingleRecordComponent_Template_mj_entity_form_host_LoadError_0_listener() { return ctx.onLoadComplete(); })("RecordReady", function SingleRecordComponent_Template_mj_entity_form_host_RecordReady_0_listener($event) { return ctx.onRecordReady($event); })("Saved", function SingleRecordComponent_Template_mj_entity_form_host_Saved_0_listener($event) { return ctx.onSaved($event); })("Navigate", function SingleRecordComponent_Template_mj_entity_form_host_Navigate_0_listener($event) { return ctx.handleNavigation($event); })("Notification", function SingleRecordComponent_Template_mj_entity_form_host_Notification_0_listener($event) { return ctx.onNotification($event); })("Dismissed", function SingleRecordComponent_Template_mj_entity_form_host_Dismissed_0_listener() { return ctx.recordDismissed.emit(); });
95
+ i0.ɵɵelementEnd();
328
96
  } if (rf & 2) {
329
- let _t;
330
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.formContainer = _t.first);
331
- } }, inputs: { PrimaryKey: "PrimaryKey", entityName: "entityName", newRecordValues: "newRecordValues" }, outputs: { loadComplete: "loadComplete", recordSaved: "recordSaved" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 3, vars: 2, consts: [["size", "large", 3, "showText"], ["role", "alert", 1, "single-record-error"], ["mjContainer", ""], [1, "single-record-error__icon"], [1, "fa-solid", "fa-triangle-exclamation"], [1, "single-record-error__title"], [1, "single-record-error__detail"], [1, "single-record-error__hint"]], template: function SingleRecordComponent_Template(rf, ctx) { if (rf & 1) {
332
- i0.ɵɵconditionalCreate(0, SingleRecordComponent_Conditional_0_Template, 1, 1, "mj-loading", 0);
333
- i0.ɵɵconditionalCreate(1, SingleRecordComponent_Conditional_1_Template, 8, 2, "div", 1);
334
- i0.ɵɵtemplate(2, SingleRecordComponent_ng_template_2_Template, 0, 0, "ng-template", 2);
335
- } if (rf & 2) {
336
- i0.ɵɵconditional(ctx.loading ? 0 : -1);
337
- i0.ɵɵadvance();
338
- i0.ɵɵconditional(ctx.errorTitle ? 1 : -1);
339
- } }, dependencies: [i2.Container, i3.LoadingComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n.single-record-error[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 48px 32px;\n max-width: 720px;\n margin: 64px auto;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-status-error-border);\n border-radius: 8px;\n color: var(--mj-text-primary);\n}\n\n.single-record-error__icon[_ngcontent-%COMP%] {\n font-size: 36px;\n color: var(--mj-status-error);\n margin-bottom: 16px;\n}\n\n.single-record-error__title[_ngcontent-%COMP%] {\n font-size: 18px;\n font-weight: 600;\n margin: 0 0 12px 0;\n color: var(--mj-status-error-text);\n}\n\n.single-record-error__detail[_ngcontent-%COMP%] {\n font-size: 14px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n margin: 0 0 16px 0;\n white-space: pre-wrap;\n}\n\n.single-record-error__hint[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin: 0;\n font-style: italic;\n}"] });
97
+ i0.ɵɵproperty("EntityName", ctx.entityName)("PrimaryKey", ctx.PrimaryKey)("NewRecordValues", ctx.newRecordValues)("Provider", ctx.Provider);
98
+ } }, dependencies: [i1.MjEntityFormHostComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n.single-record-error[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 48px 32px;\n max-width: 720px;\n margin: 64px auto;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-status-error-border);\n border-radius: 8px;\n color: var(--mj-text-primary);\n}\n\n.single-record-error__icon[_ngcontent-%COMP%] {\n font-size: 36px;\n color: var(--mj-status-error);\n margin-bottom: 16px;\n}\n\n.single-record-error__title[_ngcontent-%COMP%] {\n font-size: 18px;\n font-weight: 600;\n margin: 0 0 12px 0;\n color: var(--mj-status-error-text);\n}\n\n.single-record-error__detail[_ngcontent-%COMP%] {\n font-size: 14px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n margin: 0 0 16px 0;\n white-space: pre-wrap;\n}\n\n.single-record-error__hint[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin: 0;\n font-style: italic;\n}"] });
340
99
  }
341
100
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SingleRecordComponent, [{
342
101
  type: Component,
343
- args: [{ standalone: false, selector: 'mj-single-record', template: "@if (loading) {\n <mj-loading [showText]=\"false\" size=\"large\"></mj-loading>\n}\n@if (errorTitle) {\n <div class=\"single-record-error\" role=\"alert\">\n <div class=\"single-record-error__icon\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n </div>\n <h2 class=\"single-record-error__title\">{{ errorTitle }}</h2>\n @if (errorDetail) {\n <p class=\"single-record-error__detail\">{{ errorDetail }}</p>\n }\n <p class=\"single-record-error__hint\">See browser console for technical details.</p>\n </div>\n}\n<ng-template mjContainer></ng-template>\n", styles: [":host {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n.single-record-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 48px 32px;\n max-width: 720px;\n margin: 64px auto;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-status-error-border);\n border-radius: 8px;\n color: var(--mj-text-primary);\n}\n\n.single-record-error__icon {\n font-size: 36px;\n color: var(--mj-status-error);\n margin-bottom: 16px;\n}\n\n.single-record-error__title {\n font-size: 18px;\n font-weight: 600;\n margin: 0 0 12px 0;\n color: var(--mj-status-error-text);\n}\n\n.single-record-error__detail {\n font-size: 14px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n margin: 0 0 16px 0;\n white-space: pre-wrap;\n}\n\n.single-record-error__hint {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin: 0;\n font-style: italic;\n}\n"] }]
344
- }], () => [{ type: i1.ActivatedRoute }], { formContainer: [{
345
- type: ViewChild,
346
- args: [Container, { static: true }]
347
- }], PrimaryKey: [{
102
+ args: [{ standalone: false, selector: 'mj-single-record', template: "<mj-entity-form-host\n [EntityName]=\"entityName\"\n [PrimaryKey]=\"PrimaryKey\"\n [NewRecordValues]=\"newRecordValues\"\n [Provider]=\"Provider\"\n (LoadComplete)=\"onLoadComplete()\"\n (LoadError)=\"onLoadComplete()\"\n (RecordReady)=\"onRecordReady($event)\"\n (Saved)=\"onSaved($event)\"\n (Navigate)=\"handleNavigation($event)\"\n (Notification)=\"onNotification($event)\"\n (Dismissed)=\"recordDismissed.emit()\">\n</mj-entity-form-host>\n", styles: [":host {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n.single-record-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 48px 32px;\n max-width: 720px;\n margin: 64px auto;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-status-error-border);\n border-radius: 8px;\n color: var(--mj-text-primary);\n}\n\n.single-record-error__icon {\n font-size: 36px;\n color: var(--mj-status-error);\n margin-bottom: 16px;\n}\n\n.single-record-error__title {\n font-size: 18px;\n font-weight: 600;\n margin: 0 0 12px 0;\n color: var(--mj-status-error-text);\n}\n\n.single-record-error__detail {\n font-size: 14px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n margin: 0 0 16px 0;\n white-space: pre-wrap;\n}\n\n.single-record-error__hint {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin: 0;\n font-style: italic;\n}\n"] }]
103
+ }], null, { PrimaryKey: [{
348
104
  type: Input
349
105
  }], entityName: [{
350
106
  type: Input
@@ -354,6 +110,8 @@ export class SingleRecordComponent extends BaseAngularComponent {
354
110
  type: Output
355
111
  }], recordSaved: [{
356
112
  type: Output
113
+ }], recordDismissed: [{
114
+ type: Output
357
115
  }] }); })();
358
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SingleRecordComponent, { className: "SingleRecordComponent", filePath: "src/lib/single-record/single-record.component.ts", lineNumber: 18 }); })();
116
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SingleRecordComponent, { className: "SingleRecordComponent", filePath: "src/lib/single-record/single-record.component.ts", lineNumber: 27 }); })();
359
117
  //# sourceMappingURL=single-record.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"single-record.component.js","sourceRoot":"","sources":["../../../src/lib/single-record/single-record.component.ts","../../../src/lib/single-record/single-record.component.html"],"names":[],"mappings":"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","sourcesContent":["import { AfterViewInit, ChangeDetectorRef, Component, ComponentRef, EventEmitter, inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router'\nimport { Metadata, KeyValuePair, CompositeKey, BaseEntity, BaseEntityEvent, FieldValueCollection, EntityFieldTSType } from '@memberjunction/core';\nimport { Subscription } from 'rxjs';\nimport { MJGlobal } from '@memberjunction/global';\nimport { Container } from '@memberjunction/ng-container-directives';\nimport { BaseFormComponent, FormNavigationEvent, FormNotificationEvent } from '@memberjunction/ng-base-forms';\nimport { NavigationService, RecentAccessService, SharedService } from '@memberjunction/ng-shared';\n\n\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\n@Component({\n standalone: false,\n selector: 'mj-single-record',\n templateUrl: './single-record.component.html',\n styleUrls: ['./single-record.component.css']\n})\nexport class SingleRecordComponent extends BaseAngularComponent implements OnInit, AfterViewInit, OnDestroy {\n @ViewChild(Container, {static: true}) formContainer!: Container;\n @Input() public PrimaryKey: CompositeKey = new CompositeKey();\n @Input() public entityName: string | null = '';\n @Input() public newRecordValues: string | Record<string, unknown> | null = '';\n\n @Output() public loadComplete: EventEmitter<any> = new EventEmitter<any>();\n @Output() public recordSaved: EventEmitter<BaseEntity> = new EventEmitter<BaseEntity>();\n\n private recentAccessService: RecentAccessService;\n private navigationService = inject(NavigationService);\n private sharedService = inject(SharedService);\n private cdr = inject(ChangeDetectorRef);\n\n constructor (private route: ActivatedRoute) {\n super();\n this.recentAccessService = new RecentAccessService();\n }\n\n public appDescription: string = ''\n public useGenericForm: boolean = false;\n public loading: boolean = true;\n public errorTitle: string | null = null;\n public errorDetail: string | null = null;\n\n // Track dynamically created components and entities for cleanup\n private _formComponentRef: ComponentRef<BaseFormComponent> | null = null;\n private _currentRecord: BaseEntity | null = null;\n private _eventHandlerSubscription: Subscription | null = null;\n private _formEventSubscriptions: Subscription[] = [];\n\n ngOnInit(): void {\n }\n\n ngAfterViewInit() {\n this.LoadForm(this.PrimaryKey, <string>this.entityName)\n }\n\n public async LoadForm(primaryKey: CompositeKey, entityName: string) {\n // Perform any necessary actions with the ViewID, such as fetching data\n if (!entityName || entityName.trim().length === 0) {\n // No entity yet — caller will re-invoke once it has one. Don't emit loadComplete;\n // it would race the real load. The shell's recovery timer will surface the\n // \"taking longer than expected\" reset if this is the terminal state.\n return;\n }\n\n this.entityName = entityName;\n if (primaryKey.HasValue) {\n // we have an existing record to load up\n this.PrimaryKey = primaryKey;\n }\n else {\n // new record, no existing primary key\n this.PrimaryKey = new CompositeKey();\n }\n\n const formReg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseFormComponent, entityName);\n\n const md = this.ProviderToUse;\n const entity = md.EntityByName(entityName);\n const permissions = entity?.GetUserPermisions(md.CurrentUser);\n\n try {\n if (!entity) {\n this.failWithUserError(\n `Entity not found: \"${entityName}\"`,\n `This MemberJunction instance has no metadata for entity \"${entityName}\". Check that the entity name in the URL matches a row in __mj.Entity, and that the metadata cache is current.`\n );\n return;\n }\n\n if (!formReg) {\n this.failWithUserError(\n `No form is registered for \"${entityName}\".`,\n `MemberJunction could not find an Angular form component registered against BaseFormComponent for entity \"${entityName}\". This usually means CodeGen has not generated a form for this entity in the running build (forms live under packages/MJExplorer or your app's entity-form package). Run CodeGen, ensure the generated module is imported, or register a custom form via @RegisterClass(BaseFormComponent, '${entityName}').`,\n { entityId: entity.ID, recordKey: primaryKey?.ToString?.() ?? '(none)' }\n );\n return;\n }\n\n const record = await md.GetEntityObject<BaseEntity>(entityName);\n if (!record) {\n throw new Error(`Unable to instantiate entity ${entityName} with primary key values: ${primaryKey.ToString()}`);\n }\n\n if (primaryKey.HasValue) {\n const loadOk = await record.InnerLoad(primaryKey);\n if (!loadOk) {\n this.failWithUserError(\n `Could not load ${entityName} record.`,\n record.LatestResult?.Message\n ? `Server error: ${record.LatestResult.Message}`\n : `InnerLoad returned false for entity \"${entityName}\" with key ${primaryKey.ToString()}. The record may not exist, you may lack permission to view it, or the load may have been blocked server-side.`,\n { recordKey: primaryKey.ToString() }\n );\n return;\n }\n // Log access to existing record (fire-and-forget, don't await)\n this.recentAccessService.logAccess(entityName, primaryKey, 'record');\n }\n else {\n record.NewRecord();\n this.SetNewRecordValues(record);\n }\n\n // CRITICAL: Track the event handler subscription for cleanup\n this._eventHandlerSubscription = record.RegisterEventHandler((eventType: BaseEntityEvent) => {\n if (eventType.type === 'save')\n this.recordSaved.emit(record);\n });\n\n const viewContainerRef = this.formContainer.viewContainerRef;\n viewContainerRef.clear();\n\n const componentRef = viewContainerRef.createComponent<typeof formReg.SubClass>(formReg.SubClass);\n\n // Track component and record for cleanup\n this._formComponentRef = componentRef;\n this._currentRecord = record;\n\n componentRef.instance.record = record\n componentRef.instance.userPermissions = permissions\n componentRef.instance.EditMode = !primaryKey.HasValue; // for new records go direct into edit mode\n\n // Subscribe to form @Output events and map them to Explorer services\n this.subscribeToFormEvents(componentRef.instance);\n\n this.useGenericForm = false;\n this.errorTitle = null;\n this.errorDetail = null;\n this.loadComplete.emit();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n this.failWithUserError(\n `Failed to load ${entityName} record.`,\n `An unexpected error occurred while loading this record: ${msg}`,\n { error: err }\n );\n }\n\n this.loading = false;\n this.cdr.detectChanges();\n }\n\n /**\n * Render a user-visible error state inside the record pane AND log a structured\n * console.error for developers. Always emits `loadComplete` so the Explorer shell\n * does not hang on its first-resource-load gate.\n */\n private failWithUserError(title: string, detail: string, context?: Record<string, unknown>): void {\n this.errorTitle = title;\n this.errorDetail = detail;\n this.loading = false;\n\n // Single structured console.error for devs — easy to grep, easy to read.\n console.error(\n `[SingleRecord] ${title}\\n${detail}` +\n (context ? `\\nContext: ${JSON.stringify(context, null, 2)}` : '')\n );\n\n // Clear any prior form/component so the error UI is what shows.\n if (this._formComponentRef) {\n try { this._formComponentRef.destroy(); } catch { /* noop */ }\n this._formComponentRef = null;\n }\n if (this.formContainer?.viewContainerRef) {\n this.formContainer.viewContainerRef.clear();\n }\n\n // Always unblock the shell.\n this.loadComplete.emit();\n this.cdr.detectChanges();\n }\n\n protected SetNewRecordValues(record: BaseEntity) {\n if (!this.newRecordValues) {\n return;\n }\n\n // Handle both object and string (URL segment) formats\n if (typeof this.newRecordValues === 'string') {\n if (this.newRecordValues.length === 0) {\n return;\n }\n // we have a URL segment string format: \"field1|value1||field2|value2\"\n const fv = new FieldValueCollection();\n fv.SimpleLoadFromURLSegment(this.newRecordValues);\n // now apply the values to the record\n fv.KeyValuePairs.filter(kvp => kvp.Value !== null && kvp.Value !== undefined).forEach(kvp => {\n const f = record.Fields.find(f => f.Name.trim().toLowerCase() === kvp.FieldName.trim().toLowerCase());\n if (f) {\n // make sure we set the value to the right type based on the f.TSType property\n switch (f.EntityFieldInfo.TSType) {\n case EntityFieldTSType.String:\n record.Set(kvp.FieldName, kvp.Value);\n break;\n case EntityFieldTSType.Number:\n record.Set(kvp.FieldName, parseFloat(kvp.Value));\n break;\n case EntityFieldTSType.Boolean:\n if (kvp.Value === 'false' || kvp.Value === '0' || kvp.Value.toString().trim().length === 0 )\n record.Set(kvp.FieldName, false);\n else\n record.Set(kvp.FieldName, true);\n break;\n case EntityFieldTSType.Date:\n record.Set(kvp.FieldName, new Date(kvp.Value));\n break;\n }\n }\n });\n }\n else {\n // we have a plain object format: { field1: value1, field2: value2 }\n const recordValues = this.newRecordValues as Record<string, unknown>;\n Object.keys(recordValues)\n .filter(key => recordValues[key] !== null && recordValues[key] !== undefined)\n .forEach(key => {\n const f = record.Fields.find(f => f.Name.trim().toLowerCase() === key.trim().toLowerCase());\n if (f) {\n const value = recordValues[key];\n // Set the value with proper type conversion\n switch (f.EntityFieldInfo.TSType) {\n case EntityFieldTSType.String:\n record.Set(key, value?.toString() || '');\n break;\n case EntityFieldTSType.Number:\n record.Set(key, typeof value === 'number' ? value : parseFloat(value?.toString() || '0'));\n break;\n case EntityFieldTSType.Boolean:\n if (typeof value === 'boolean') {\n record.Set(key, value);\n }\n else if (typeof value === 'string') {\n record.Set(key, value !== 'false' && value !== '0' && value.trim().length > 0);\n }\n else {\n record.Set(key, !!value);\n }\n break;\n case EntityFieldTSType.Date:\n record.Set(key, value instanceof Date ? value : new Date(value?.toString() || ''));\n break;\n default:\n record.Set(key, value);\n break;\n }\n }\n });\n }\n }\n\n /**\n * Subscribe to BaseFormComponent @Output events and map them to Explorer services.\n */\n private subscribeToFormEvents(form: BaseFormComponent): void {\n this.cleanupFormSubscriptions();\n\n this._formEventSubscriptions.push(\n form.Navigate.subscribe((event: FormNavigationEvent) => this.handleNavigation(event)),\n form.Notification.subscribe((event: FormNotificationEvent) => {\n this.sharedService.CreateSimpleNotification(event.Message, event.Type, event.Duration);\n })\n );\n }\n\n private handleNavigation(event: FormNavigationEvent): void {\n switch (event.Kind) {\n case 'record':\n this.navigationService.OpenEntityRecord(event.EntityName, event.PrimaryKey, { forceNewTab: event.OpenInNewTab });\n break;\n case 'new-record':\n // Creating a new related record from inside an open record form (e.g. + New\n // on a related-entity grid). Force a new tab so the parent record stays\n // intact — otherwise the new-record form silently replaces the parent in\n // single-resource mode and the user loses their context. This is the\n // original intent of dea32401ff, now stated explicitly at the call site\n // instead of as a global navigation heuristic.\n this.navigationService.OpenNewEntityRecord(event.EntityName, {\n newRecordValues: event.DefaultValues,\n forceNewTab: true,\n });\n break;\n case 'entity-hierarchy':\n this.navigationService.OpenEntityRecord(event.EntityName, event.PrimaryKey);\n break;\n case 'external-link':\n window.open(event.Url, '_blank');\n break;\n case 'email':\n window.open(`mailto:${event.EmailAddress}`, '_self');\n break;\n }\n }\n\n private cleanupFormSubscriptions(): void {\n for (const sub of this._formEventSubscriptions) {\n sub.unsubscribe();\n }\n this._formEventSubscriptions = [];\n }\n\n ngOnDestroy(): void {\n // CRITICAL: Clean up form event subscriptions first\n this.cleanupFormSubscriptions();\n\n // CRITICAL: Clean up dynamically created form component to prevent zombie components\n if (this._formComponentRef) {\n this._formComponentRef.destroy();\n this._formComponentRef = null;\n }\n\n // CRITICAL: Unsubscribe from event handler to prevent memory leaks\n if (this._eventHandlerSubscription) {\n this._eventHandlerSubscription.unsubscribe();\n this._eventHandlerSubscription = null;\n }\n \n // Clean up record reference\n if (this._currentRecord) {\n this._currentRecord = null;\n }\n \n // Clear the view container to ensure no lingering references\n if (this.formContainer?.viewContainerRef) {\n this.formContainer.viewContainerRef.clear();\n }\n \n // Reset state\n this.loading = true;\n this.useGenericForm = false;\n }\n}\n","@if (loading) {\n <mj-loading [showText]=\"false\" size=\"large\"></mj-loading>\n}\n@if (errorTitle) {\n <div class=\"single-record-error\" role=\"alert\">\n <div class=\"single-record-error__icon\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n </div>\n <h2 class=\"single-record-error__title\">{{ errorTitle }}</h2>\n @if (errorDetail) {\n <p class=\"single-record-error__detail\">{{ errorDetail }}</p>\n }\n <p class=\"single-record-error__hint\">See browser console for technical details.</p>\n </div>\n}\n<ng-template mjContainer></ng-template>\n"]}
1
+ {"version":3,"file":"single-record.component.js","sourceRoot":"","sources":["../../../src/lib/single-record/single-record.component.ts","../../../src/lib/single-record/single-record.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, EventEmitter, inject, Input, Output } from '@angular/core';\nimport { CompositeKey, BaseEntity } from '@memberjunction/core';\nimport { FormNavigationEvent, FormNotificationEvent, MJFormPresenterService } from '@memberjunction/ng-base-forms';\nimport { NavigationService, RecentAccessService, SharedService } from '@memberjunction/ng-shared';\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\n\n/**\n * Explorer-side host for a single entity record in the main tab area.\n *\n * This is now a **thin wrapper** around the Generic `<mj-entity-form-host>`\n * (in `@memberjunction/ng-base-forms`), which owns all the mechanics: resolving\n * the form (class / custom / interactive override + variants), loading the\n * record, dynamically creating the form, binding it, and tearing it down.\n *\n * SingleRecordComponent's only remaining job is the **Explorer mapping**:\n * translating the host's framework-agnostic events into Explorer services —\n * `Navigate` → {@link NavigationService}, `Notification` → {@link SharedService},\n * record loads → {@link RecentAccessService} — none of which belong in a Generic\n * component.\n */\n@Component({\n standalone: false,\n selector: 'mj-single-record',\n templateUrl: './single-record.component.html',\n styleUrls: ['./single-record.component.css']\n})\nexport class SingleRecordComponent extends BaseAngularComponent {\n @Input() public PrimaryKey: CompositeKey = new CompositeKey();\n @Input() public entityName: string | null = '';\n @Input() public newRecordValues: string | Record<string, unknown> | null = '';\n\n @Output() public loadComplete: EventEmitter<void> = new EventEmitter<void>();\n @Output() public recordSaved: EventEmitter<BaseEntity> = new EventEmitter<BaseEntity>();\n /** Emitted when the hosted form asks to be dismissed (e.g. Discard on a new record). */\n @Output() public recordDismissed: EventEmitter<void> = new EventEmitter<void>();\n\n private navigationService = inject(NavigationService);\n private sharedService = inject(SharedService);\n private formPresenter = inject(MJFormPresenterService);\n private recentAccessService = new RecentAccessService();\n\n /** Unblock the shell's first-resource-load gate (success or error). */\n onLoadComplete(): void {\n this.loadComplete.emit();\n }\n\n /** Log access for existing records once the form's record is ready. */\n onRecordReady(record: BaseEntity): void {\n if (record?.IsSaved) {\n this.recentAccessService.logAccess(record.EntityInfo.Name, record.PrimaryKey, 'record');\n }\n }\n\n onSaved(record: BaseEntity): void {\n this.recordSaved.emit(record);\n }\n\n onNotification(event: FormNotificationEvent): void {\n this.sharedService.CreateSimpleNotification(event.Message, event.Type, event.Duration);\n }\n\n /** Map the form's navigation requests onto Explorer's NavigationService. */\n handleNavigation(event: FormNavigationEvent): void {\n switch (event.Kind) {\n case 'record':\n this.navigationService.OpenEntityRecord(event.EntityName, event.PrimaryKey, { forceNewTab: event.OpenInNewTab });\n break;\n case 'new-record':\n // Creating a related record from inside an open form: force a new tab so the\n // parent record stays intact in single-resource mode.\n this.navigationService.OpenNewEntityRecord(event.EntityName, {\n newRecordValues: event.DefaultValues,\n forceNewTab: true,\n });\n break;\n case 'entity-hierarchy':\n this.navigationService.OpenEntityRecord(event.EntityName, event.PrimaryKey);\n break;\n case 'external-link':\n window.open(event.Url, '_blank');\n break;\n case 'email':\n window.open(`mailto:${event.EmailAddress}`, '_self');\n break;\n case 'dismiss':\n this.recordDismissed.emit();\n break;\n case 'create-related': {\n // A FK field wants a new related record created. Open the related entity's form\n // as a dialog/slide-in (prefilled), then hand the saved record back so the field\n // can select it.\n const ref = this.formPresenter.Open({\n EntityName: event.EntityName,\n Presentation: event.Presentation ?? 'dialog',\n NewRecordValues: event.NewRecordValues,\n Provider: event.Provider,\n });\n ref.AfterSaved().then(created => event.Complete(created));\n break;\n }\n }\n }\n}\n","<mj-entity-form-host\n [EntityName]=\"entityName\"\n [PrimaryKey]=\"PrimaryKey\"\n [NewRecordValues]=\"newRecordValues\"\n [Provider]=\"Provider\"\n (LoadComplete)=\"onLoadComplete()\"\n (LoadError)=\"onLoadComplete()\"\n (RecordReady)=\"onRecordReady($event)\"\n (Saved)=\"onSaved($event)\"\n (Navigate)=\"handleNavigation($event)\"\n (Notification)=\"onNotification($event)\"\n (Dismissed)=\"recordDismissed.emit()\">\n</mj-entity-form-host>\n"]}