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