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';
17 /** Manage permission groups and group permissions */
20 templateUrl: './perm-group-tree.component.html'
23 export class PermGroupTreeComponent implements OnInit {
27 permissions: IdlObject[];
28 permIdMap: {[id: number]: IdlObject};
29 permEntries: ComboboxEntry[];
30 permMaps: IdlObject[];
34 // Have to fetch quite a bit of data for this UI.
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;
50 private idl: IdlService,
51 private org: OrgService,
52 private auth: AuthService,
53 private pcrud: PcrudService,
54 private toast: ToastService
56 this.permissions = [];
57 this.permEntries = [];
65 await this.loadPgtTree();
66 this.loadProgress.increment();
67 await this.loadPermissions();
68 this.loadProgress.increment();
69 await this.loadPermMaps();
70 this.loadProgress.increment();
72 this.loadProgress.increment();
74 return Promise.resolve();
77 onNavChange(evt: NgbNavChangeEvent) {
78 this.permTab = evt.nextId;
82 const depths = this.org.typeList().map(t => Number(t.depth()));
85 if (!depths2.includes(d)) {
89 this.orgDepths = depths2.sort();
92 // Returns maps for this group and ancestors
93 groupPermMaps(): IdlObject[] {
94 if (!this.selected) { return []; }
96 let maps = this.inheritedPermissions();
98 this.permMaps.filter(m => +m.grp().id() === +this.selected.id));
100 maps = this.applyFilter(maps);
102 return maps.sort((m1, m2) =>
103 m1.perm().code() < m2.perm().code() ? -1 : 1);
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(' ');
113 maps = maps.filter(m => {
114 const desc = m.perm().description() || ''; // null-able
117 m.perm().code().toLowerCase() + ' ' + desc.toLowerCase();
119 for (let i = 0; i < parts.length; i++) {
120 const part = parts[i];
121 if (part && target.indexOf(part) === -1) {
132 async loadPgtTree(): Promise<any> {
134 return this.pcrud.search('pgt', {parent: null},
135 {flesh: -1, flesh_fields: {pgt: ['children']}}
136 ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
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'}})
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);
153 async loadPermMaps(): Promise<any> {
155 return this.pcrud.retrieveAll('pgpm', {},
156 {fleshSelectors: true, authoritative: true})
158 if (this.loadProgress) {
159 this.loadProgress.increment();
161 this.permMaps.push(m);
165 fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
168 customValues: this.permEntries
173 // Translate the org unt type tree into a structure EgTree can use.
174 ingestPgtTree(pgtTree: IdlObject) {
176 const handleNode = (pgtNode: IdlObject): TreeNode => {
177 if (!pgtNode) { return; }
179 const treeNode = new TreeNode({
181 label: pgtNode.name(),
186 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
187 .forEach(childNode =>
188 treeNode.children.push(handleNode(childNode))
194 const rootNode = handleNode(pgtTree);
195 this.tree = new Tree(rootNode);
198 groupById(id: number): IdlObject {
199 return this.tree.findNode(id).callerData;
202 permById(id: number): IdlObject {
203 return this.permIdMap[id];
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();
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();
219 if (m.grp().id() === grpId) { // Selected group has the perm.
221 // See if at least one of our ancestors also has the perm.
222 return this.groupPermMaps().filter(mp => {
224 mp.perm().id() === m.perm().id() &&
225 mp.grp().id() !== grpId
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[] = [];
238 let treeNode = this.tree.findNode(this.selected.callerData.parent());
241 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
242 treeNode = this.tree.findNode(treeNode.callerData.parent());
249 nodeClicked($event: any) {
250 this.selected = $event;
252 // When the user selects a different perm tree node,
253 // reset the edit state for our perm maps.
255 this.permMaps.forEach(m => {
263 this.editDialog.mode = 'update';
264 this.editDialog.setRecord(this.selected.callerData);
266 this.editDialog.open({size: 'lg'}).subscribe(
268 this.successString.current().then(str => this.toast.success(str));
270 (failed: unknown) => {
271 this.errorString.current()
272 .then(str => this.toast.danger(str));
278 this.delConfirm.open().subscribe(
280 if (!confirmed) { return; }
282 this.pcrud.remove(this.selected.callerData)
283 // eslint-disable-next-line rxjs/no-nested-subscribe
287 this.errorString.current()
288 .then(str => this.toast.danger(str));
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));
303 const parentTreeNode = this.selected;
304 const parentType = parentTreeNode.callerData;
306 const newType = this.idl.create('pgt');
307 newType.parent(parentType.id());
309 this.editDialog.setRecord(newType);
310 this.editDialog.mode = 'create';
312 this.editDialog.open({size: 'lg'}).subscribe(
313 result => { // pgt object
315 // Add our new node to the tree
316 const newNode = new TreeNode({
318 label: result.name(),
321 parentTreeNode.children.push(newNode);
322 this.createString.current().then(str => this.toast.success(str));
324 (failed: unknown) => {
325 this.errorString.current()
326 .then(str => this.toast.danger(str));
331 changesPending(): boolean {
332 return this.modifiedMaps().length > 0;
335 modifiedMaps(): IdlObject[] {
336 return this.permMaps.filter(
337 m => m.isnew() || m.ischanged() || m.isdeleted()
343 const maps: IdlObject[] = this.modifiedMaps()
344 .map(m => this.idl.clone(m)); // Clone for de-fleshing
348 m.perm(m.perm().id());
351 this.pcrud.autoApply(maps).subscribe(
352 one => console.debug('Modified one mapping: ', one),
355 this.errorMapString.current().then(msg => this.toast.danger(msg));
358 this.successMapString.current().then(msg => this.toast.success(msg));
365 this.addMappingDialog.open({size: 'lg'}).subscribe(
368 this.createMapString.current().then(msg => this.toast.success(msg));
371 this.errorMapString.current().then(msg => this.toast.danger(msg));
377 selectGroup(id: number) {
378 const node: TreeNode = this.tree.findNode(id);
379 this.tree.selectNode(node);
380 this.nodeClicked(node);