LP1821409 Ang admin editor clears fields on new
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / admin-page / admin-page.component.ts
1 import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
2 import {IdlService, IdlObject} from '@eg/core/idl.service';
3 import {GridDataSource} from '@eg/share/grid/grid';
4 import {GridComponent} from '@eg/share/grid/grid.component';
5 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
6 import {ToastService} from '@eg/share/toast/toast.service';
7 import {Pager} from '@eg/share/util/pager';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {OrgService} from '@eg/core/org.service';
10 import {PermService} from '@eg/core/perm.service';
11 import {AuthService} from '@eg/core/auth.service';
12 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
13 import {StringComponent} from '@eg/share/string/string.component';
14
15 /**
16  * General purpose CRUD interface for IDL objects
17  *
18  * Object types using this component must be editable via PCRUD.
19  */
20
21 @Component({
22     selector: 'eg-admin-page',
23     templateUrl: './admin-page.component.html'
24 })
25
26 export class AdminPageComponent implements OnInit {
27
28     @Input() idlClass: string;
29
30     // Default sort field, used when no grid sorting is applied.
31     @Input() sortField: string;
32
33     // Data source may be provided by the caller.  This gives the caller
34     // complete control over the contents of the grid.  If no data source
35     // is provided, a generic one is create which is sufficient for data
36     // that requires no special handling, filtering, etc.
37     @Input() dataSource: GridDataSource;
38
39     // Size of create/edito dialog.  Uses large by default.
40     @Input() dialogSize: 'sm' | 'lg' = 'lg';
41
42     // If an org unit field is specified, an org unit filter
43     // is added to the top of the page.
44     @Input() orgField: string;
45
46     // Disable the auto-matic org unit field filter
47     @Input() disableOrgFilter: boolean;
48
49     // Include objects linking to org units which are ancestors
50     // of the selected org unit.
51     @Input() includeOrgAncestors: boolean;
52
53     // Ditto includeOrgAncestors, but descendants.
54     @Input() includeOrgDescendants: boolean;
55
56     // Optional grid persist key.  This is the part of the key
57     // following eg.grid.
58     @Input() persistKey: string;
59
60     // Optional path component to add to the generated grid persist key,
61     // formatted as (for example):
62     // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
63     @Input() persistKeyPfx: string;
64
65     // Optional comma-separated list of read-only fields
66     @Input() readonlyFields: string;
67
68     @ViewChild('grid') grid: GridComponent;
69     @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
70     @ViewChild('successString') successString: StringComponent;
71     @ViewChild('createString') createString: StringComponent;
72     @ViewChild('createErrString') createErrString: StringComponent;
73     @ViewChild('updateFailedString') updateFailedString: StringComponent;
74     @ViewChild('translator') translator: TranslateComponent;
75
76     idlClassDef: any;
77     pkeyField: string;
78     createNew: () => void;
79     deleteSelected: (rows: IdlObject[]) => void;
80     editSelected: (rows: IdlObject[]) => void;
81
82     // True if any columns on the object support translations
83     translateRowIdx: number;
84     translateFieldIdx: number;
85     translatableFields: string[];
86     translate: () => void;
87
88     contextOrg: IdlObject;
89     orgFieldLabel: string;
90     viewPerms: string;
91     canCreate: boolean;
92
93     constructor(
94         private idl: IdlService,
95         private org: OrgService,
96         private auth: AuthService,
97         private pcrud: PcrudService,
98         private perm: PermService,
99         private toast: ToastService
100     ) {
101         this.translatableFields = [];
102     }
103
104     applyOrgValues() {
105
106         if (this.disableOrgFilter) {
107             this.orgField = null;
108             return;
109         }
110
111         if (!this.orgField) {
112             // If no org unit field is specified, try to find one.
113             // If an object type has multiple org unit fields, the
114             // caller should specify one or disable org unit filter.
115             this.idlClassDef.fields.forEach(field => {
116                 if (field['class'] === 'aou') {
117                     this.orgField = field.name;
118                 }
119             });
120         }
121
122         if (this.orgField) {
123             this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
124             this.contextOrg = this.org.root();
125         }
126     }
127
128     ngOnInit() {
129         this.idlClassDef = this.idl.classes[this.idlClass];
130         this.pkeyField = this.idlClassDef.pkey || 'id';
131
132         this.translatableFields =
133             this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
134
135         if (!this.persistKey) {
136             this.persistKey =
137                 'admin.' +
138                 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
139                 this.idlClassDef.table;
140         }
141
142         // Limit the view org selector to orgs where the user has
143         // permacrud-encoded view permissions.
144         const pc = this.idlClassDef.permacrud;
145         if (pc && pc.retrieve) {
146             this.viewPerms = pc.retrieve.perms;
147         }
148
149         this.checkCreatePerms();
150         this.applyOrgValues();
151
152         // If the caller provides not data source, create a generic one.
153         if (!this.dataSource) {
154             this.initDataSource();
155         }
156
157         // TODO: pass the row activate handler via the grid markup
158         this.grid.onRowActivate.subscribe(
159             (idlThing: IdlObject) => this.showEditDialog(idlThing)
160         );
161
162         this.editSelected = (idlThings: IdlObject[]) => {
163
164             // Edit each IDL thing one at a time
165             const editOneThing = (thing: IdlObject) => {
166                 if (!thing) { return; }
167
168                 this.showEditDialog(thing).then(
169                     () => editOneThing(idlThings.shift()));
170             };
171
172             editOneThing(idlThings.shift());
173         };
174
175         this.createNew = () => {
176             this.editDialog.mode = 'create';
177             // We reuse the same editor for all actions.  Be sure
178             // create action does not try to modify an existing record.
179             this.editDialog.recId = null;
180             this.editDialog.record = null;
181             this.editDialog.open({size: this.dialogSize}).then(
182                 ok => {
183                     this.createString.current()
184                         .then(str => this.toast.success(str));
185                     this.grid.reload();
186                 },
187                 rejection => {
188                     if (!rejection.dismissed) {
189                         this.createErrString.current()
190                             .then(str => this.toast.danger(str));
191                     }
192                 }
193             );
194         };
195
196         this.deleteSelected = (idlThings: IdlObject[]) => {
197             idlThings.forEach(idlThing => idlThing.isdeleted(true));
198             this.pcrud.autoApply(idlThings).subscribe(
199                 val => console.debug('deleted: ' + val),
200                 err => {},
201                 ()  => this.grid.reload()
202             );
203         };
204
205         // Open the field translation dialog.
206         // Link the next/previous actions to cycle through each translatable
207         // field on each row.
208         this.translate = () => {
209             this.translateRowIdx = 0;
210             this.translateFieldIdx = 0;
211             this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
212             this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
213
214             this.translator.nextString = () => {
215
216                 if (this.translateFieldIdx < this.translatableFields.length - 1) {
217                     this.translateFieldIdx++;
218
219                 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
220                     this.translateRowIdx++;
221                     this.translateFieldIdx = 0;
222                 }
223
224                 this.translator.idlObject =
225                     this.dataSource.data[this.translateRowIdx];
226                 this.translator.fieldName =
227                     this.translatableFields[this.translateFieldIdx];
228             };
229
230             this.translator.prevString = () => {
231
232                 if (this.translateFieldIdx > 0) {
233                     this.translateFieldIdx--;
234
235                 } else if (this.translateRowIdx > 0) {
236                     this.translateRowIdx--;
237                     this.translateFieldIdx = 0;
238                 }
239
240                 this.translator.idlObject =
241                     this.dataSource.data[this.translateRowIdx];
242                 this.translator.fieldName =
243                     this.translatableFields[this.translateFieldIdx];
244             };
245
246             this.translator.open({size: 'lg'});
247         };
248     }
249
250     checkCreatePerms() {
251         this.canCreate = false;
252         const pc = this.idlClassDef.permacrud || {};
253         const perms = pc.create ? pc.create.perms : [];
254         if (perms.length === 0) { return; }
255
256         this.perm.hasWorkPermAt(perms, true).then(permMap => {
257             Object.keys(permMap).forEach(key => {
258                 if (permMap[key].length > 0) {
259                     this.canCreate = true;
260                 }
261             });
262         });
263     }
264
265     orgOnChange(org: IdlObject) {
266         this.contextOrg = org;
267         this.grid.reload();
268     }
269
270     initDataSource() {
271         this.dataSource = new GridDataSource();
272
273         this.dataSource.getRows = (pager: Pager, sort: any[]) => {
274             const orderBy: any = {};
275
276             if (sort.length) {
277                 // Sort specified from grid
278                 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
279
280             } else if (this.sortField) {
281                 // Default sort field
282                 orderBy[this.idlClass] = this.sortField;
283             }
284
285             const searchOps = {
286                 offset: pager.offset,
287                 limit: pager.limit,
288                 order_by: orderBy
289             };
290
291             if (this.contextOrg) {
292                 // Filter rows by those linking to the context org and
293                 // optionally ancestor and descendant org units.
294
295                 let orgs = [this.contextOrg.id()];
296
297                 if (this.includeOrgAncestors) {
298                     orgs = this.org.ancestors(this.contextOrg, true);
299                 }
300
301                 if (this.includeOrgDescendants) {
302                     // can result in duplicate workstation org IDs... meh
303                     orgs = orgs.concat(
304                         this.org.descendants(this.contextOrg, true));
305                 }
306
307                 const search = {};
308                 search[this.orgField] = orgs;
309                 return this.pcrud.search(
310                     this.idlClass, search, searchOps, {fleshSelectors: true});
311             }
312
313             // No org filter -- fetch all rows
314             return this.pcrud.retrieveAll(
315                 this.idlClass, searchOps, {fleshSelectors: true});
316         };
317     }
318
319     disableAncestorSelector(): boolean {
320         return this.contextOrg &&
321             this.contextOrg.id() === this.org.root().id();
322     }
323
324     disableDescendantSelector(): boolean {
325         return this.contextOrg && this.contextOrg.children().length === 0;
326     }
327
328     showEditDialog(idlThing: IdlObject) {
329         this.editDialog.mode = 'update';
330         this.editDialog.recId = idlThing[this.pkeyField]();
331         return this.editDialog.open({size: this.dialogSize}).then(
332             ok => {
333                 this.successString.current()
334                     .then(str => this.toast.success(str));
335                 this.grid.reload();
336             },
337             rejection => {
338                 if (!rejection.dismissed) {
339                     this.updateFailedString.current()
340                         .then(str => this.toast.danger(str));
341                 }
342             }
343         );
344     }
345
346 }
347
348