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