]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
LP1857350 Org selector sorts by display value
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / share / org-select / org-select.component.ts
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';
11
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
15
16 interface OrgDisplay {
17   id: number;
18   label: string;
19   disabled: boolean;
20 }
21
22 @Component({
23   selector: 'eg-org-select',
24   templateUrl: './org-select.component.html'
25 })
26 export class OrgSelectComponent implements OnInit {
27
28     selected: OrgDisplay;
29     hidden: number[] = [];
30     click$ = new Subject<string>();
31     startOrg: IdlObject;
32
33     // Disable the entire input
34     @Input() disabled: boolean;
35
36     @ViewChild('instance', { static: false }) instance: NgbTypeahead;
37
38     // Placeholder text for selector input
39     @Input() placeholder = '';
40     @Input() stickySetting: string;
41
42     // ID to display in the DOM for this selector
43     @Input() domId = '';
44
45     // Org unit field displayed in the selector
46     @Input() displayField = 'shortname';
47
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;
52
53     @Input() readOnly = false;
54
55     // List of org unit IDs to exclude from the selector
56     @Input() set hideOrgs(ids: number[]) {
57         if (ids) { this.hidden = ids; }
58     }
59
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; }
64     }
65
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; }
70     }
71
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); }
76     }
77
78     // Modify the selected org unit via data binding.
79     // This WILL result in an onChange event firing.
80     @Input() set applyOrg(org: IdlObject) {
81         if (org) {
82             this.selected = this.formatForDisplay(org);
83         }
84     }
85
86     permLimitOrgs: number[];
87     @Input() set limitPerms(perms: string[]) {
88         this.applyPermLimitOrgs(perms);
89     }
90
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) {
94         if (id) {
95             this.selected = this.formatForDisplay(this.org.get(id));
96         }
97     }
98
99     // Emitted when the org unit value is changed via the selector.
100     // Does not fire on initialOrg
101     @Output() onChange = new EventEmitter<IdlObject>();
102
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) {
108             return null;
109         }
110         return this.org.get(this.selected.id);
111     }
112
113     sortedOrgs: IdlObject[] = [];
114
115     constructor(
116       private auth: AuthService,
117       private store: StoreService,
118       private org: OrgService,
119       private perm: PermService
120     ) { }
121
122     ngOnInit() {
123
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();
131
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())
139             );
140
141             // avoid notifying mid-digest
142             setTimeout(() => this.onChange.emit(this.startOrg), 0);
143         }
144
145         if (this.startOrg) {
146             this.selected = this.formatForDisplay(this.startOrg);
147         }
148     }
149
150     //
151     applyPermLimitOrgs(perms: string[]) {
152
153         if (!perms) {
154             return;
155         }
156
157         // handle lazy clients that pass null perm names
158         perms = perms.filter(p => p !== null && p !== undefined);
159
160         if (perms.length === 0) {
161             return;
162         }
163
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 => {
167             this.permLimitOrgs =
168                 // safari-friendly version of Array.flat()
169                 Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
170         });
171     }
172
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;
178         }
179         return {
180             id : org.id(),
181             label : label,
182             disabled : false
183         };
184     }
185
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));
192     }
193
194     // Remove the tree-padding spaces when matching.
195     formatter = (result: OrgDisplay) => result ? result.label.trim() : '';
196
197     // reset the state of the component
198     reset() {
199         this.selected = null;
200     }
201
202     filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
203         return text$.pipe(
204             debounceTime(200),
205             distinctUntilChanged(),
206             merge(
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_'))
211             ),
212             map(term => {
213
214                 let orgs = this.sortedOrgs.filter(org =>
215                     this.hidden.filter(id => org.id() === id).length === 0
216                 );
217
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()));
223                 }
224
225                 if (term !== '_CLICK_') {
226                     // For search-driven events, limit to the matching
227                     // org units.
228                     orgs = orgs.filter(org => {
229                         return term === '' || // show all
230                             org[this.displayField]()
231                                 .toLowerCase().indexOf(term.toLowerCase()) > -1;
232
233                     });
234                 }
235
236                 return orgs.map(org => this.formatForDisplay(org));
237             })
238         );
239     }
240 }
241
242