1 /** TODO PORT ME TO <eg-combobox> */
2 import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
3 import {Observable} from 'rxjs/Observable';
4 import {map} from 'rxjs/operators/map';
5 import {mapTo} from 'rxjs/operators/mapTo';
6 import {debounceTime} from 'rxjs/operators/debounceTime';
7 import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
8 import {merge} from 'rxjs/operators/merge';
9 import {filter} from 'rxjs/operators/filter';
10 import {Subject} from 'rxjs/Subject';
11 import {AuthService} from '@eg/core/auth.service';
12 import {StoreService} from '@eg/core/store.service';
13 import {OrgService} from '@eg/core/org.service';
14 import {IdlObject} from '@eg/core/idl.service';
15 import {PermService} from '@eg/core/perm.service';
16 import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
18 // Use a unicode char for spacing instead of ASCII=32 so the browser
19 // won't collapse the nested display entries down to a single space.
20 const PAD_SPACE = ' '; // U+2007
22 interface OrgDisplay {
29 selector: 'eg-org-select',
30 templateUrl: './org-select.component.html'
32 export class OrgSelectComponent implements OnInit {
35 hidden: number[] = [];
36 disabled: number[] = [];
37 click$ = new Subject<string>();
40 @ViewChild('instance') instance: NgbTypeahead;
42 // Placeholder text for selector input
43 @Input() placeholder = '';
44 @Input() stickySetting: string;
46 // ID to display in the DOM for this selector
49 // Org unit field displayed in the selector
50 @Input() displayField = 'shortname';
52 // Apply a default org unit value when none is set.
53 // First tries workstation org unit, then user home org unit.
54 // An onChange event WILL be generated when a default is applied.
55 @Input() applyDefault = false;
57 // List of org unit IDs to exclude from the selector
58 @Input() set hideOrgs(ids: number[]) {
59 if (ids) { this.hidden = ids; }
62 // List of org unit IDs to disable in the selector
63 @Input() set disableOrgs(ids: number[]) {
64 if (ids) { this.disabled = ids; }
67 // Apply an org unit value at load time.
68 // This will NOT result in an onChange event.
69 @Input() set initialOrg(org: IdlObject) {
70 if (org) { this.startOrg = org; }
73 // Apply an org unit value by ID at load time.
74 // This will NOT result in an onChange event.
75 @Input() set initialOrgId(id: number) {
76 if (id) { this.startOrg = this.org.get(id); }
79 // Modify the selected org unit via data binding.
80 // This WILL result in an onChange event firing.
81 @Input() set applyOrg(org: IdlObject) {
83 this.selected = this.formatForDisplay(org);
87 permLimitOrgs: number[];
88 @Input() set limitPerms(perms: string[]) {
89 this.applyPermLimitOrgs(perms);
92 // Modify the selected org unit by ID via data binding.
93 // This WILL result in an onChange event firing.
94 @Input() set applyOrgId(id: number) {
96 this.selected = this.formatForDisplay(this.org.get(id));
100 // Emitted when the org unit value is changed via the selector.
101 // Does not fire on initialOrg
102 @Output() onChange = new EventEmitter<IdlObject>();
105 private auth: AuthService,
106 private store: StoreService,
107 private org: OrgService,
108 private perm: PermService
113 // Apply a default org unit if desired and possible.
114 if (!this.startOrg && this.applyDefault && this.auth.user()) {
115 // note: ws_ou defaults to home_ou on the server
116 // when when no workstation is used
117 this.startOrg = this.org.get(this.auth.user().ws_ou());
118 this.selected = this.formatForDisplay(
119 this.org.get(this.auth.user().ws_ou())
122 // avoid notifying mid-digest
123 setTimeout(() => this.onChange.emit(this.startOrg), 0);
127 this.selected = this.formatForDisplay(this.startOrg);
132 applyPermLimitOrgs(perms: string[]) {
138 // handle lazy clients that pass null perm names
139 perms = perms.filter(p => p !== null && p !== undefined);
141 if (perms.length === 0) {
145 // NOTE: If permLimitOrgs is useful in a non-staff context
146 // we need to change this to support non-staff perm checks.
147 this.perm.hasWorkPermAt(perms, true).then(permMap => {
149 // safari-friendly version of Array.flat()
150 Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
154 // Format for display in the selector drop-down and input.
155 formatForDisplay(org: IdlObject): OrgDisplay {
158 label : PAD_SPACE.repeat(org.ou_type().depth())
159 + org[this.displayField](),
164 // Fired by the typeahead to inform us of a change.
165 // TODO: this does not fire when the value is cleared :( -- implement
166 // change detection on this.selected to look specifically for NULL.
167 orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
168 // console.debug('org unit change occurred ' + selEvent.item);
169 this.onChange.emit(this.org.get(selEvent.item.id));
172 // Remove the tree-padding spaces when matching.
173 formatter = (result: OrgDisplay) => result.label.trim();
175 filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
178 distinctUntilChanged(),
180 // Inject a specifier indicating the source of the
181 // action is a user click
182 this.click$.pipe(filter(() => !this.instance.isPopupOpen()))
183 .pipe(mapTo('_CLICK_'))
187 let orgs = this.org.list().filter(org =>
188 this.hidden.filter(id => org.id() === id).length === 0
191 if (this.permLimitOrgs) {
192 // Avoid showing org units where the user does
193 // not have the requested permission.
194 orgs = orgs.filter(org =>
195 this.permLimitOrgs.includes(org.id()));
198 if (term !== '_CLICK_') {
199 // For search-driven events, limit to the matching
201 orgs = orgs.filter(org => {
202 return term === '' || // show all
203 org[this.displayField]()
204 .toLowerCase().indexOf(term.toLowerCase()) > -1;
209 return orgs.map(org => this.formatForDisplay(org));