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