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