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 if (this.loadProgress) {
152 this.loadProgress.increment();
154 this.permMaps.push(m);
158 fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
161 customValues: this.permEntries
166 // Translate the org unt type tree into a structure EgTree can use.
167 ingestPgtTree(pgtTree: IdlObject) {
169 const handleNode = (pgtNode: IdlObject): TreeNode => {
170 if (!pgtNode) { return; }
172 const treeNode = new TreeNode({
174 label: pgtNode.name(),
179 .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
180 .forEach(childNode =>
181 treeNode.children.push(handleNode(childNode))
187 const rootNode = handleNode(pgtTree);
188 this.tree = new Tree(rootNode);
191 groupById(id: number): IdlObject {
192 return this.tree.findNode(id).callerData;
195 permById(id: number): IdlObject {
196 return this.permIdMap[id];
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();
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[] = [];
212 let treeNode = this.tree.findNode(this.selected.callerData.parent());
215 this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
216 treeNode = this.tree.findNode(treeNode.callerData.parent());
223 nodeClicked($event: any) {
224 this.selected = $event;
226 // When the user selects a different perm tree node,
227 // reset the edit state for our perm maps.
229 this.permMaps.forEach(m => {
237 this.editDialog.mode = 'update';
238 this.editDialog.setRecord(this.selected.callerData);
240 this.editDialog.open({size: 'lg'}).subscribe(
242 this.successString.current().then(str => this.toast.success(str));
245 this.errorString.current()
246 .then(str => this.toast.danger(str));
252 this.delConfirm.open().subscribe(
254 if (!confirmed) { return; }
256 this.pcrud.remove(this.selected.callerData)
260 this.errorString.current()
261 .then(str => this.toast.danger(str));
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));
276 const parentTreeNode = this.selected;
277 const parentType = parentTreeNode.callerData;
279 const newType = this.idl.create('pgt');
280 newType.parent(parentType.id());
282 this.editDialog.setRecord(newType);
283 this.editDialog.mode = 'create';
285 this.editDialog.open({size: 'lg'}).subscribe(
286 result => { // pgt object
288 // Add our new node to the tree
289 const newNode = new TreeNode({
291 label: result.name(),
294 parentTreeNode.children.push(newNode);
295 this.createString.current().then(str => this.toast.success(str));
298 this.errorString.current()
299 .then(str => this.toast.danger(str));
304 changesPending(): boolean {
305 return this.modifiedMaps().length > 0;
308 modifiedMaps(): IdlObject[] {
309 return this.permMaps.filter(
310 m => m.isnew() || m.ischanged() || m.isdeleted()
316 const maps: IdlObject[] = this.modifiedMaps()
317 .map(m => this.idl.clone(m)); // Clone for de-fleshing
321 m.perm(m.perm().id());
324 this.pcrud.autoApply(maps).subscribe(
325 one => console.debug('Modified one mapping: ', one),
328 this.errorMapString.current().then(msg => this.toast.danger(msg));
331 this.successMapString.current().then(msg => this.toast.success(msg));
338 this.addMappingDialog.open().subscribe(
340 this.createMapString.current().then(msg => this.toast.success(msg));
346 selectGroup(id: number) {
347 const node: TreeNode = this.tree.findNode(id);
348 this.tree.selectNode(node);
349 this.nodeClicked(node);