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';
16 /** Manage permission groups and group permissions */
19 templateUrl: './perm-group-tree.component.html'
22 export class PermGroupTreeComponent implements OnInit {
26 permissions: IdlObject[];
27 permIdMap: {[id: number]: IdlObject};
28 permEntries: ComboboxEntry[];
29 permMaps: IdlObject[];
33 // Have to fetch quite a bit of data for this UI.
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;
48 private idl: IdlService,
49 private org: OrgService,
50 private auth: AuthService,
51 private pcrud: PcrudService,
52 private toast: ToastService
54 this.permissions = [];
55 this.permEntries = [];
63 await this.loadPgtTree();
64 this.loadProgress.increment();
65 await this.loadPermissions();
66 this.loadProgress.increment();
67 await this.loadPermMaps();
68 this.loadProgress.increment();
70 this.loadProgress.increment();
72 return Promise.resolve();
76 const depths = this.org.typeList().map(t => Number(t.depth()));
79 if (!depths2.includes(d)) {
83 this.orgDepths = depths2.sort();
86 groupPermMaps(): IdlObject[] {
87 if (!this.selected) { return []; }
89 let maps = this.inheritedPermissions();
91 this.permMaps.filter(m => +m.grp().id() === +this.selected.id));
93 maps = this.applyFilter(maps);
95 return maps.sort((m1, m2) =>
96 m1.perm().code() < m2.perm().code() ? -1 : 1);
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(' ');
106 maps = maps.filter(m => {
107 const desc = m.perm().description() || ''; // null-able
110 m.perm().code().toLowerCase() + ' ' + desc.toLowerCase();
112 for (let i = 0; i < parts.length; i++) {
113 const part = parts[i];
114 if (part && target.indexOf(part) === -1) {
125 async loadPgtTree(): Promise<any> {
127 return this.pcrud.search('pgt', {parent: null},
128 {flesh: -1, flesh_fields: {pgt: ['children']}}
129 ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
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'}})
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);
146 async loadPermMaps(): Promise<any> {
148 return this.pcrud.retrieveAll('pgpm', {},
149 {fleshSelectors: true, authoritative: true})
151 this.loadProgress.increment();
152 this.permMaps.push(m);
156 fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
159 customValues: this.permEntries
164 // Translate the org unt type tree into a structure EgTree can use.
165 ingestPgtTree(pgtTree: IdlObject) {
167 const handleNode = (pgtNode: IdlObject): TreeNode => {
168 if (!pgtNode) { return; }
170 const treeNode = new TreeNode({
172 label: pgtNode.name(),
177 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
178 .forEach(childNode =>
179 treeNode.children.push(handleNode(childNode))
185 const rootNode = handleNode(pgtTree);
186 this.tree = new Tree(rootNode);
189 groupById(id: number): IdlObject {
190 return this.tree.findNode(id).callerData;
193 permById(id: number): IdlObject {
194 return this.permIdMap[id];
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();
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[] = [];
210 let treeNode = this.tree.findNode(this.selected.callerData.parent());
213 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
214 treeNode = this.tree.findNode(treeNode.callerData.parent());
221 nodeClicked($event: any) {
222 this.selected = $event;
224 // When the user selects a different perm tree node,
225 // reset the edit state for our perm maps.
227 this.permMaps.forEach(m => {
235 this.editDialog.mode = 'update';
236 this.editDialog.setRecord(this.selected.callerData);
238 this.editDialog.open({size: 'lg'}).subscribe(
240 this.successString.current().then(str => this.toast.success(str));
243 this.errorString.current()
244 .then(str => this.toast.danger(str));
250 this.delConfirm.open().subscribe(
252 if (!confirmed) { return; }
254 this.pcrud.remove(this.selected.callerData)
258 this.errorString.current()
259 .then(str => this.toast.danger(str));
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));
274 const parentTreeNode = this.selected;
275 const parentType = parentTreeNode.callerData;
277 const newType = this.idl.create('pgt');
278 newType.parent(parentType.id());
280 this.editDialog.setRecord(newType);
281 this.editDialog.mode = 'create';
283 this.editDialog.open({size: 'lg'}).subscribe(
284 result => { // pgt object
286 // Add our new node to the tree
287 const newNode = new TreeNode({
289 label: result.name(),
292 parentTreeNode.children.push(newNode);
293 this.createString.current().then(str => this.toast.success(str));
296 this.errorString.current()
297 .then(str => this.toast.danger(str));
302 changesPending(): boolean {
303 return this.modifiedMaps().length > 0;
306 modifiedMaps(): IdlObject[] {
307 return this.permMaps.filter(
308 m => m.isnew() || m.ischanged() || m.isdeleted()
314 const maps: IdlObject[] = this.modifiedMaps()
315 .map(m => this.idl.clone(m)); // Clone for de-fleshing
319 m.perm(m.perm().id());
322 this.pcrud.autoApply(maps).subscribe(
323 one => console.debug('Modified one mapping: ', one),
326 this.errorMapString.current().then(msg => this.toast.danger(msg));
329 this.successMapString.current().then(msg => this.toast.success(msg));
336 this.addMappingDialog.open().subscribe(
338 this.createMapString.current().then(msg => this.toast.success(msg));
344 selectGroup(id: number) {
345 const node: TreeNode = this.tree.findNode(id);
346 this.tree.selectNode(node);
347 this.nodeClicked(node);