@memberjunction/ng-base-forms 3.4.0 → 4.1.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 (80) hide show
  1. package/dist/lib/base-form-component.d.ts +103 -215
  2. package/dist/lib/base-form-component.d.ts.map +1 -1
  3. package/dist/lib/base-form-component.js +384 -544
  4. package/dist/lib/base-form-component.js.map +1 -1
  5. package/dist/lib/container/record-form-container.component.d.ts +165 -0
  6. package/dist/lib/container/record-form-container.component.d.ts.map +1 -0
  7. package/dist/lib/container/record-form-container.component.js +534 -0
  8. package/dist/lib/container/record-form-container.component.js.map +1 -0
  9. package/dist/lib/explorer-entity-data-grid.component.d.ts +7 -9
  10. package/dist/lib/explorer-entity-data-grid.component.d.ts.map +1 -1
  11. package/dist/lib/explorer-entity-data-grid.component.js +15 -16
  12. package/dist/lib/explorer-entity-data-grid.component.js.map +1 -1
  13. package/dist/lib/field/form-field.component.d.ts +268 -0
  14. package/dist/lib/field/form-field.component.d.ts.map +1 -0
  15. package/dist/lib/field/form-field.component.js +1194 -0
  16. package/dist/lib/field/form-field.component.js.map +1 -0
  17. package/dist/lib/form-state.interface.d.ts +2 -0
  18. package/dist/lib/form-state.interface.d.ts.map +1 -1
  19. package/dist/lib/form-state.interface.js.map +1 -1
  20. package/dist/lib/form-state.service.d.ts +14 -0
  21. package/dist/lib/form-state.service.d.ts.map +1 -1
  22. package/dist/lib/form-state.service.js +19 -0
  23. package/dist/lib/form-state.service.js.map +1 -1
  24. package/dist/lib/panel/collapsible-panel.component.d.ts +139 -0
  25. package/dist/lib/panel/collapsible-panel.component.d.ts.map +1 -0
  26. package/dist/lib/panel/collapsible-panel.component.js +468 -0
  27. package/dist/lib/panel/collapsible-panel.component.js.map +1 -0
  28. package/dist/lib/section-loader-component.d.ts.map +1 -1
  29. package/dist/lib/section-loader-component.js +3 -2
  30. package/dist/lib/section-loader-component.js.map +1 -1
  31. package/dist/lib/toolbar/form-toolbar.component.d.ts +170 -0
  32. package/dist/lib/toolbar/form-toolbar.component.d.ts.map +1 -0
  33. package/dist/lib/toolbar/form-toolbar.component.js +830 -0
  34. package/dist/lib/toolbar/form-toolbar.component.js.map +1 -0
  35. package/dist/lib/types/form-events.d.ts +90 -0
  36. package/dist/lib/types/form-events.d.ts.map +1 -0
  37. package/dist/lib/types/form-events.js +45 -0
  38. package/dist/lib/types/form-events.js.map +1 -0
  39. package/dist/lib/types/form-types.d.ts +150 -0
  40. package/dist/lib/types/form-types.d.ts.map +1 -0
  41. package/dist/lib/types/form-types.js +12 -0
  42. package/dist/lib/types/form-types.js.map +1 -0
  43. package/dist/lib/types/navigation-events.d.ts +88 -0
  44. package/dist/lib/types/navigation-events.d.ts.map +1 -0
  45. package/dist/lib/types/navigation-events.js +2 -0
  46. package/dist/lib/types/navigation-events.js.map +1 -0
  47. package/dist/lib/types/toolbar-config.d.ts +66 -0
  48. package/dist/lib/types/toolbar-config.d.ts.map +1 -0
  49. package/dist/lib/types/toolbar-config.js +38 -0
  50. package/dist/lib/types/toolbar-config.js.map +1 -0
  51. package/dist/module.d.ts +28 -20
  52. package/dist/module.d.ts.map +1 -1
  53. package/dist/module.js +47 -74
  54. package/dist/module.js.map +1 -1
  55. package/dist/public-api.d.ts +20 -8
  56. package/dist/public-api.d.ts.map +1 -1
  57. package/dist/public-api.js +24 -9
  58. package/dist/public-api.js.map +1 -1
  59. package/package.json +27 -30
  60. package/README.md +0 -449
  61. package/dist/lib/base-field-component.d.ts +0 -152
  62. package/dist/lib/base-field-component.d.ts.map +0 -1
  63. package/dist/lib/base-field-component.js +0 -667
  64. package/dist/lib/base-field-component.js.map +0 -1
  65. package/dist/lib/base-form-context.d.ts +0 -23
  66. package/dist/lib/base-form-context.d.ts.map +0 -1
  67. package/dist/lib/base-form-context.js +0 -10
  68. package/dist/lib/base-form-context.js.map +0 -1
  69. package/dist/lib/collapsible-panel.component.d.ts +0 -68
  70. package/dist/lib/collapsible-panel.component.d.ts.map +0 -1
  71. package/dist/lib/collapsible-panel.component.js +0 -330
  72. package/dist/lib/collapsible-panel.component.js.map +0 -1
  73. package/dist/lib/form-section-controls.component.d.ts +0 -35
  74. package/dist/lib/form-section-controls.component.d.ts.map +0 -1
  75. package/dist/lib/form-section-controls.component.js +0 -272
  76. package/dist/lib/form-section-controls.component.js.map +0 -1
  77. package/dist/lib/link-field.component.d.ts +0 -62
  78. package/dist/lib/link-field.component.d.ts.map +0 -1
  79. package/dist/lib/link-field.component.js +0 -391
  80. package/dist/lib/link-field.component.js.map +0 -1
@@ -1,73 +1,75 @@
1
- import { Directive, ViewChildren, ViewChild, inject } from '@angular/core';
2
- import { Subject, debounceTime, fromEvent } from 'rxjs';
1
+ import { Directive, ViewChildren, ElementRef, ChangeDetectorRef, Output, EventEmitter, inject } from '@angular/core';
2
+ import { Subject, debounceTime } from 'rxjs';
3
3
  import { EntityInfo, EntityPermissionType, Metadata, LogError, RunView } from '@memberjunction/core';
4
- import { BaseRecordComponent } from './base-record-component';
5
- import { BaseFormSectionInfo } from './base-form-section-info';
6
- import { CollapsiblePanelComponent } from './collapsible-panel.component';
7
- import { NavigationService } from '@memberjunction/ng-shared';
8
- import { MJTabStripComponent } from '@memberjunction/ng-tabstrip';
9
4
  import { MJEventType, MJGlobal } from '@memberjunction/global';
10
5
  import { FormEditingCompleteEvent, BaseFormComponentEventCodes } from '@memberjunction/ng-base-types';
6
+ import { BaseRecordComponent } from './base-record-component';
7
+ import { BaseFormSectionInfo } from './base-form-section-info';
8
+ import { MjCollapsiblePanelComponent } from './panel/collapsible-panel.component';
11
9
  import { FormStateService } from './form-state.service';
12
10
  import * as i0 from "@angular/core";
