]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
LP1891355 Perm group refresh after changes
[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 desc = m.perm().description() || ''; // null-able
108
109             const target =
110                 m.perm().code().toLowerCase() + ' ' + desc.toLowerCase();
111
112             for (let i = 0; i < parts.length; i++) {
113                 const part = parts[i];
114                 if (part && target.indexOf(part) === -1) {
115                     return false;
116                 }
117             }
118
119             return true;
120         });
121
122         return maps;
123     }
124
125     async loadPgtTree(): Promise<any> {
126
127         return this.pcrud.search('pgt', {parent: null},
128             {flesh: -1, flesh_fields: {pgt: ['children']}}
129         ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
130     }
131
132     async loadPermissions(): Promise<any> {
133         // ComboboxEntry's for perms uses code() for id instead of
134         // the database ID, because the application_perm field on
135         // "pgt" is text instead of a link.  So the value it expects
136         // is the code, not the ID.
137         return this.pcrud.retrieveAll('ppl', {order_by: {ppl: 'code'}})
138         .pipe(map(perm => {
139             this.loadProgress.increment();
140             this.permissions.push(perm);
141             this.permEntries.push({id: perm.code(), label: perm.code()});
142             this.permissions.forEach(p => this.permIdMap[+p.id()] = p);
143         })).toPromise();
144     }
145
146     async loadPermMaps(): Promise<any> {
147         this.permMaps = [];
148         return this.pcrud.retrieveAll('pgpm', {},
149             {fleshSelectors: true, authoritative: true})
150         .pipe(map(m => {
151             if (this.loadProgress) {
152                 this.loadProgress.increment();
153             }
154             this.permMaps.push(m);
155         })).toPromise();
156     }
157
158     fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
159         return {
160             application_perm: {
161                 customValues: this.permEntries
162             }
163         };
164     }
165
166     // Translate the org unt type tree into a structure EgTree can use.
167     ingestPgtTree(pgtTree: IdlObject) {
168
169         const handleNode = (pgtNode: IdlObject): TreeNode => {
170             if (!pgtNode) { return; }
171
172             const treeNode = new TreeNode({
173                 id: pgtNode.id(),
174                 label: pgtNode.name(),
175                 callerData: pgtNode
176             });
177
178             pgtNode.children()
179                 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
180                 .forEach(childNode =>
181                 treeNode.children.push(handleNode(childNode))
182             );
183
184             return treeNode;
185         };
186
187         const rootNode = handleNode(pgtTree);
188         this.tree = new Tree(rootNode);
189     }
190
191     groupById(id: number): IdlObject {
192         return this.tree.findNode(id).callerData;
193     }
194
195     permById(id: number): IdlObject {
196         return this.permIdMap[id];
197     }
198
199     // Returns true if the perm map belongs to an ancestore of the
200     // currently selected group.
201     permIsInherited(m: IdlObject): boolean {
202         // We know the provided map came from this.groupPermMaps() which
203         // only returns maps for the selected group plus parent groups.
204         return m.grp().id() !== this.selected.callerData.id();
205     }
206
207     // List of perm maps that owned by perm groups which are ancestors
208     // of the selected group
209     inheritedPermissions(): IdlObject[] {
210         let maps: IdlObject[] = [];
211
212         let treeNode = this.tree.findNode(this.selected.callerData.parent());
213         while (treeNode) {
214             maps = maps.concat(
215                 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
216             treeNode = this.tree.findNode(treeNode.callerData.parent());
217         }
218
219         return maps;
220     }
221
222
223     nodeClicked($event: any) {
224         this.selected = $event;
225
226         // When the user selects a different perm tree node,
227         // reset the edit state for our perm maps.
228
229         this.permMaps.forEach(m => {
230             m.isnew(false);
231             m.ischanged(false);
232             m.isdeleted(false);
233         });
234     }
235
236     edit() {
237         this.editDialog.mode = 'update';
238         this.editDialog.setRecord(this.selected.callerData);
239
240         this.editDialog.open({size: 'lg'}).subscribe(
241             success => {
242                 this.successString.current().then(str => this.toast.success(str));
243             },
244             failed => {
245                 this.errorString.current()
246                     .then(str => this.toast.danger(str));
247             }
248         );
249     }
250
251     remove() {
252         this.delConfirm.open().subscribe(
253             confirmed => {
254                 if (!confirmed) { return; }
255
256                 this.pcrud.remove(this.selected.callerData)
257                 .subscribe(
258                     ok2 => {},
259                     err => {
260                         this.errorString.current()
261                           .then(str => this.toast.danger(str));
262                     },
263                     ()  => {
264                         // Avoid updating until we know the entire
265                         // pcrud action/transaction completed.
266                         this.tree.removeNode(this.selected);
267                         this.selected = null;
268                         this.successString.current().then(str => this.toast.success(str));
269                     }
270                 );
271             }
272         );
273     }
274
275     addChild() {
276         const parentTreeNode = this.selected;
277         const parentType = parentTreeNode.callerData;
278
279         const newType = this.idl.create('pgt');
280         newType.parent(parentType.id());
281
282         this.editDialog.setRecord(newType);
283         this.editDialog.mode = 'create';
284
285         this.editDialog.open({size: 'lg'}).subscribe(
286             result => { // pgt object
287
288                 // Add our new node to the tree
289                 const newNode = new TreeNode({
290                     id: result.id(),
291                     label: result.name(),
292                     callerData: result
293                 });
294                 parentTreeNode.children.push(newNode);
295                 this.createString.current().then(str => this.toast.success(str));
296             },
297             failed => {
298                 this.errorString.current()
299                     .then(str => this.toast.danger(str));
300             }
301         );
302     }
303
304     changesPending(): boolean {
305         return this.modifiedMaps().length > 0;
306     }
307
308     modifiedMaps(): IdlObject[] {
309         return this.permMaps.filter(
310             m => m.isnew() || m.ischanged() || m.isdeleted()
311         );
312     }
313
314     applyChanges() {
315
316         const maps: IdlObject[] = this.modifiedMaps()
317             .map(m => this.idl.clone(m)); // Clone for de-fleshing
318
319         maps.forEach(m => {
320             m.grp(m.grp().id());
321             m.perm(m.perm().id());
322         });
323
324         this.pcrud.autoApply(maps).subscribe(
325             one => console.debug('Modified one mapping: ', one),
326             err => {
327                 console.error(err);
328                 this.errorMapString.current().then(msg => this.toast.danger(msg));
329             },
330             ()  => {
331                 this.successMapString.current().then(msg => this.toast.success(msg));
332                 this.loadPermMaps();
333             }
334         );
335     }
336
337     openAddDialog() {
338         this.addMappingDialog.open().subscribe(
339             modified => {
340                 this.createMapString.current().then(msg => this.toast.success(msg));
341                 this.loadPermMaps();
342             }
343         );
344     }
345
346     selectGroup(id: number) {
347         const node: TreeNode = this.tree.findNode(id);
348         this.tree.selectNode(node);
349         this.nodeClicked(node);
350     }
351 }
352