@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.
- package/dist/lib/base-form-component.d.ts +103 -215
- package/dist/lib/base-form-component.d.ts.map +1 -1
- package/dist/lib/base-form-component.js +384 -544
- package/dist/lib/base-form-component.js.map +1 -1
- package/dist/lib/container/record-form-container.component.d.ts +165 -0
- package/dist/lib/container/record-form-container.component.d.ts.map +1 -0
- package/dist/lib/container/record-form-container.component.js +534 -0
- package/dist/lib/container/record-form-container.component.js.map +1 -0
- package/dist/lib/explorer-entity-data-grid.component.d.ts +7 -9
- package/dist/lib/explorer-entity-data-grid.component.d.ts.map +1 -1
- package/dist/lib/explorer-entity-data-grid.component.js +15 -16
- package/dist/lib/explorer-entity-data-grid.component.js.map +1 -1
- package/dist/lib/field/form-field.component.d.ts +268 -0
- package/dist/lib/field/form-field.component.d.ts.map +1 -0
- package/dist/lib/field/form-field.component.js +1194 -0
- package/dist/lib/field/form-field.component.js.map +1 -0
- package/dist/lib/form-state.interface.d.ts +2 -0
- package/dist/lib/form-state.interface.d.ts.map +1 -1
- package/dist/lib/form-state.interface.js.map +1 -1
- package/dist/lib/form-state.service.d.ts +14 -0
- package/dist/lib/form-state.service.d.ts.map +1 -1
- package/dist/lib/form-state.service.js +19 -0
- package/dist/lib/form-state.service.js.map +1 -1
- package/dist/lib/panel/collapsible-panel.component.d.ts +139 -0
- package/dist/lib/panel/collapsible-panel.component.d.ts.map +1 -0
- package/dist/lib/panel/collapsible-panel.component.js +468 -0
- package/dist/lib/panel/collapsible-panel.component.js.map +1 -0
- package/dist/lib/section-loader-component.d.ts.map +1 -1
- package/dist/lib/section-loader-component.js +3 -2
- package/dist/lib/section-loader-component.js.map +1 -1
- package/dist/lib/toolbar/form-toolbar.component.d.ts +170 -0
- package/dist/lib/toolbar/form-toolbar.component.d.ts.map +1 -0
- package/dist/lib/toolbar/form-toolbar.component.js +830 -0
- package/dist/lib/toolbar/form-toolbar.component.js.map +1 -0
- package/dist/lib/types/form-events.d.ts +90 -0
- package/dist/lib/types/form-events.d.ts.map +1 -0
- package/dist/lib/types/form-events.js +45 -0
- package/dist/lib/types/form-events.js.map +1 -0
- package/dist/lib/types/form-types.d.ts +150 -0
- package/dist/lib/types/form-types.d.ts.map +1 -0
- package/dist/lib/types/form-types.js +12 -0
- package/dist/lib/types/form-types.js.map +1 -0
- package/dist/lib/types/navigation-events.d.ts +88 -0
- package/dist/lib/types/navigation-events.d.ts.map +1 -0
- package/dist/lib/types/navigation-events.js +2 -0
- package/dist/lib/types/navigation-events.js.map +1 -0
- package/dist/lib/types/toolbar-config.d.ts +66 -0
- package/dist/lib/types/toolbar-config.d.ts.map +1 -0
- package/dist/lib/types/toolbar-config.js +38 -0
- package/dist/lib/types/toolbar-config.js.map +1 -0
- package/dist/module.d.ts +28 -20
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +47 -74
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +20 -8
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +24 -9
- package/dist/public-api.js.map +1 -1
- package/package.json +27 -30
- package/README.md +0 -449
- package/dist/lib/base-field-component.d.ts +0 -152
- package/dist/lib/base-field-component.d.ts.map +0 -1
- package/dist/lib/base-field-component.js +0 -667
- package/dist/lib/base-field-component.js.map +0 -1
- package/dist/lib/base-form-context.d.ts +0 -23
- package/dist/lib/base-form-context.d.ts.map +0 -1
- package/dist/lib/base-form-context.js +0 -10
- package/dist/lib/base-form-context.js.map +0 -1
- package/dist/lib/collapsible-panel.component.d.ts +0 -68
- package/dist/lib/collapsible-panel.component.d.ts.map +0 -1
- package/dist/lib/collapsible-panel.component.js +0 -330
- package/dist/lib/collapsible-panel.component.js.map +0 -1
- package/dist/lib/form-section-controls.component.d.ts +0 -35
- package/dist/lib/form-section-controls.component.d.ts.map +0 -1
- package/dist/lib/form-section-controls.component.js +0 -272
- package/dist/lib/form-section-controls.component.js.map +0 -1
- package/dist/lib/link-field.component.d.ts +0 -62
- package/dist/lib/link-field.component.d.ts.map +0 -1
- package/dist/lib/link-field.component.js +0 -391
- package/dist/lib/link-field.component.js.map +0 -1
|
@@ -1,73 +1,75 @@
|
|
|
1
|
-
import { Directive, ViewChildren,
|
|
2
|
-
import { Subject, debounceTime
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
// #region Injected Dependencies (no constructor params)
|
|
47
|
+
elementRef = inject(ElementRef);
|
|
48
|
+
cdr = inject(ChangeDetectorRef);
|
|
41
49
|
formStateService = inject(FormStateService);
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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;
|
|
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 (
|
|
380
|
-
// ignore
|
|
189
|
+
catch (_e) {
|
|
190
|
+
// ignore blur errors
|
|
381
191
|
}
|
|
382
192
|
if (this.record) {
|
|
383
|
-
this.PopulatePendingRecords();
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
404
|
-
return false;
|
|
231
|
+
LogError("Could not save record: Record not found");
|
|
232
|
+
return false;
|
|
405
233
|
}
|
|
406
234
|
catch (e) {
|
|
407
|
-
|
|
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
|
-
//
|
|
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();
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
376
|
+
return undefined;
|
|
501
377
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
534
|
-
this.
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
*
|
|
715
|
-
*
|
|
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.
|
|
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;
|
|
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
|
-
|
|
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(
|
|
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
|
-
}],
|
|
894
|
-
type:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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: [
|
|
902
|
-
}], topArea: [{
|
|
903
|
-
type: ViewChild,
|
|
904
|
-
args: ['topArea']
|
|
744
|
+
args: [MjCollapsiblePanelComponent]
|
|
905
745
|
}] }); })();
|
|
906
746
|
//# sourceMappingURL=base-form-component.js.map
|