1 /** TODO PORT ME TO <eg-combobox> */
2 import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
3 import {Observable, Subject} from 'rxjs';
4 import {map, mapTo, debounceTime, distinctUntilChanged, merge, filter} from 'rxjs/operators';
5 import {AuthService} from '@eg/core/auth.service';
6 import {StoreService} from '@eg/core/store.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {IdlObject} from '@eg/core/idl.service';
9 import {PermService} from '@eg/core/perm.service';
10 import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
12 // Use a unicode char for spacing instead of ASCII=32 so the browser
13 // won't collapse the nested display entries down to a single space.
14 const PAD_SPACE = ' '; // U+2007
16 interface OrgDisplay {
23 selector: 'eg-org-select',
24 templateUrl: './org-select.component.html'
26 export class OrgSelectComponent implements OnInit {
29 hidden: number[] = [];
30 click$ = new Subject<string>();
33 // Disable the entire input
34 @Input() disabled: boolean;
36 @ViewChild('instance', { static: false }) instance: NgbTypeahead;
38 // Placeholder text for selector input
39 @Input() placeholder = '';
40 @Input() stickySetting: string;
42 // ID to display in the DOM for this selector
45 // Org unit field displayed in the selector
46 @Input() displayField = 'shortname';
48 // Apply a default org unit value when none is set.
49 // First tries workstation org unit, then user home org unit.
50 // An onChange event WILL be generated when a default is applied.
51 @Input() applyDefault = false;
53 @Input() readOnly = false;
55 // List of org unit IDs to exclude from the selector
56 @Input() set hideOrgs(ids: number[]) {
57 if (ids) { this.hidden = ids; }
60 // List of org unit IDs to disable in the selector
61 _disabledOrgs: number[] = [];
62 @Input() set disableOrgs(ids: number[]) {
63 if (ids) { this._disabledOrgs = ids; }
66 // Apply an org unit value at load time.
67 // This will NOT result in an onChange event.
68 @Input() set initialOrg(org: IdlObject) {
69 if (org) { this.startOrg = org; }
72 // Apply an org unit value by ID at load time.
73 // This will NOT result in an onChange event.
74 @Input() set initialOrgId(id: number) {
75 if (id) { this.startOrg = this.org.get(id); }
78 // Modify the selected org unit via data binding.
79 // This WILL result in an onChange event firing.
80 @Input() set applyOrg(org: IdlObject) {
82 this.selected = this.formatForDisplay(org);
86 permLimitOrgs: number[];
87 @Input() set limitPerms(perms: string[]) {
88 this.applyPermLimitOrgs(perms);
91 // Modify the selected org unit by ID via data binding.
92 // This WILL result in an onChange event firing.
93 @Input() set applyOrgId(id: number) {
95 this.selected = this.formatForDisplay(this.org.get(id));
99 // Emitted when the org unit value is changed via the selector.
100 // Does not fire on initialOrg
101 @Output() onChange = new EventEmitter<IdlObject>();
103 // convenience method to get an IdlObject representing the current
104 // selected org unit. One way of invoking this is via a template
105 // reference variable.
106 selectedOrg(): IdlObject {
107 if (this.selected == null) {
110 return this.org.get(this.selected.id);
113 sortedOrgs: IdlObject[] = [];
116 private auth: AuthService,
117 private store: StoreService,
118 private org: OrgService,
119 private perm: PermService
124 // Sort the tree and reabsorb to propagate the sorted nodes to the
125 // org.list() used by this component.
126 this.org.sortTree(this.displayField);
127 this.org.absorbTree();
128 // Maintain our own copy of the org list in case the org service
129 // is sorted in a different manner by other parts of the code.
130 this.sortedOrgs = this.org.list();
132 // Apply a default org unit if desired and possible.
133 if (!this.startOrg && this.applyDefault && this.auth.user()) {
134 // note: ws_ou defaults to home_ou on the server
135 // when when no workstation is used
136 this.startOrg = this.org.get(this.auth.user().ws_ou());
137 this.selected = this.formatForDisplay(
138 this.org.get(this.auth.user().ws_ou())
141 // avoid notifying mid-digest
142 setTimeout(() => this.onChange.emit(this.startOrg), 0);
146 this.selected = this.formatForDisplay(this.startOrg);
151 applyPermLimitOrgs(perms: string[]) {
157 // handle lazy clients that pass null perm names
158 perms = perms.filter(p => p !== null && p !== undefined);
160 if (perms.length === 0) {
164 // NOTE: If permLimitOrgs is useful in a non-staff context
165 // we need to change this to support non-staff perm checks.
166 this.perm.hasWorkPermAt(perms, true).then(permMap => {
168 // safari-friendly version of Array.flat()
169 Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
173 // Format for display in the selector drop-down and input.
174 formatForDisplay(org: IdlObject): OrgDisplay {
175 let label = org[this.displayField]();
176 if (!this.readOnly) {
177 label = PAD_SPACE.repeat(org.ou_type().depth()) + label;
186 // Fired by the typeahead to inform us of a change.
187 // TODO: this does not fire when the value is cleared :( -- implement
188 // change detection on this.selected to look specifically for NULL.
189 orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
190 // console.debug('org unit change occurred ' + selEvent.item);
191 this.onChange.emit(this.org.get(selEvent.item.id));
194 // Remove the tree-padding spaces when matching.
195 formatter = (result: OrgDisplay) => result ? result.label.trim() : '';
197 // reset the state of the component
199 this.selected = null;
202 filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
205 distinctUntilChanged(),
207 // Inject a specifier indicating the source of the
208 // action is a user click
209 this.click$.pipe(filter(() => !this.instance.isPopupOpen()))
210 .pipe(mapTo('_CLICK_'))
214 let orgs = this.sortedOrgs.filter(org =>
215 this.hidden.filter(id => org.id() === id).length === 0
218 if (this.permLimitOrgs) {
219 // Avoid showing org units where the user does
220 // not have the requested permission.
221 orgs = orgs.filter(org =>
222 this.permLimitOrgs.includes(org.id()));
225 if (term !== '_CLICK_') {
226 // For search-driven events, limit to the matching
228 orgs = orgs.filter(org => {
229 return term === '' || // show all
230 org[this.displayField]()
231 .toLowerCase().indexOf(term.toLowerCase()) > -1;
236 return orgs.map(org => this.formatForDisplay(org));