]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
LP1830973 Angular 8 updates
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / perm-group-tree.component.ts
1 import {Component, ViewChild, OnInit} from '@angular/core';
2 import {map} from 'rxjs/operators';
3 import {Tree, TreeNode} from '@eg/share/tree/tree';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {OrgService} from '@eg/core/org.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {StringComponent} from '@eg/share/string/string.component';
10 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
11 import {FmRecordEditorComponent, FmFieldOptions} from '@eg/share/fm-editor/fm-editor.component';
12 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
13 import {PermGroupMapDialogComponent} from './perm-group-map-dialog.component';
14 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
15
16 /** Manage permission groups and group permissions */
17
18 @Component({
19     templateUrl: './perm-group-tree.component.html'
20 })
21
22 export class PermGroupTreeComponent implements OnInit {
23
24     tree: Tree;
25     selected: TreeNode;
26     permissions: IdlObject[];
27     permIdMap: {[id: number]: IdlObject};
28     permEntries: ComboboxEntry[];
29     permMaps: IdlObject[];
30     orgDepths: number[];
31     filterText: string;
32
33     // Have to fetch quite a bit of data for this UI.
34     loading: boolean;
35
36     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
37     @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
38     @ViewChild('successString', { static: true }) successString: StringComponent;
39     @ViewChild('createString', { static: true }) createString: StringComponent;
40     @ViewChild('errorString', { static: true }) errorString: StringComponent;
41     @ViewChild('successMapString', { static: true }) successMapString: StringComponent;
42     @ViewChild('createMapString', { static: true }) createMapString: StringComponent;
43     @ViewChild('errorMapString', { static: true }) errorMapString: StringComponent;
44     @ViewChild('addMappingDialog', { static: true }) addMappingDialog: PermGroupMapDialogComponent;
45     @ViewChild('loadProgress', { static: false }) loadProgress: ProgressInlineComponent;
46
47     constructor(
48         private idl: IdlService,
49         private org: OrgService,
50         private auth: AuthService,
51         private pcrud: PcrudService,
52         private toast: ToastService
53     ) {
54         this.permissions = [];
55         this.permEntries = [];
56         this.permMaps = [];
57         this.permIdMap = {};
58     }
59
60
61     async ngOnInit() {
62         this.loading = true;
63         await this.loadPgtTree();
64         this.loadProgress.increment();
65         await this.loadPermissions();
66         this.loadProgress.increment();
67         await this.loadPermMaps();
68         this.loadProgress.increment();
69         this.setOrgDepths();
70         this.loadProgress.increment();
71         this.loading = false;
72         return Promise.resolve();
73     }
74
75     setOrgDepths() {
76         const depths = this.org.typeList().map(t => Number(t.depth()));
77         const depths2 = [];
78         depths.forEach(d => {
79             if (!depths2.includes(d)) {
80                 depths2.push(d);
81             }
82         });
83         this.orgDepths = depths2.sort();
84     }
85
86     groupPermMaps(): IdlObject[] {
87         if (!this.selected) { return []; }
88
89         let maps = this.inheritedPermissions();
90         maps = maps.concat(
91             this.permMaps.filter(m => +m.grp().id() === +this.selected.id));
92
93         maps = this.applyFilter(maps);
94
95         return maps.sort((m1, m2) =>
96             m1.perm().code() < m2.perm().code() ? -1 : 1);
97     }
98
99     // Chop the filter text into separate words and return true if all
100     // of the words appear somewhere in the combined permission code
101     // plus description text.
102     applyFilter(maps: IdlObject[]) {
103         if (!this.filterText) { return maps; }
104         const parts = this.filterText.toLowerCase().split(' ');
105
106         maps = maps.filter(m => {
107             const target = m.perm().code().toLowerCase()
108                 + ' ' + m.perm().description().toLowerCase();
109
110             for (let i = 0; i < parts.length; i++) {
111                 const part = parts[i];
112                 if (part && target.indexOf(part) === -1) {
113                     return false;
114                 }
115             }
116
117             return true;
118         });
119
120         return maps;
121     }
122
123     async loadPgtTree(): Promise<any> {
124
125         return this.pcrud.search('pgt', {parent: null},
126             {flesh: -1, flesh_fields: {pgt: ['children']}}
127         ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
128     }
129
130     async loadPermissions(): Promise<any> {
131         // ComboboxEntry's for perms uses code() for id instead of
132         // the database ID, because the application_perm field on
133         // "pgt" is text instead of a link.  So the value it expects
134         // is the code, not the ID.
135         return this.pcrud.retrieveAll('ppl', {order_by: {ppl: 'code'}})
136         .pipe(map(perm => {
137             this.loadProgress.increment();
138             this.permissions.push(perm);
139             this.permEntries.push({id: perm.code(), label: perm.code()});
140             this.permissions.forEach(p => this.permIdMap[+p.id()] = p);
141         })).toPromise();
142     }
143
144     async loadPermMaps(): Promise<any> {
145         this.permMaps = [];
146         return this.pcrud.retrieveAll('pgpm', {},
147             {fleshSelectors: true, authoritative: true})
148         .pipe(map(m => {
149             this.loadProgress.increment();
150             this.permMaps.push(m);
151         })).toPromise();
152     }
153
154     fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
155         return {
156             application_perm: {
157                 customValues: this.permEntries
158             }
159         };
160     }
161
162     // Translate the org unt type tree into a structure EgTree can use.
163     ingestPgtTree(pgtTree: IdlObject) {
164
165         const handleNode = (pgtNode: IdlObject): TreeNode => {
166             if (!pgtNode) { return; }
167
168             const treeNode = new TreeNode({
169                 id: pgtNode.id(),
170                 label: pgtNode.name(),
171                 callerData: pgtNode
172             });
173
174             pgtNode.children()
175                 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
176                 .forEach(childNode =>
177                 treeNode.children.push(handleNode(childNode))
178             );
179
180             return treeNode;
181         };
182
183         const rootNode = handleNode(pgtTree);
184         this.tree = new Tree(rootNode);
185     }
186
187     groupById(id: number): IdlObject {
188         return this.tree.findNode(id).callerData;
189     }
190
191     permById(id: number): IdlObject {
192         return this.permIdMap[id];
193     }
194
195     // Returns true if the perm map belongs to an ancestore of the
196     // currently selected group.
197     permIsInherited(m: IdlObject): boolean {
198         // We know the provided map came from this.groupPermMaps() which
199         // only returns maps for the selected group plus parent groups.
200         return m.grp().id() !== this.selected.callerData.id();
201     }
202
203     // List of perm maps that owned by perm groups which are ancestors
204     // of the selected group
205     inheritedPermissions(): IdlObject[] {
206         let maps: IdlObject[] = [];
207
208         let treeNode = this.tree.findNode(this.selected.callerData.parent());
209         while (treeNode) {
210             maps = maps.concat(
211                 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
212             treeNode = this.tree.findNode(treeNode.callerData.parent());
213         }
214
215         return maps;
216     }
217
218
219     nodeClicked($event: any) {
220         this.selected = $event;
221
222         // When the user selects a different perm tree node,
223         // reset the edit state for our perm maps.
224
225         this.permMaps.forEach(m => {
226             m.isnew(false);
227             m.ischanged(false);
228             m.isdeleted(false);
229         });
230     }
231
232     edit() {
233         this.editDialog.mode = 'update';
234         this.editDialog.setRecord(this.selected.callerData);
235
236         this.editDialog.open({size: 'lg'}).subscribe(
237             success => {
238                 this.successString.current().then(str => this.toast.success(str));
239             },
240             failed => {
241                 this.errorString.current()
242                     .then(str => this.toast.danger(str));
243             }
244         );
245     }
246
247     remove() {
248         this.delConfirm.open().subscribe(
249             confirmed => {
250                 if (!confirmed) { return; }
251
252                 this.pcrud.remove(this.selected.callerData)
253                 .subscribe(
254                     ok2 => {},
255                     err => {
256                         this.errorString.current()
257                           .then(str => this.toast.danger(str));
258                     },
259                     ()  => {
260                         // Avoid updating until we know the entire
261                         // pcrud action/transaction completed.
262                         this.tree.removeNode(this.selected);
263                         this.selected = null;
264                         this.successString.current().then(str => this.toast.success(str));
265                     }
266                 );
267             }
268         );
269     }
270
271     addChild() {
272         const parentTreeNode = this.selected;
273         const parentType = parentTreeNode.callerData;
274
275         const newType = this.idl.create('pgt');
276         newType.parent(parentType.id());
277
278         this.editDialog.setRecord(newType);
279         this.editDialog.mode = 'create';
280
281         this.editDialog.open({size: 'lg'}).subscribe(
282             result => { // pgt object
283
284                 // Add our new node to the tree
285                 const newNode = new TreeNode({
286                     id: result.id(),
287                     label: result.name(),
288                     callerData: result
289                 });
290                 parentTreeNode.children.push(newNode);
291                 this.createString.current().then(str => this.toast.success(str));
292             },
293             failed => {
294                 this.errorString.current()
295                     .then(str => this.toast.danger(str));
296             }
297         );
298     }
299
300     changesPending(): boolean {
301         return this.modifiedMaps().length > 0;
302     }
303
304     modifiedMaps(): IdlObject[] {
305         return this.permMaps.filter(
306             m => m.isnew() || m.ischanged() || m.isdeleted()
307         );
308     }
309
310     applyChanges() {
311
312         const maps: IdlObject[] = this.modifiedMaps()
313             .map(m => this.idl.clone(m)); // Clone for de-fleshing
314
315         maps.forEach(m => {
316             m.grp(m.grp().id());
317             m.perm(m.perm().id());
318         });
319
320         this.pcrud.autoApply(maps).subscribe(
321             one => console.debug('Modified one mapping: ', one),
322             err => {
323                 console.error(err);
324                 this.errorMapString.current().then(msg => this.toast.danger(msg));
325             },
326             ()  => {
327                 this.successMapString.current().then(msg => this.toast.success(msg));
328                 this.loadPermMaps();
329             }
330         );
331     }
332
333     openAddDialog() {
334         this.addMappingDialog.open().subscribe(
335             modified => {
336                 this.createMapString.current().then(msg => this.toast.success(msg));
337                 this.loadPermMaps();
338             }
339         );
340     }
341
342     selectGroup(id: number) {
343         const node: TreeNode = this.tree.findNode(id);
344         this.tree.selectNode(node);
345         this.nodeClicked(node);
346     }
347 }
348