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';
15 /** Manage permission groups and group permissions */
18 templateUrl: './perm-group-tree.component.html'
21 export class PermGroupTreeComponent implements OnInit {
25 permissions: IdlObject[];
26 permIdMap: {[id: number]: IdlObject};
27 permEntries: ComboboxEntry[];
28 permMaps: IdlObject[];
32 // Have to fetch quite a bit of data for this UI.
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;
46 private idl: IdlService,
47 private org: OrgService,
48 private auth: AuthService,
49 private pcrud: PcrudService,
50 private toast: ToastService
52 this.permissions = [];
53 this.permEntries = [];
61 await this.loadPgtTree();
62 await this.loadPermissions();
63 await this.loadPermMaps();
66 return Promise.resolve();
70 const depths = this.org.typeList().map(t => Number(t.depth()));
73 if (!depths2.includes(d)) {
77 this.orgDepths = depths2.sort();
80 groupPermMaps(): IdlObject[] {
81 if (!this.selected) { return []; }
83 let maps = this.inheritedPermissions();
85 this.permMaps.filter(m => +m.grp().id() === +this.selected.id));
87 maps = this.applyFilter(maps);
89 return maps.sort((m1, m2) =>
90 m1.perm().code() < m2.perm().code() ? -1 : 1);
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(' ');
100 maps = maps.filter(m => {
101 const target = m.perm().code().toLowerCase()
102 + ' ' + m.perm().description().toLowerCase();
104 for (let i = 0; i < parts.length; i++) {
105 const part = parts[i];
106 if (part && target.indexOf(part) === -1) {
117 async loadPgtTree(): Promise<any> {
119 return this.pcrud.search('pgt', {parent: null},
120 {flesh: -1, flesh_fields: {pgt: ['children']}}
121 ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
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']}})
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);
137 async loadPermMaps(): Promise<any> {
139 return this.pcrud.retrieveAll('pgpm', {},
140 {fleshSelectors: true, authoritative: true})
141 .pipe(map((m => this.permMaps.push(m)))).toPromise();
144 fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
147 customValues: this.permEntries
152 // Translate the org unt type tree into a structure EgTree can use.
153 ingestPgtTree(pgtTree: IdlObject) {
155 const handleNode = (pgtNode: IdlObject): TreeNode => {
156 if (!pgtNode) { return; }
158 const treeNode = new TreeNode({
160 label: pgtNode.name(),
165 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
166 .forEach(childNode =>
167 treeNode.children.push(handleNode(childNode))
173 const rootNode = handleNode(pgtTree);
174 this.tree = new Tree(rootNode);
177 groupById(id: number): IdlObject {
178 return this.tree.findNode(id).callerData;
181 permById(id: number): IdlObject {
182 return this.permIdMap[id];
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();
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[] = [];
198 let treeNode = this.tree.findNode(this.selected.callerData.parent());
201 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
202 treeNode = this.tree.findNode(treeNode.callerData.parent());
209 nodeClicked($event: any) {
210 this.selected = $event;
212 // When the user selects a different perm tree node,
213 // reset the edit state for our perm maps.
215 this.permMaps.forEach(m => {
223 this.editDialog.mode = 'update';
224 this.editDialog.setRecord(this.selected.callerData);
226 this.editDialog.open({size: 'lg'}).subscribe(
228 this.successString.current().then(str => this.toast.success(str));
231 this.errorString.current()
232 .then(str => this.toast.danger(str));
238 this.delConfirm.open().subscribe(
240 if (!confirmed) { return; }
242 this.pcrud.remove(this.selected.callerData)
246 this.errorString.current()
247 .then(str => this.toast.danger(str));
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));
262 const parentTreeNode = this.selected;
263 const parentType = parentTreeNode.callerData;
265 const newType = this.idl.create('pgt');
266 newType.parent(parentType.id());
268 this.editDialog.setRecord(newType);
269 this.editDialog.mode = 'create';
271 this.editDialog.open({size: 'lg'}).subscribe(
272 result => { // pgt object
274 // Add our new node to the tree
275 const newNode = new TreeNode({
277 label: result.name(),
280 parentTreeNode.children.push(newNode);
281 this.createString.current().then(str => this.toast.success(str));
284 this.errorString.current()
285 .then(str => this.toast.danger(str));
290 changesPending(): boolean {
291 return this.modifiedMaps().length > 0;
294 modifiedMaps(): IdlObject[] {
295 return this.permMaps.filter(
296 m => m.isnew() || m.ischanged() || m.isdeleted()
302 const maps: IdlObject[] = this.modifiedMaps()
303 .map(m => this.idl.clone(m)); // Clone for de-fleshing
307 m.perm(m.perm().id());
310 this.pcrud.autoApply(maps).subscribe(
311 one => console.debug('Modified one mapping: ', one),
314 this.errorMapString.current().then(msg => this.toast.danger(msg));
317 this.successMapString.current().then(msg => this.toast.success(msg));
324 this.addMappingDialog.open().subscribe(
326 this.createMapString.current().then(msg => this.toast.success(msg));
332 selectGroup(id: number) {
333 const node: TreeNode = this.tree.findNode(id);
334 this.tree.selectNode(node);
335 this.nodeClicked(node);