13
- import * as i1 from "@memberjunction/ng-shared";
14
- import * as i2 from "@angular/router";
15
- const _c0 = ["topArea"];
11
+ /**
12
+ * Abstract base class for all entity record forms in MemberJunction.
13
+ *
14
+ * Generated forms (via CodeGen) and custom forms extend this class.
15
+ * It provides core form logic — section management, validation, pending records,
16
+ * permissions, favorites, related entity helpers — with ZERO Explorer dependencies.
17
+ *
18
+ * Instead of calling Explorer services directly, events are emitted via @Output:
19
+ * - **Navigate**: Form navigation requests (record links, new records, external links)
20
+ * - **Notification**: User-facing notifications (save success, validation errors, etc.)
21
+ * - **RecordSaved**: Emitted after a successful save
22
+ * - **RecordDeleted**: Emitted after a successful delete
23
+ *
24
+ * The host application (e.g. SingleRecordComponent in Explorer) subscribes to
25
+ * these events and maps them to its own services (NavigationService, SharedService, etc.).
26
+ */
16
27
  export class BaseFormComponent extends BaseRecordComponent {
17
- elementRef;
18
- sharedService;
19
- router;
20
- route;
21
- cdr;
22
28
  EditMode = false;
23
- BottomMargin = 10;
24
- TabHeight = '500px';
25
- /**
26
- * This property is automatically updated by the BaseFormComponent to reflect the height of the "Top Area" of the form. The Top Area is defined in a template by having
27
- * a #topArea element anywhere in the form. This property will automatically be updated AfterViewInit to reflect the height of the top area. This is useful for various things
28
- * like setting a pane in a splitter to a specific height based on the top area height.
29
- */
30
- TopAreaHeight = '300px';
31
- GridBottomMargin = 100;
32
29
  FavoriteInitDone = false;
33
30
  isHistoryDialogOpen = false;
34
31
  showDeleteDialog = false;
35
32
  showCreateDialog = false;
36
- splitterLayoutChangeSubject = new Subject();
33
+ /**
34
+ * Height of the top area when a splitter layout is used.
35
+ * Referenced by CodeGen-generated templates for entities with "top area" sections.
36
+ */
37
+ TopAreaHeight = '300px';
38
+ /**
39
+ * Called when the splitter layout changes (for entities with "top area" sections).
40
+ * No-op in the generic version; override in host application if splitter resizing is needed.
41
+ */
42
+ splitterLayoutChange() {
43
+ // No-op — host application can override
44
+ }
37
45
  _pendingRecords = [];
38
- _updatingBrowserUrl = false;
39
- __debug = false;
40
- /** Form state service for persisting section states to User Settings */
46
+ // #region Injected Dependencies (no constructor params)
47
+ elementRef = inject(ElementRef);
48
+ cdr = inject(ChangeDetectorRef);
41
49
  formStateService = inject(FormStateService);
42
- /** Navigation service for URL management */
43
- navigationService = inject(NavigationService);
50
+ // #endregion
51
+ // #region @Output Events
52
+ /** Emitted when navigation is requested (record link, external link, email, new record, etc.) */
53
+ Navigate = new EventEmitter();
54
+ /** Emitted when a user-facing notification should be shown */
55
+ Notification = new EventEmitter();
56
+ /** Emitted after a record is saved successfully */
57
+ RecordSaved = new EventEmitter();
58
+ /** Emitted after a record is deleted successfully */
59
+ RecordDeleted = new EventEmitter();
60
+ /** Emitted when a record save fails */
61
+ RecordSaveFailed = new EventEmitter();
62
+ /** Emitted when a record delete fails */
63
+ RecordDeleteFailed = new EventEmitter();
64
+ /** Emitted when validation fails before save */
65
+ ValidationFailed = new EventEmitter();
66
+ // #endregion
44
67
  /** Subscription to form state changes */
45
68
  formStateSubscription;
46
- constructor(elementRef, sharedService, router, route, cdr) {
47
- super();
48
- this.elementRef = elementRef;
49
- this.sharedService = sharedService;
50
- this.router = router;
51
- this.route = route;
52
- this.cdr = cdr;
53
- this.setupSplitterLayoutDebounce();
54
- }
55
- tabComponent;
56
- tabStrips;
57
69
  collapsiblePanels;
58
- get TabStripComponent() {
59
- return this.tabComponent;
60
- }
61
- /**
62
- * Convenience method to resize application container when required
63
- */
64
- InvokeManualResize(delay) {
65
- this.sharedService.InvokeManualResize(delay);
66
- }
67
70
  async ngOnInit() {
68
71
  if (this.record) {
69
72
  if (!this.record.IsSaved) {
70
- // we have a new record so by definition we are in edit mode
71
73
  this.StartEditMode();
72
74
  }
73
75
  const md = new Metadata();
@@ -77,85 +79,28 @@ export class BaseFormComponent extends BaseRecordComponent {
77
79
  const entityName = this.getEntityName();
78
80
  if (entityName) {
79
81
  await this.formStateService.initializeState(entityName);
80
- // Subscribe to state changes for reactive updates
81
82
  this.formStateSubscription = this.formStateService.getState$(entityName).subscribe(state => {
82
83
  this.showEmptyFields = state.showEmptyFields;
83
84
  this.cdr.markForCheck();
84
85
  });
85
86
  }
86
- // DEBUG ONLY output to console our full record info for debugging
87
- if (this.__debug) {
88
- const start = new Date().getTime();
89
- console.log('Full Record Info: ' + this.record.EntityInfo.Name + ' ' + this.record.PrimaryKey.ToString());
90
- const dataObject = await this.record.GetDataObject({
91
- includeRelatedEntityData: true,
92
- oldValues: false,
93
- omitEmptyStrings: false,
94
- omitNullValues: false,
95
- excludeFields: [],
96
- relatedEntityList: this.record.EntityInfo.RelatedEntities.map(x => {
97
- return {
98
- relatedEntityName: x.RelatedEntity
99
- };
100
- })
101
- });
102
- const end = new Date().getTime();
103
- console.log(dataObject);
104
- console.log('Time to get full record info: ' + (end - start) + 'ms');
105
- }
106
87
  }
107
88
  // Set up debounced filter subscription
108
89
  this.filterSubscription = this.filterSubject
109
90
  .pipe(debounceTime(100))
110
91
  .subscribe(searchTerm => {
111
92
  this.searchFilter = searchTerm;
93
+ // After change detection propagates FormContext to panels and they update
94
+ // their IsVisible state, expand any visible (matching) sections that are collapsed
95
+ if (searchTerm) {
96
+ Promise.resolve().then(() => this.expandMatchingSections());
97
+ }
112
98
  });
113
99
  }
114
- topArea;
115
100
  ngAfterViewInit() {
116
- this.setTabHeight();
117
- //this.route.queryParams doesnt seem to be picking up the query params
118
- // so we're going to get teh tab query param from the URLSearchParams class
119
- const url = window.location.href;
120
- const urlObj = new URL(url);
121
- const tabName = urlObj.searchParams.get('tab');
122
- // only do this if WE didn't invoke the browser URL update
123
- if (tabName && !this._updatingBrowserUrl) {
124
- // Select the proper tab based on the tabName
125
- this.tabComponent?.SelectTabByName(tabName);
126
- }
127
- // now resize after a pause to allow the UI to settle
128
- setTimeout(() => {
129
- this.sharedService.InvokeManualResize();
130
- }, 250);
131
- this.CalcTopAreaHeight();
132
- }
133
- /**
134
- * This method can be called at anytime to re-calculate the height of the top area and set the TopAreaHeight property. This is useful if the top area height changes dynamically
135
- */
136
- CalcTopAreaHeight() {
137
- // calculate the top area height and set it to our TopAreaHeight property, sub-classes can then do whatever they want with that property
138
- if (this.topArea && this.topArea.nativeElement) {
139
- // give it a brief pause to let the UI settle
140
- setTimeout(() => {
141
- let height = this.topArea.nativeElement.offsetHeight;
142
- if (height === 0) {
143
- // if we get here, still probably a timing issue so chekc our immediate parent which might be a splitter pane, if it is, use it's height
144
- const parent = this.topArea.nativeElement.parentElement;
145
- if (parent && parent.offsetHeight > 0 && parent.nodeName === "KENDO-SPLITTER-PANE") {
146
- height = parent.offsetHeight;
147
- }
148
- }
149
- if (height > 0)
150
- this.TopAreaHeight = `${height}px`;
151
- }, 100);
152
- }
101
+ // Subclasses can override for post-view-init logic
153
102
  }
154
- resizeSub = null;
155
103
  ngOnDestroy() {
156
- if (this.resizeSub) {
157
- this.resizeSub.unsubscribe();
158
- }
159
104
  if (this.filterSubscription) {
160
105
  this.filterSubscription.unsubscribe();
161
106
  }
@@ -163,178 +108,31 @@ export class BaseFormComponent extends BaseRecordComponent {
163
108
  this.formStateSubscription.unsubscribe();
164
109
  }
165
110
  }
111
+ // #region Pending Records
166
112
  get PendingRecords() {
167
113
  return this._pendingRecords;
168
114
  }
169
- onTabSelect(e) {
170
- this.sharedService.InvokeManualResize();
171
- // Update the browser URL to add the tab name as a query parameter
172
- // Use NavigationService to properly update query params while respecting app-scoped routes
173
- this._updatingBrowserUrl = true;
174
- this.navigationService.UpdateActiveTabQueryParams({
175
- tab: e.tab?.Name || null
176
- });
177
- this._updatingBrowserUrl = false;
178
- }
179
- StartEditMode() {
180
- this.EditMode = true;
181
- }
182
- EndEditMode() {
183
- this.EditMode = false;
184
- }
185
- handleHistoryDialog() {
186
- this.isHistoryDialogOpen = !this.isHistoryDialogOpen;
187
- }
188
- ShowChanges() {
189
- if (this.record) {
190
- const changes = this.record.GetAll(false, true);
191
- const oldValues = this.record.GetAll(true, true);
192
- console.log('Changes for: ' + this.record.EntityInfo.Name + ' ' + this.record.PrimaryKey.ToString());
193
- console.log(changes);
194
- console.log('Old Values');
195
- console.log(oldValues);
196
- this.sharedService.CreateSimpleNotification('Changes for: ' + this.record.EntityInfo.Name + ' ' + this.record.PrimaryKey.ToString() + '\n\n' + JSON.stringify(changes, null, 2) + '\n\nOld Values\n\n' + JSON.stringify(oldValues, null, 2), 'info', 30000);
197
- }
198
- }
199
- async RemoveFavorite() {
200
- return this.SetFavoriteStatus(false);
201
- }
202
- async MakeFavorite() {
203
- return this.SetFavoriteStatus(true);
204
- }
205
- async SetFavoriteStatus(isFavorite) {
206
- const md = new Metadata();
207
- await md.SetRecordFavoriteStatus(md.CurrentUser.ID, this.record.EntityInfo.Name, this.record.PrimaryKey, isFavorite);
208
- this._isFavorite = isFavorite;
209
- }
210
- _isFavorite = false;
211
- get IsFavorite() {
212
- return this._isFavorite;
213
- }
214
- _userPermissions = [];
215
- CheckUserPermission(type) {
216
- try {
217
- if (this.record) {
218
- const perm = this._userPermissions.find(x => x.permission === type);
219
- if (perm)
220
- return perm.canDo;
221
- else {
222
- const result = this.record.CheckPermissions(type, false);
223
- this._userPermissions.push({ permission: type, canDo: result });
224
- return result;
225
- }
226
- }
227
- else
228
- return false;
229
- }
230
- catch (e) {
231
- LogError(e);
232
- return false;
233
- }
234
- }
235
- get UserCanEdit() {
236
- return this.CheckUserPermission(EntityPermissionType.Update);
237
- }
238
- get UserCanRead() {
239
- return this.CheckUserPermission(EntityPermissionType.Read);
240
- }
241
- get UserCanCreate() {
242
- return this.CheckUserPermission(EntityPermissionType.Create);
243
- }
244
- get UserCanDelete() {
245
- return this.CheckUserPermission(EntityPermissionType.Delete);
246
- }
247
- GridEditMode() {
248
- return this.EditMode ? "Queue" : "None";
249
- }
250
- /**
251
- * Returns true if the tabName specified is the currently displayed tab, otherwise returns false
252
- * @param tabName
253
- * @returns
254
- */
255
- IsCurrentTab(tabName) {
256
- if (this.tabComponent) {
257
- const tab = this.tabComponent.GetTabByName(tabName);
258
- if (tab) {
259
- return tab.index === this.tabComponent.SelectedTabIndex;
115
+ PendingRecordsDirty() {
116
+ this.PopulatePendingRecords();
117
+ const pendingRecords = this.PendingRecords;
118
+ if (pendingRecords && pendingRecords.length > 0) {
119
+ for (const p of pendingRecords) {
120
+ if (p.action === 'delete')
121
+ return true;
122
+ else if (p.entityObject.Dirty)
123
+ return true;
260
124
  }
261
125
  }
262
- // if we get here we are not in a state where we can determine the current tab, so return false
263
126
  return false;
264
127
  }
265
- BuildRelationshipViewParams(item) {
266
- return EntityInfo.BuildRelationshipViewParams(this.record, item); // new helper method in EntityInfo
267
- }
268
- /**
269
- * Builds a RunViewParams object for a related entity based on the relationship between the current entity and the related entity
270
- * @param relatedEntityName - the name of the entity that is related to the current entity where we're building the view params from
271
- * @param relatedEntityJoinField - the name of the foreign key field in the current entity that links to the related entity, only required if there are multiple relationships between the entities
272
- * @returns
273
- */
274
- BuildRelationshipViewParamsByEntityName(relatedEntityName, relatedEntityJoinField) {
275
- const eri = this.GetEntityRelationshipByRelatedEntityName(relatedEntityName, relatedEntityJoinField);
276
- if (eri)
277
- return this.BuildRelationshipViewParams(eri);
278
- else
279
- return {};
280
- }
281
- /**
282
- * Looks up and returns the EntityRelationshipInfo object for the related entity name
283
- * @param relatedEntityName - the name of the related entity to look up the relationship for
284
- * @param relatedEntityJoinField - the name of the foreign key field in the current entity that links to the related entity, only required if there are multiple relationships between the entities
285
- * @returns
286
- */
287
- GetEntityRelationshipByRelatedEntityName(relatedEntityName, relatedEntityJoinField) {
288
- if (this.record) {
289
- const r = this.record;
290
- const ret = r.EntityInfo.RelatedEntities.filter(x => x.RelatedEntity.trim().toLowerCase() === relatedEntityName.trim().toLowerCase());
291
- // now if ret.length > 1, we need to find the one that matches the foreign key field name
292
- if (ret.length > 1 && relatedEntityJoinField) {
293
- const ret2 = ret.find(x => x.RelatedEntityJoinField.trim().toLowerCase() === relatedEntityJoinField.trim().toLowerCase());
294
- if (ret2)
295
- return ret2;
296
- }
297
- else if (ret.length === 1) {
298
- return ret[0];
299
- }
300
- else
301
- return undefined;
302
- }
303
- return undefined;
304
- }
305
- /**
306
- * Builds new record values for a related entity based on the relationship between the current entity and the related entity
307
- * @param relatedEntityName
308
- * @returns
309
- */
310
- NewRecordValues(relatedEntityName) {
311
- const eri = this.GetEntityRelationshipByRelatedEntityName(relatedEntityName);
312
- if (eri)
313
- return this.NewRecordValuesByEntityRelationship(eri);
314
- else
315
- return {};
316
- }
317
- NewRecordValuesByEntityRelationship(item) {
318
- return EntityInfo.BuildRelationshipNewRecordValues(this.record, item); // new helper method in EntityInfo
319
- }
320
- GetRelatedEntityTabDisplayName(relatedEntityName) {
321
- if (this.record) {
322
- const eri = this.record.EntityInfo.RelatedEntities.find(x => x.RelatedEntity === relatedEntityName);
323
- if (eri)
324
- return eri.DisplayName;
325
- }
326
- return relatedEntityName;
327
- }
328
- /**
329
- * Returns whether the current entity has any related entities defined.
330
- * Useful for conditionally showing/hiding related entity sections.
331
- */
332
- get HasRelatedEntities() {
333
- if (this.record) {
334
- const relatedEntities = this.record.EntityInfo.RelatedEntities;
335
- return relatedEntities && relatedEntities.length > 0;
128
+ PopulatePendingRecords() {
129
+ this._pendingRecords = [];
130
+ this.RaiseEvent(BaseFormComponentEventCodes.EDITING_COMPLETE);
131
+ const result = this.RaiseEvent(BaseFormComponentEventCodes.POPULATE_PENDING_RECORDS);
132
+ if (result && result.pendingChanges) {
133
+ for (const p of result.pendingChanges)
134
+ this.PendingRecords.push(p);
336
135
  }
337
- return false;
338
136
  }
339
137
  ValidatePendingRecords() {
340
138
  const results = [];
@@ -344,6 +142,20 @@ export class BaseFormComponent extends BaseRecordComponent {
344
142
  }
345
143
  return results;
346
144
  }
145
+ // #endregion
146
+ // #region Edit Mode
147
+ StartEditMode() {
148
+ this.EditMode = true;
149
+ }
150
+ EndEditMode() {
151
+ this.EditMode = false;
152
+ this.clearValidationState();
153
+ }
154
+ handleHistoryDialog() {
155
+ this.isHistoryDialogOpen = !this.isHistoryDialogOpen;
156
+ }
157
+ // #endregion
158
+ // #region Core Form Operations
347
159
  Validate() {
348
160
  const valResults = this.record.Validate();
349
161
  const pendingValResults = this.ValidatePendingRecords();
@@ -366,109 +178,93 @@ export class BaseFormComponent extends BaseRecordComponent {
366
178
  eventCode: BaseFormComponentEventCodes.BASE_CODE,
367
179
  args: event
368
180
  });
369
- return event; // if the recipients of the event provided any values, we have them now
181
+ return event;
370
182
  }
371
183
  async SaveRecord(StopEditModeAfterSave) {
372
184
  try {
373
185
  try {
374
- // atempt to blur the active element to force any pending changes to be committed
375
186
  document.activeElement.blur();
376
- // notify child components of this component that they must stop editing with an EDITING_COMPLETE message via MJ_global
377
187
  this.RaiseEvent(BaseFormComponentEventCodes.EDITING_COMPLETE);
378
188
  }
379
- catch (e2) {
380
- // ignore
189
+ catch (_e) {
190
+ // ignore blur errors
381
191
  }
382
192
  if (this.record) {
383
- this.PopulatePendingRecords(); // do this before we validate as we must validate pending records too
193
+ this.PopulatePendingRecords();
384
194
  const valResults = this.Validate();
385
195
  if (valResults.Success) {
386
196
  const result = await this.InternalSaveRecord();
387
197
  if (result) {
388
- // we have saved the record, so clear the pending records
389
198
  this._pendingRecords = [];
199
+ this.clearValidationState();
390
200
  if (StopEditModeAfterSave)
391
201
  this.EndEditMode();
392
- this.sharedService.CreateSimpleNotification('Record saved succesfully', 'success', 2500);
202
+ this.Notification.emit({ Message: 'Record saved successfully', Type: 'success', Duration: 2500 });
203
+ this.RecordSaved.emit({
204
+ EntityName: this.record.EntityInfo.Name,
205
+ RecordId: this.record.PrimaryKey.ToString(),
206
+ Result: { Success: true }
207
+ });
393
208
  return true;
394
209
  }
395
210
  else {
396
- this.sharedService.CreateSimpleNotification('Error saving record', 'error', 5000);
211
+ const serverMsg = this.record.LatestResult?.Message || '';
212
+ const errorMsg = serverMsg ? `Save failed: ${serverMsg}` : 'Error saving record';
213
+ this.Notification.emit({ Message: errorMsg, Type: 'error', Duration: 5000 });
214
+ this.RecordSaveFailed.emit({ EntityName: this.record.EntityInfo.Name, ErrorMessage: errorMsg });
397
215
  }
398
216
  }
399
217
  else {
400
- this.sharedService.CreateSimpleNotification('Validation Errors\n' + valResults.Errors.map(x => x.Message).join('\n'), 'warning', 5000);
218
+ // Broadcast validation errors to all fields via FormContext
219
+ this._showValidation = true;
220
+ this._validationErrors = valResults.Errors;
221
+ this.cdr.markForCheck();
222
+ const errorMessages = valResults.Errors.map(x => x.Message);
223
+ this.Notification.emit({
224
+ Message: 'Validation Errors\n' + errorMessages.join('\n'),
225
+ Type: 'warning',
226
+ Duration: 5000
227
+ });
228
+ this.ValidationFailed.emit({ EntityName: this.record.EntityInfo.Name, Errors: errorMessages });
401
229
  }
402
230
  }
403
- LogError("Could not save reocrd: Record not found");
404
- return false; // if we get here, we have failed/validation error/etc
231
+ LogError("Could not save record: Record not found");
232
+ return false;
405
233
  }
406
234
  catch (e) {
407
- this.sharedService.CreateSimpleNotification('Error saving record: ' + e, 'error', 5000);
235
+ const errorMsg = 'Error saving record: ' + e;
236
+ this.Notification.emit({ Message: errorMsg, Type: 'error', Duration: 5000 });
237
+ if (this.record) {
238
+ this.RecordSaveFailed.emit({ EntityName: this.record.EntityInfo.Name, ErrorMessage: errorMsg });
239
+ }
408
240
  return false;
409
241
  }
410
242
  }
411
- async Wait(duration) {
412
- return new Promise(resolve => setTimeout(resolve, duration));
413
- }
414
243
  CancelEdit() {
415
244
  if (this.record) {
416
245
  const r = this.record;
417
246
  if (r.Dirty || this.PendingRecordsDirty()) {
418
- // ask the user to make sure they want to throw out changes
419
- if (!confirm('Are you sure you want to cancel your changes?')) {
420
- return;
421
- }
247
+ // Revert is safe here — the toolbar's discard dialog already confirmed with the user
422
248
  r.Revert();
423
249
  const pendingRecords = this.PendingRecords;
424
250
  if (pendingRecords && pendingRecords.length > 0) {
425
- // we have pending records, so we need to revert them as well
426
251
  pendingRecords.forEach(p => {
427
252
  if (p.action === 'save')
428
- p.entityObject.Revert(); // only do this if we are saving the record, ignore deletes
253
+ p.entityObject.Revert();
429
254
  });
430
255
  }
431
256
  this.RaiseEvent(BaseFormComponentEventCodes.REVERT_PENDING_CHANGES);
432
257
  }
433
- // if we get here we are good to go
434
258
  this.EndEditMode();
435
259
  }
436
260
  }
437
- PendingRecordsDirty() {
438
- this.PopulatePendingRecords();
439
- const pendingRecords = this.PendingRecords;
440
- if (pendingRecords && pendingRecords.length > 0) {
441
- for (const p of pendingRecords) {
442
- if (p.action === 'delete')
443
- return true;
444
- else if (p.entityObject.Dirty)
445
- return true;
446
- }
447
- }
448
- return false;
449
- }
450
- PopulatePendingRecords() {
451
- // this method is called by the parent class at the right time to populate all of the pending records for a transaction
452
- // all we do is talk to all of our child compoennts and get their pending records and xfer them over to the PendingRecords array
453
- // the parent class will then take care of the rest...
454
- this._pendingRecords = []; // wipe out our array first
455
- this.RaiseEvent(BaseFormComponentEventCodes.EDITING_COMPLETE); // first tell the child components to finish up their editing
456
- const result = this.RaiseEvent(BaseFormComponentEventCodes.POPULATE_PENDING_RECORDS);
457
- if (result && result.pendingChanges) {
458
- for (const p of result.pendingChanges)
459
- this.PendingRecords.push(p);
460
- }
461
- }
462
261
  async InternalSaveRecord() {
463
262
  if (this.record) {
464
- // DONT POPULATE PENDING RECORDS AGAIN HERE AS IT WAS DONE before Validate() was called
465
263
  if (this._pendingRecords.length > 0) {
466
- // we need to create a transaction group
467
264
  const md = new Metadata();
468
265
  const tg = await md.CreateTransactionGroup();
469
266
  this.record.TransactionGroup = tg;
470
267
  await this.record.Save();
471
- // now add to the rest of the pending records
472
268
  for (const x of this._pendingRecords) {
473
269
  x.entityObject.TransactionGroup = tg;
474
270
  if (x.action === 'save') {
@@ -478,75 +274,163 @@ export class BaseFormComponent extends BaseRecordComponent {
478
274
  await x.entityObject.Delete();
479
275
  }
480
276
  }
481
- // finally submit the TG
482
277
  return await tg.Submit();
483
278
  }
484
- else
279
+ else {
485
280
  return await this.record.Save();
281
+ }
486
282
  }
487
- else
283
+ else {
284
+ return false;
285
+ }
286
+ }
287
+ async Wait(duration) {
288
+ return new Promise(resolve => setTimeout(resolve, duration));
289
+ }
290
+ // #endregion
291
+ // #region Permissions
292
+ _userPermissions = [];
293
+ CheckUserPermission(type) {
294
+ try {
295
+ if (this.record) {
296
+ const perm = this._userPermissions.find(x => x.permission === type);
297
+ if (perm)
298
+ return perm.canDo;
299
+ else {
300
+ const result = this.record.CheckPermissions(type, false);
301
+ this._userPermissions.push({ permission: type, canDo: result });
302
+ return result;
303
+ }
304
+ }
305
+ else {
306
+ return false;
307
+ }
308
+ }
309
+ catch (e) {
310
+ LogError(e);
488
311
  return false;
312
+ }
313
+ }
314
+ get UserCanEdit() {
315
+ return this.CheckUserPermission(EntityPermissionType.Update);
316
+ }
317
+ get UserCanRead() {
318
+ return this.CheckUserPermission(EntityPermissionType.Read);
319
+ }
320
+ get UserCanCreate() {
321
+ return this.CheckUserPermission(EntityPermissionType.Create);
322
+ }
323
+ get UserCanDelete() {
324
+ return this.CheckUserPermission(EntityPermissionType.Delete);
489
325
  }
490
- GetTabTopPosition() {
491
- if (this.elementRef && this.elementRef.nativeElement) {
492
- const tabs = this.elementRef.nativeElement.getElementsByClassName('k-tabstrip');
493
- if (tabs && tabs.length > 0) {
494
- const tabElement = tabs[0];
495
- const tabRect = tabElement.getBoundingClientRect();
496
- const bodyRect = document.body.getBoundingClientRect();
497
- return tabRect.top - bodyRect.top;
326
+ // #endregion
327
+ // #region Grid Edit Mode
328
+ GridEditMode() {
329
+ return this.EditMode ? "Queue" : "None";
330
+ }
331
+ // #endregion
332
+ // #region Favorites
333
+ _isFavorite = false;
334
+ get IsFavorite() {
335
+ return this._isFavorite;
336
+ }
337
+ async RemoveFavorite() {
338
+ return this.SetFavoriteStatus(false);
339
+ }
340
+ async MakeFavorite() {
341
+ return this.SetFavoriteStatus(true);
342
+ }
343
+ async SetFavoriteStatus(isFavorite) {
344
+ const md = new Metadata();
345
+ await md.SetRecordFavoriteStatus(md.CurrentUser.ID, this.record.EntityInfo.Name, this.record.PrimaryKey, isFavorite);
346
+ this._isFavorite = isFavorite;
347
+ }
348
+ // #endregion
349
+ // #region Related Entity Helpers
350
+ BuildRelationshipViewParams(item) {
351
+ return EntityInfo.BuildRelationshipViewParams(this.record, item);
352
+ }
353
+ BuildRelationshipViewParamsByEntityName(relatedEntityName, relatedEntityJoinField) {
354
+ const eri = this.GetEntityRelationshipByRelatedEntityName(relatedEntityName, relatedEntityJoinField);
355
+ if (eri)
356
+ return this.BuildRelationshipViewParams(eri);
357
+ else
358
+ return {};
359
+ }
360
+ GetEntityRelationshipByRelatedEntityName(relatedEntityName, relatedEntityJoinField) {
361
+ if (this.record) {
362
+ const r = this.record;
363
+ const ret = r.EntityInfo.RelatedEntities.filter(x => x.RelatedEntity.trim().toLowerCase() === relatedEntityName.trim().toLowerCase());
364
+ if (ret.length > 1 && relatedEntityJoinField) {
365
+ const ret2 = ret.find(x => x.RelatedEntityJoinField.trim().toLowerCase() === relatedEntityJoinField.trim().toLowerCase());
366
+ if (ret2)
367
+ return ret2;
368
+ }
369
+ else if (ret.length === 1) {
370
+ return ret[0];
371
+ }
372
+ else {
373
+ return undefined;
498
374
  }
499
375
  }
500
- return 0;
376
+ return undefined;
501
377
  }
502
- _tabMargin = 10;
503
- setTabHeight() {
504
- // Subscribe to the window resize event
505
- this.resizeSub = fromEvent(window, 'resize').pipe(debounceTime(100) // Debounce the resize event to avoid frequent updates
506
- ).subscribe(() => {
507
- // Update the grid height when the window is resized
508
- this.ResizeTab();
509
- });
510
- // Set the initial grid height with a slight delay to allow stuff to get set
511
- setTimeout(() => {
512
- this.ResizeTab();
513
- }, 100);
514
- }
515
- get ContainerObjectHeight() {
516
- return window.innerHeight; // default, can be overriden by subclasses
517
- }
518
- ResizeTab() {
519
- this._tabMargin = this.GetTabTopPosition();
520
- const height = this.ContainerObjectHeight - this._tabMargin - this.BottomMargin;
521
- this.TabHeight = height.toString() + 'px';
522
- this.GridBottomMargin = 40;
523
- }
524
- setupSplitterLayoutDebounce() {
525
- this.splitterLayoutChangeSubject.pipe(debounceTime(300)).subscribe(() => {
526
- this.setTabHeight(); // set the height of the tab first
527
- // then wait through the timeout so the tab height change actually takes effect in the DOM - that's why we have the timeout
528
- setTimeout(() => {
529
- this.sharedService.InvokeManualResize();
530
- }, 100);
531
- });
378
+ NewRecordValues(relatedEntityName) {
379
+ const eri = this.GetEntityRelationshipByRelatedEntityName(relatedEntityName);
380
+ if (eri)
381
+ return this.NewRecordValuesByEntityRelationship(eri);
382
+ else
383
+ return {};
532
384
  }
533
- splitterLayoutChange() {
534
- this.splitterLayoutChangeSubject.next();
385
+ NewRecordValuesByEntityRelationship(item) {
386
+ return EntityInfo.BuildRelationshipNewRecordValues(this.record, item);
387
+ }
388
+ GetRelatedEntityTabDisplayName(relatedEntityName) {
389
+ if (this.record) {
390
+ const eri = this.record.EntityInfo.RelatedEntities.find(x => x.RelatedEntity === relatedEntityName);
391
+ if (eri)
392
+ return eri.DisplayName;
393
+ }
394
+ return relatedEntityName;
395
+ }
396
+ get HasRelatedEntities() {
397
+ if (this.record) {
398
+ const relatedEntities = this.record.EntityInfo.RelatedEntities;
399
+ return relatedEntities && relatedEntities.length > 0;
400
+ }
401
+ return false;
402
+ }
403
+ // #endregion
404
+ // #region Entity Info & Dependencies
405
+ get EntityInfo() {
406
+ try {
407
+ const r = this.record;
408
+ if (r)
409
+ return r.EntityInfo;
410
+ else
411
+ return undefined;
412
+ }
413
+ catch (e) {
414
+ LogError(e);
415
+ return undefined;
416
+ }
535
417
  }
536
418
  async ShowDependencies() {
537
- // for now dump to console
538
419
  const md = new Metadata();
539
420
  const dep = await md.GetRecordDependencies(this.record.EntityInfo.Name, this.record.PrimaryKey);
540
421
  console.log('Dependencies for: ' + this.record.EntityInfo.Name + ' ' + this.record.PrimaryKey.ToString());
541
422
  console.log(dep);
542
- // if (confirm('Do you want to merge records test?') == true) {
543
- // const mergeResult = await md.MergeRecords({
544
- // EntityName: this.record.EntityInfo.Name,
545
- // SurvivingRecordID: this.record.PrimaryKey.Value,
546
- // RecordsToMerge: [4],
547
- // })
548
- // console.log(mergeResult);
549
- // }
423
+ }
424
+ ShowChanges() {
425
+ if (this.record) {
426
+ const changes = this.record.GetAll(false, true);
427
+ const oldValues = this.record.GetAll(true, true);
428
+ const message = 'Changes for: ' + this.record.EntityInfo.Name + ' ' + this.record.PrimaryKey.ToString() +
429
+ '\n\n' + JSON.stringify(changes, null, 2) +
430
+ '\n\nOld Values\n\n' + JSON.stringify(oldValues, null, 2);
431
+ console.log(message);
432
+ this.Notification.emit({ Message: message, Type: 'info', Duration: 30000 });
433
+ }
550
434
  }
551
435
  async GetListsCanAddTo() {
552
436
  if (!this.record) {
@@ -571,59 +455,31 @@ export class BaseFormComponent extends BaseRecordComponent {
571
455
  const dependencies = await md.GetRecordDependencies(this.record.EntityInfo.Name, this.record.PrimaryKey);
572
456
  return dependencies;
573
457
  }
574
- get EntityInfo() {
575
- try {
576
- const r = this.record;
577
- if (r)
578
- return r.EntityInfo;
579
- else
580
- return undefined;
581
- }
582
- catch (e) {
583
- LogError(e);
584
- return undefined;
585
- }
586
- }
458
+ // #endregion
587
459
  // #region Collapsible Section Management
588
- /**
589
- * Array of section information including expanded state and row counts.
590
- * Initialized by subclasses via initSections().
591
- */
592
460
  sections = [];
593
- /**
594
- * Map for fast section lookup by key.
595
- */
596
461
  sectionMap = new Map();
597
- /**
598
- * Current search filter text for filtering sections.
599
- */
600
462
  searchFilter = '';
601
- /**
602
- * Controls whether empty fields should be shown in read-only mode.
603
- * When false (default), empty fields are hidden to reduce visual clutter.
604
- */
605
463
  showEmptyFields = false;
606
- /**
607
- * Returns the current form context containing all form-level state.
608
- * This is a computed property that creates a fresh context object on each access,
609
- * ensuring child components always have the latest values.
610
- */
464
+ /** Whether to show all validation errors on fields (set after save failure) */
465
+ _showValidation = false;
466
+ /** Validation errors from the most recent failed save attempt */
467
+ _validationErrors = [];
611
468
  get formContext() {
612
469
  return {
613
470
  sectionFilter: this.searchFilter,
614
- showEmptyFields: this.showEmptyFields
471
+ showEmptyFields: this.showEmptyFields,
472
+ showValidation: this._showValidation,
473
+ validationErrors: this._validationErrors
615
474
  };
616
475
  }
617
- /**
618
- * Subject for debouncing filter changes.
619
- */
476
+ /** Clears all validation display state (called on save success, cancel, end edit) */
477
+ clearValidationState() {
478
+ this._showValidation = false;
479
+ this._validationErrors = [];
480
+ }
620
481
  filterSubject = new Subject();
621
482
  filterSubscription;
622
- /**
623
- * Initializes the sections array. Called by subclasses in ngOnInit.
624
- * Accepts either BaseFormSectionInfo instances or plain objects that will be converted.
625
- * @param sections Array of section information or plain objects
626
- */
627
483
  initSections(sections) {
628
484
  this.sections = sections.map(s => s instanceof BaseFormSectionInfo
629
485
  ? s
@@ -633,117 +489,93 @@ export class BaseFormComponent extends BaseRecordComponent {
633
489
  this.sectionMap.set(section.sectionKey, section);
634
490
  });
635
491
  }
636
- /**
637
- * Gets the section info by key.
638
- * @param sectionKey The section key
639
- * @returns The section info or undefined
640
- */
641
492
  getSection(sectionKey) {
642
493
  return this.sectionMap.get(sectionKey);
643
494
  }
644
- /**
645
- * Checks if a section is expanded.
646
- * Uses FormStateService for persisted state.
647
- * @param sectionKey The section key
648
- * @param defaultExpanded Optional default value to use when no persisted state exists
649
- * @returns True if expanded, false otherwise
650
- */
651
495
  IsSectionExpanded(sectionKey, defaultExpanded) {
652
496
  const entityName = this.getEntityName();
653
497
  if (entityName) {
654
498
  return this.formStateService.isSectionExpanded(entityName, sectionKey, defaultExpanded);
655
499
  }
656
- // Fallback to in-memory state if no entity name
657
500
  const section = this.sectionMap.get(sectionKey);
658
501
  return section ? section.isExpanded : (defaultExpanded !== undefined ? defaultExpanded : true);
659
502
  }
660
- /**
661
- * Sets the expanded state of a section.
662
- * Persists to User Settings via FormStateService.
663
- * @param sectionKey The section key
664
- * @param isExpanded The new expanded state
665
- */
666
503
  SetSectionExpanded(sectionKey, isExpanded) {
667
504
  const entityName = this.getEntityName();
668
505
  if (entityName) {
669
506
  this.formStateService.setSectionExpanded(entityName, sectionKey, isExpanded);
670
507
  }
671
- // Also update in-memory state for immediate UI response
672
508
  const section = this.sectionMap.get(sectionKey);
673
509
  if (section) {
674
510
  section.isExpanded = isExpanded;
675
511
  }
676
512
  }
677
- /**
678
- * Gets the row count for a section.
679
- * @param sectionKey The section key
680
- * @returns The row count or undefined
681
- */
682
513
  GetSectionRowCount(sectionKey) {
683
514
  const section = this.sectionMap.get(sectionKey);
684
515
  return section?.rowCount;
685
516
  }
686
- /**
687
- * Sets the row count for a section.
688
- * @param sectionKey The section key
689
- * @param rowCount The row count
690
- */
691
517
  SetSectionRowCount(sectionKey, rowCount) {
692
518
  const section = this.sectionMap.get(sectionKey);
693
519
  if (section) {
694
520
  section.rowCount = rowCount;
695
521
  }
696
522
  }
697
- /**
698
- * Toggles the expanded state of a section.
699
- * Persists to User Settings via FormStateService.
700
- * @param sectionKey The section key to toggle
701
- */
523
+ GetSectionPanelHeight(sectionKey) {
524
+ const entityName = this.getEntityName();
525
+ if (entityName) {
526
+ return this.formStateService.getSectionPanelHeight(entityName, sectionKey);
527
+ }
528
+ return undefined;
529
+ }
530
+ SetSectionPanelHeight(sectionKey, height) {
531
+ const entityName = this.getEntityName();
532
+ if (entityName) {
533
+ this.formStateService.setSectionPanelHeight(entityName, sectionKey, height);
534
+ }
535
+ }
702
536
  toggleSection(sectionKey) {
703
537
  const entityName = this.getEntityName();
704
538
  if (entityName) {
705
539
  this.formStateService.toggleSection(entityName, sectionKey);
706
540
  }
707
- // Also update in-memory state
708
541
  const section = this.sectionMap.get(sectionKey);
709
542
  if (section) {
710
543
  section.isExpanded = !section.isExpanded;
711
544
  }
712
545
  }
713
546
  /**
714
- * Expands all sections.
715
- * Persists to User Settings via FormStateService.
547
+ * Expand any sections that are visible (match the current filter) but currently collapsed.
548
+ * Called after filter changes so the user can immediately see matching content.
716
549
  */
550
+ expandMatchingSections() {
551
+ if (!this.collapsiblePanels)
552
+ return;
553
+ this.collapsiblePanels.forEach(panel => {
554
+ if (panel.IsVisible && !panel.Expanded) {
555
+ this.SetSectionExpanded(panel.SectionKey, true);
556
+ }
557
+ });
558
+ }
717
559
  expandAllSections() {
718
560
  const entityName = this.getEntityName();
719
561
  const sectionKeys = this.sections.map(s => s.sectionKey);
720
562
  if (entityName && sectionKeys.length > 0) {
721
563
  this.formStateService.expandAllSections(entityName, sectionKeys);
722
564
  }
723
- // Also update in-memory state
724
565
  this.sections.forEach(section => {
725
566
  section.isExpanded = true;
726
567
  });
727
568
  }
728
- /**
729
- * Collapses all sections.
730
- * Persists to User Settings via FormStateService.
731
- */
732
569
  collapseAllSections() {
733
570
  const entityName = this.getEntityName();
734
571
  const sectionKeys = this.sections.map(s => s.sectionKey);
735
572
  if (entityName && sectionKeys.length > 0) {
736
573
  this.formStateService.collapseAllSections(entityName, sectionKeys);
737
574
  }
738
- // Also update in-memory state
739
575
  this.sections.forEach(section => {
740
576
  section.isExpanded = false;
741
577
  });
742
578
  }
743
- /**
744
- * Gets the count of currently expanded sections.
745
- * @returns Number of expanded sections
746
- */
747
579
  getExpandedCount() {
748
580
  const entityName = this.getEntityName();
749
581
  const sectionKeys = this.sections.map(s => s.sectionKey);
@@ -752,42 +584,21 @@ export class BaseFormComponent extends BaseRecordComponent {
752
584
  }
753
585
  return this.sections.filter(section => section.isExpanded).length;
754
586
  }
755
- /**
756
- * Gets the count of visible sections after filtering.
757
- * @returns Number of sections currently visible (not hidden by search filter)
758
- */
759
587
  getVisibleSectionCount() {
760
588
  if (!this.collapsiblePanels || this.collapsiblePanels.length === 0) {
761
589
  return this.sections.length;
762
590
  }
763
- return this.collapsiblePanels.filter(panel => panel.isVisible).length;
591
+ return this.collapsiblePanels.filter(panel => panel.IsVisible).length;
764
592
  }
765
- /**
766
- * Gets the total count of all sections (regardless of filter).
767
- * @returns Total number of sections
768
- */
769
593
  getTotalSectionCount() {
770
594
  return this.sections.length;
771
595
  }
772
- /**
773
- * Handles filter change events from the section controls.
774
- * Debounces the filter updates by 250ms to avoid excessive re-rendering during typing.
775
- * @param searchTerm The search term to filter sections
776
- */
777
596
  onFilterChange(searchTerm) {
778
597
  this.filterSubject.next(searchTerm);
779
598
  }
780
- /**
781
- * Gets the entity name for the current record (used for state persistence keys).
782
- * @returns Entity name or empty string
783
- */
784
599
  getEntityName() {
785
600
  return this.record?.EntityInfo?.Name || '';
786
601
  }
787
- /**
788
- * Gets the form width mode from persisted state.
789
- * @returns Width mode ('centered' or 'full-width')
790
- */
791
602
  getFormWidthMode() {
792
603
  const entityName = this.getEntityName();
793
604
  if (entityName) {
@@ -795,21 +606,12 @@ export class BaseFormComponent extends BaseRecordComponent {
795
606
  }
796
607
  return 'centered';
797
608
  }
798
- /**
799
- * Sets the form width mode.
800
- * Persists to User Settings via FormStateService.
801
- * @param widthMode The width mode
802
- */
803
609
  setFormWidthMode(widthMode) {
804
610
  const entityName = this.getEntityName();
805
611
  if (entityName) {
806
612
  this.formStateService.setWidthMode(entityName, widthMode);
807
613
  }
808
614
  }
809
- /**
810
- * Toggles form width mode between centered and full-width.
811
- * Persists to User Settings via FormStateService.
812
- */
813
615
  toggleFormWidthMode() {
814
616
  const entityName = this.getEntityName();
815
617
  if (entityName) {
@@ -818,10 +620,6 @@ export class BaseFormComponent extends BaseRecordComponent {
818
620
  }
819
621
  // #endregion
820
622
  // #region Section Ordering
821
- /**
822
- * Gets the current section order. Returns custom order if set, otherwise returns default order.
823
- * @returns Array of section keys in display order
824
- */
825
623
  getSectionOrder() {
826
624
  const entityName = this.getEntityName();
827
625
  if (entityName) {
@@ -830,32 +628,20 @@ export class BaseFormComponent extends BaseRecordComponent {
830
628
  return customOrder;
831
629
  }
832
630
  }
833
- // Return default order (order defined in sections array)
834
631
  return this.sections.map(s => s.sectionKey);
835
632
  }
836
- /**
837
- * Sets a custom section order.
838
- * @param sectionOrder Array of section keys in the desired order
839
- */
840
633
  setSectionOrder(sectionOrder) {
841
634
  const entityName = this.getEntityName();
842
635
  if (entityName) {
843
636
  this.formStateService.setSectionOrder(entityName, sectionOrder);
844
637
  }
845
638
  }
846
- /**
847
- * Resets section order to the default.
848
- */
849
639
  resetSectionOrder() {
850
640
  const entityName = this.getEntityName();
851
641
  if (entityName) {
852
642
  this.formStateService.resetSectionOrder(entityName);
853
643
  }
854
644
  }
855
- /**
856
- * Checks if a custom section order is set.
857
- * @returns True if custom order exists
858
- */
859
645
  hasCustomSectionOrder() {
860
646
  const entityName = this.getEntityName();
861
647
  if (entityName) {
@@ -863,44 +649,98 @@ export class BaseFormComponent extends BaseRecordComponent {
863
649
  }
864
650
  return false;
865
651
  }
866
- /**
867
- * Gets the CSS order value for a section.
868
- * Used by CollapsiblePanelComponent to set the order style property.
869
- * @param sectionKey The section key
870
- * @returns The order value (0-based index)
871
- */
872
652
  getSectionDisplayOrder(sectionKey) {
873
653
  const order = this.getSectionOrder();
874
654
  const index = order.indexOf(sectionKey);
875
- return index >= 0 ? index : this.sections.length; // Put unknown sections at the end
655
+ return index >= 0 ? index : this.sections.length;
656
+ }
657
+ // #endregion
658
+ // #region Form Container Event Handlers
659
+ /**
660
+ * Handles navigation events from the form container and related entity grids.
661
+ * Relays the event upward via the Navigate @Output for the host application to handle.
662
+ * Generated form templates bind this as: (Navigate)="OnFormNavigate($event)"
663
+ */
664
+ OnFormNavigate(event) {
665
+ this.Navigate.emit(event);
876
666
  }
877
- static ɵfac = function BaseFormComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || BaseFormComponent)(i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i1.SharedService), i0.ɵɵdirectiveInject(i2.Router), i0.ɵɵdirectiveInject(i2.ActivatedRoute), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
667
+ /**
668
+ * Handles delete requests from the form container.
669
+ * Emits RecordDeleted on success, RecordDeleteFailed on failure.
670
+ * Generated form templates bind this as: (DeleteRequested)="OnDeleteRequested()"
671
+ */
672
+ async OnDeleteRequested() {
673
+ if (!this.record || !this.record.IsSaved)
674
+ return;
675
+ try {
676
+ const result = await this.record.Delete();
677
+ if (result) {
678
+ this.Notification.emit({ Message: 'Record deleted successfully', Type: 'success', Duration: 2500 });
679
+ this.RecordDeleted.emit({
680
+ EntityName: this.record.EntityInfo.Name,
681
+ RecordId: this.record.PrimaryKey.ToString()
682
+ });
683
+ }
684
+ else {
685
+ const errorMsg = 'Error deleting record';
686
+ this.Notification.emit({ Message: errorMsg, Type: 'error', Duration: 5000 });
687
+ this.RecordDeleteFailed.emit({ EntityName: this.record.EntityInfo.Name, ErrorMessage: errorMsg });
688
+ }
689
+ }
690
+ catch (e) {
691
+ const errorMsg = 'Error deleting record: ' + e;
692
+ this.Notification.emit({ Message: errorMsg, Type: 'error', Duration: 5000 });
693
+ this.RecordDeleteFailed.emit({ EntityName: this.record.EntityInfo.Name, ErrorMessage: errorMsg });
694
+ }
695
+ }
696
+ /**
697
+ * Handles favorite toggle from the form container.
698
+ * Generated form templates bind this as: (FavoriteToggled)="OnFavoriteToggled()"
699
+ */
700
+ async OnFavoriteToggled() {
701
+ await this.SetFavoriteStatus(!this.IsFavorite);
702
+ }
703
+ /**
704
+ * Handles history view requests from the form container.
705
+ * Generated form templates bind this as: (HistoryRequested)="OnHistoryRequested()"
706
+ */
707
+ OnHistoryRequested() {
708
+ this.handleHistoryDialog();
709
+ }
710
+ /**
711
+ * Handles list management requests from the form container.
712
+ * Generated form templates bind this as: (ListManagementRequested)="OnListManagementRequested()"
713
+ */
714
+ OnListManagementRequested() {
715
+ // List management dialog is handled by the host application
716
+ // Subclasses can override this to implement custom list management
717
+ }
718
+ static ɵfac = /*@__PURE__*/ (() => { let ɵBaseFormComponent_BaseFactory; return function BaseFormComponent_Factory(__ngFactoryType__) { return (ɵBaseFormComponent_BaseFactory || (ɵBaseFormComponent_BaseFactory = i0.ɵɵgetInheritedFactory(BaseFormComponent)))(__ngFactoryType__ || BaseFormComponent); }; })();
878
719
  static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: BaseFormComponent, viewQuery: function BaseFormComponent_Query(rf, ctx) { if (rf & 1) {
879
- i0.ɵɵviewQuery(MJTabStripComponent, 5);
880
- i0.ɵɵviewQuery(_c0, 5);
881
- i0.ɵɵviewQuery(MJTabStripComponent, 5);
882
- i0.ɵɵviewQuery(CollapsiblePanelComponent, 5);
720
+ i0.ɵɵviewQuery(MjCollapsiblePanelComponent, 5);
883
721
  } if (rf & 2) {
884
722
  let _t;
885
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.tabComponent = _t.first);
886
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.topArea = _t.first);
887
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.tabStrips = _t);
888
723
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.collapsiblePanels = _t);
889
- } }, features: [i0.ɵɵInheritDefinitionFeature] });
724
+ } }, outputs: { Navigate: "Navigate", Notification: "Notification", RecordSaved: "RecordSaved", RecordDeleted: "RecordDeleted", RecordSaveFailed: "RecordSaveFailed", RecordDeleteFailed: "RecordDeleteFailed", ValidationFailed: "ValidationFailed" }, features: [i0.ɵɵInheritDefinitionFeature] });
890
725
  }
891
726
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BaseFormComponent, [{
892
727
  type: Directive
893
- }], () => [{ type: i0.ElementRef }, { type: i1.SharedService }, { type: i2.Router }, { type: i2.ActivatedRoute }, { type: i0.ChangeDetectorRef }], { tabComponent: [{
894
- type: ViewChild,
895
- args: [MJTabStripComponent, { static: false }]
896
- }], tabStrips: [{
897
- type: ViewChildren,
898
- args: [MJTabStripComponent]
728
+ }], null, { Navigate: [{
729
+ type: Output
730
+ }], Notification: [{
731
+ type: Output
732
+ }], RecordSaved: [{
733
+ type: Output
734
+ }], RecordDeleted: [{
735
+ type: Output
736
+ }], RecordSaveFailed: [{
737
+ type: Output
738
+ }], RecordDeleteFailed: [{
739
+ type: Output
740
+ }], ValidationFailed: [{
741
+ type: Output
899
742
  }], collapsiblePanels: [{
900
743
  type: ViewChildren,
901
- args: [CollapsiblePanelComponent]
902
- }], topArea: [{
903
- type: ViewChild,
904
- args: ['topArea']
744
+ args: [MjCollapsiblePanelComponent]
905
745
  }] }); })();
906
746
  //# sourceMappingURL=base-form-component.js.map