1 import {Component, OnInit, Input,
2 Output, EventEmitter, TemplateRef} from '@angular/core';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {AuthService} from '@eg/core/auth.service';
5 import {PcrudService} from '@eg/core/pcrud.service';
6 import {DialogComponent} from '@eg/share/dialog/dialog.component';
7 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
9 interface CustomFieldTemplate {
10 template: TemplateRef<any>;
12 // Allow the caller to pass in a free-form context blob to
13 // be addedto the caller's custom template context, along
14 // with our stock context.
15 context?: {[fields: string]: any};
18 interface CustomFieldContext {
19 // Current create/edit/view record
22 // IDL field definition blob
25 // additional context values passed via CustomFieldTemplate
26 [fields: string]: any;
30 selector: 'eg-fm-record-editor',
31 templateUrl: './fm-editor.component.html',
32 /* align checkboxes when not using class="form-check" */
33 styles: ['input[type="checkbox"] {margin-left: 0px;}']
35 export class FmRecordEditorComponent
36 extends DialogComponent implements OnInit {
38 // IDL class hint (e.g. "aou")
39 @Input() idlClass: string;
41 // mode: 'create' for creating a new record,
42 // 'update' for editing an existing record
43 // 'view' for viewing an existing record without editing
44 mode: 'create' | 'update' | 'view' = 'create';
46 // IDL record we are editing
47 // TODO: allow this to be update in real time by the caller?
50 // Permissions extracted from the permacrud defs in the IDL
51 // for the current IDL class
52 modePerms: {[mode: string]: string};
54 @Input() customFieldTemplates:
55 {[fieldName: string]: CustomFieldTemplate} = {};
57 // list of fields that should not be displayed
58 @Input() hiddenFieldsList: string[] = [];
59 @Input() hiddenFields: string; // comma-separated string version
61 // list of fields that should always be read-only
62 @Input() readonlyFieldsList: string[] = [];
63 @Input() readonlyFields: string; // comma-separated string version
65 // list of required fields; this supplements what the IDL considers
67 @Input() requiredFieldsList: string[] = [];
68 @Input() requiredFields: string; // comma-separated string version
70 // list of org_unit fields where a default value may be applied by
71 // the org-select if no value is present.
72 @Input() orgDefaultAllowedList: string[] = [];
73 @Input() orgDefaultAllowed: string; // comma-separated string version
75 // hash, keyed by field name, of functions to invoke to check
76 // whether a field is required. Each callback is passed the field
77 // name and the record and should return a boolean value. This
78 // supports cases where whether a field is required or not depends
79 // on the current value of another field.
80 @Input() isRequiredOverride:
81 {[field: string]: (field: string, record: IdlObject) => boolean};
83 // IDL record display label. Defaults to the IDL label.
84 @Input() recordLabel: string;
86 // Emit the modified object when the save action completes.
87 @Output() onSave$ = new EventEmitter<IdlObject>();
89 // Emit the original object when the save action is canceled.
90 @Output() onCancel$ = new EventEmitter<IdlObject>();
92 // Emit an error message when the save action fails.
93 @Output() onError$ = new EventEmitter<string>();
95 // IDL info for the the selected IDL class
98 // Can we edit the primary key?
99 pkeyIsEditable = false;
101 // List of IDL field definitions. This is a subset of the full
102 // list of fields on the IDL, since some are hidden, virtual, etc.
105 // DOM id prefix to prevent id collisions.
108 @Input() editMode(mode: 'create' | 'update' | 'view') {
112 // Record ID to view/update. Value is dynamic. Records are not
113 // fetched until .open() is called.
114 @Input() set recordId(id: any) {
115 if (id) { this.recId = id; }
119 private modal: NgbModal, // required for passing to parent
120 private idl: IdlService,
121 private auth: AuthService,
122 private pcrud: PcrudService) {
126 // Avoid fetching data on init since that may lead to unnecessary
129 this.listifyInputs();
130 this.idlDef = this.idl.classes[this.idlClass];
131 this.recordLabel = this.idlDef.label;
133 // Add some randomness to the generated DOM IDs to ensure against clobbering
134 this.idPrefix = 'fm-editor-' + Math.floor(Math.random() * 100000);
137 // Opening dialog, fetch data.
138 open(options?: NgbModalOptions): Promise<any> {
139 return this.initRecord().then(
140 ok => super.open(options),
141 err => console.warn(`Error fetching FM data: ${err}`)
145 // Translate comma-separated string versions of various inputs
147 private listifyInputs() {
148 if (this.hiddenFields) {
149 this.hiddenFieldsList = this.hiddenFields.split(/,/);
151 if (this.readonlyFields) {
152 this.readonlyFieldsList = this.readonlyFields.split(/,/);
154 if (this.requiredFields) {
155 this.requiredFieldsList = this.requiredFields.split(/,/);
157 if (this.orgDefaultAllowed) {
158 this.orgDefaultAllowedList = this.orgDefaultAllowed.split(/,/);
162 private initRecord(): Promise<any> {
164 const pc = this.idlDef.permacrud || {};
166 view: pc.retrieve ? pc.retrieve.perms : [],
167 create: pc.create ? pc.create.perms : [],
168 update: pc.update ? pc.update.perms : [],
171 if (this.mode === 'update' || this.mode === 'view') {
172 return this.pcrud.retrieve(this.idlClass, this.recId)
173 .toPromise().then(rec => {
176 return Promise.reject(`No '${this.idlClass}'
177 record found with id ${this.recId}`);
181 this.convertDatatypesToJs();
182 return this.getFieldList();
186 // create a new record from scratch or from a stub record
187 // provided by the caller.
188 this.pkeyIsEditable = !('pkey_sequence' in this.idlDef);
190 this.record = this.idl.create(this.idlClass);
192 return this.getFieldList();
195 // Modifies the FM record in place, replacing IDL-compatible values
196 // with native JS values.
197 private convertDatatypesToJs() {
198 this.idlDef.fields.forEach(field => {
199 if (field.datatype === 'bool') {
200 if (this.record[field.name]() === 't') {
201 this.record[field.name](true);
202 } else if (this.record[field.name]() === 'f') {
203 this.record[field.name](false);
209 // Modifies the provided FM record in place, replacing JS values
210 // with IDL-compatible values.
211 convertDatatypesToIdl(rec: IdlObject) {
212 const fields = this.idlDef.fields;
213 fields.forEach(field => {
214 if (field.datatype === 'bool') {
215 if (rec[field.name]() === true) {
216 rec[field.name]('t');
217 // } else if (rec[field.name]() === false) {
218 } else { // TODO: some bools can be NULL
219 rec[field.name]('f');
221 } else if (field.datatype === 'org_unit') {
222 const org = rec[field.name]();
223 if (org && typeof org === 'object') {
224 rec[field.name](org.id());
231 private flattenLinkedValues(cls: string, list: IdlObject[]): any[] {
232 const idField = this.idl.classes[cls].pkey;
234 this.idl.classes[cls].field_map[idField].selector || idField;
236 return list.map(item => {
237 return {id: item[idField](), name: item[selector]()};
241 private getFieldList(): Promise<any> {
243 this.fields = this.idlDef.fields.filter(f =>
244 !f.virtual && !this.hiddenFieldsList.includes(f.name)
249 this.fields.forEach(field => {
250 field.readOnly = this.mode === 'view'
251 || this.readonlyFieldsList.includes(field.name);
253 if (this.isRequiredOverride &&
254 field.name in this.isRequiredOverride) {
255 field.isRequired = () => {
256 return this.isRequiredOverride[field.name](field.name, this.record);
259 field.isRequired = () => {
260 return field.required ||
261 this.requiredFieldsList.includes(field.name);
265 if (field.datatype === 'link' && field.readOnly) {
267 // no need to fetch all possible values for read-only fields
268 const idToFetch = this.record[field.name]();
272 // If the linked class defines a selector field, fetch the
273 // linked data so we can display the data within the selector
274 // field. Otherwise, avoid the network lookup and let the
275 // bare value (usually an ID) be displayed.
277 this.idl.getLinkSelector(this.idlClass, field.name);
279 if (selector && selector !== field.name) {
281 this.pcrud.retrieve(field.class, this.record[field.name]())
282 .toPromise().then(list => {
284 this.flattenLinkedValues(field.class, Array(list));
288 // No selector, display the raw id/key value.
289 field.linkedValues = [{id: idToFetch, name: idToFetch}];
292 } else if (field.datatype === 'link') {
294 this.pcrud.retrieveAll(field.class, {}, {atomic : true})
295 .toPromise().then(list => {
297 this.flattenLinkedValues(field.class, list);
300 } else if (field.datatype === 'org_unit') {
301 field.orgDefaultAllowed =
302 this.orgDefaultAllowedList.includes(field.name);
305 if (this.customFieldTemplates[field.name]) {
306 field.template = this.customFieldTemplates[field.name].template;
307 field.context = this.customFieldTemplates[field.name].context;
312 // Wait for all network calls to complete
313 return Promise.all(promises);
316 // Returns a context object to be inserted into a custom
318 customTemplateFieldContext(fieldDef: any): CustomFieldContext {
319 return Object.assign(
320 { record : this.record,
321 field: fieldDef // from this.fields
322 }, fieldDef.context || {}
327 const recToSave = this.idl.clone(this.record);
328 this.convertDatatypesToIdl(recToSave);
329 this.pcrud[this.mode]([recToSave]).toPromise().then(
330 result => this.close(result),
331 error => this.dismiss(error)
336 this.dismiss('canceled');