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