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