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