1 import {Component, Input, Output, OnInit,
2 EventEmitter, ViewChild, forwardRef} from '@angular/core';
3 import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
4 import {Observable, of} from 'rxjs';
5 import {map} from 'rxjs/operators';
6 import {IdlService, IdlObject} from '@eg/core/idl.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {ComboboxEntry, ComboboxComponent
11 } from '@eg/share/combobox/combobox.component';
13 /* User permission group select comoboxbox.
16 * [(ngModel)]="pgtObject" [useDisplayEntries]="true">
17 * </eg-profile-select>
20 // Use a unicode char for spacing instead of ASCII=32 so the browser
21 // won't collapse the nested display entries down to a single space.
22 const PAD_SPACE = ' '; // U+2007
25 selector: 'eg-profile-select',
26 templateUrl: './profile-select.component.html',
28 provide: NG_VALUE_ACCESSOR,
29 useExisting: forwardRef(() => ProfileSelectComponent),
33 export class ProfileSelectComponent implements ControlValueAccessor, OnInit {
35 // If true, attempt to build the selector from
36 // permission.grp_tree_display_entry's for the current org unit.
37 // If false OR if no permission.grp_tree_display_entry's exist
38 // build the selector from the full permission.grp_tree
39 @Input() useDisplayEntries: boolean;
41 // Emits the selected 'pgt' object or null if the selector is cleared.
42 @Output() profileChange: EventEmitter<IdlObject>;
44 @ViewChild('combobox', {static: false}) cbox: ComboboxComponent;
46 // Set the initial value by ID
47 @Input() initialGroupId: number;
49 @Input() required = false;
51 cboxEntries: ComboboxEntry[] = [];
52 profiles: {[id: number]: IdlObject} = {};
54 // Stub functions required by ControlValueAccessor
55 propagateChange = (_: any) => {};
56 propagateTouch = () => {};
59 private org: OrgService,
60 private idl: IdlService,
61 private auth: AuthService,
62 private pcrud: PcrudService) {
63 this.profileChange = new EventEmitter<IdlObject>();
67 this.collectGroups().then(grps => this.sortGroups(grps))
68 .then(_ => this.fetchInitialGroup())
69 .then(_ => this.cbox.selectedId = this.initialGroupId);
72 // If the initial group is not included in our set of groups because
73 // we are using a custom display tree, fetch the group so we can
74 // add it to our tree.
75 fetchInitialGroup(): Promise<any> {
76 if (!this.initialGroupId || this.profiles[this.initialGroupId]) {
77 return Promise.resolve();
80 return this.pcrud.retrieve('pgt', this.initialGroupId).toPromise()
82 this.profiles[grp.id()] = grp;
84 this.cboxEntries.push(
85 {id: grp.id(), label: this.grpLabel([], grp)});
90 collectGroups(): Promise<IdlObject[]> {
92 if (!this.useDisplayEntries) {
93 return this.fetchPgt();
96 return this.pcrud.search('pgtde',
97 {org: this.org.ancestors(this.auth.user().ws_ou(), true)},
98 {flesh: 1, flesh_fields: {'pgtde': ['grp', 'children']}, 'order_by':{'pgtde':'position desc'}},
101 ).toPromise().then(groups => {
103 if (groups.length === 0) { return this.fetchPgt(); }
105 // In the query above, we fetch display entries for our org
106 // unit plus ancestors. However, we only want to use one
107 // collection of display entries, those owned at our org
108 // unit or our closest ancestor.
109 let closestOrg = this.org.get(groups[0].org());
110 groups.forEach(g => {
111 const org = this.org.get(g.org());
112 if (closestOrg.ou_type().depth() < org.ou_type().depth()) {
117 groups = groups.filter(g => g.org() === closestOrg.id());
119 // Translate the display entries into a 'pgt' tree
121 const tree: IdlObject[] = [];
123 groups.forEach(display => {
124 const grp = display.grp();
125 const displayParent = groups.filter(g => g.id() === display.parent())[0];
126 grp.parent(displayParent ? displayParent.grp().id() : null);
134 fetchPgt(): Promise<IdlObject[]> {
135 return this.pcrud.retrieveAll('pgt', {}, {atomic: true}).toPromise();
138 grpLabel(groups: IdlObject[], grp: IdlObject): string {
143 const pid = tmp.parent();
144 if (!pid) { break; } // top of the tree
146 // Should always produce a value unless a perm group
147 // display tree is poorly structured.
148 tmp = groups.filter(g => ((g._display) ? g._display.id() : g.id()) === pid)[0];
154 return PAD_SPACE.repeat(depth) + grp.name();
157 sortGroups(groups: IdlObject[]) {
159 // When using display entries, there can be multiple groups
162 groups.forEach(grp => {
163 if (grp.parent() === null) {
164 this.sortOneGroup(groups, grp);
169 sortOneGroup(groups: IdlObject[], grp: IdlObject) {
172 grp = groups.filter(g => g.parent() === null)[0];
175 this.profiles[grp.id()] = grp;
176 this.cboxEntries.push(
177 {id: grp.id(), label: this.grpLabel(groups, grp)});
180 .filter(g => g.parent() === grp.id())
183 return a._display.position() < b._display.position() ? -1 : 1;
185 return a.name() < b.name() ? -1 : 1;
188 .forEach(child => this.sortOneGroup(groups, child));
191 writeValue(pgt: IdlObject) {
192 const id = pgt ? pgt.id() : null;
194 this.cbox.selectedId = id;
196 // Will propagate to cbox after its instantiated.
197 this.initialGroupId = id;
201 registerOnChange(fn) {
202 this.propagateChange = fn;
205 registerOnTouched(fn) {
206 this.propagateTouch = fn;
209 propagateCboxChange(entry: ComboboxEntry) {
211 const grp = this.profiles[entry.id];
212 this.propagateChange(grp);
213 this.profileChange.emit(grp);
215 this.profileChange.emit(null);
216 this.propagateChange(null);