]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
LP#1801984 Upgrading Angular 6 to Angular 7
[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     disabled: number[] = [];
31     click$ = new Subject<string>();
32     startOrg: IdlObject;
33
34     @ViewChild('instance') instance: NgbTypeahead;
35
36     // Placeholder text for selector input
37     @Input() placeholder = '';
38     @Input() stickySetting: string;
39
40     // ID to display in the DOM for this selector
41     @Input() domId = '';
42
43     // Org unit field displayed in the selector
44     @Input() displayField = 'shortname';
45
46     // Apply a default org unit value when none is set.
47     // First tries workstation org unit, then user home org unit.
48     // An onChange event WILL be generated when a default is applied.
49     @Input() applyDefault = false;
50
51     @Input() readOnly = false;
52
53     // List of org unit IDs to exclude from the selector
54     @Input() set hideOrgs(ids: number[]) {
55         if (ids) { this.hidden = ids; }
56     }
57
58     // List of org unit IDs to disable in the selector
59     @Input() set disableOrgs(ids: number[]) {
60         if (ids) { this.disabled = ids; }
61     }
62
63     // Apply an org unit value at load time.
64     // This will NOT result in an onChange event.
65     @Input() set initialOrg(org: IdlObject) {
66         if (org) { this.startOrg = org; }
67     }
68
69     // Apply an org unit value by ID at load time.
70     // This will NOT result in an onChange event.
71     @Input() set initialOrgId(id: number) {
72         if (id) { this.startOrg = this.org.get(id); }
73     }
74
75     // Modify the selected org unit via data binding.
76     // This WILL result in an onChange event firing.
77     @Input() set applyOrg(org: IdlObject) {
78         if (org) {
79             this.selected = this.formatForDisplay(org);
80         }
81     }
82
83     permLimitOrgs: number[];
84     @Input() set limitPerms(perms: string[]) {
85         this.applyPermLimitOrgs(perms);
86     }
87
88     // Modify the selected org unit by ID via data binding.
89     // This WILL result in an onChange event firing.
90     @Input() set applyOrgId(id: number) {
91         if (id) {
92             this.selected = this.formatForDisplay(this.org.get(id));
93         }
94     }
95
96     // Emitted when the org unit value is changed via the selector.
97     // Does not fire on initialOrg
98     @Output() onChange = new EventEmitter<IdlObject>();
99
100     constructor(
101       private auth: AuthService,
102       private store: StoreService,
103       private org: OrgService,
104       private perm: PermService
105     ) { }
106
107     ngOnInit() {
108
109         // Apply a default org unit if desired and possible.
110         if (!this.startOrg && this.applyDefault && this.auth.user()) {
111             // note: ws_ou defaults to home_ou on the server
112             // when when no workstation is used
113             this.startOrg = this.org.get(this.auth.user().ws_ou());
114             this.selected = this.formatForDisplay(
115                 this.org.get(this.auth.user().ws_ou())
116             );
117
118             // avoid notifying mid-digest
119             setTimeout(() => this.onChange.emit(this.startOrg), 0);
120         }
121
122         if (this.startOrg) {
123             this.selected = this.formatForDisplay(this.startOrg);
124         }
125     }
126
127     //
128     applyPermLimitOrgs(perms: string[]) {
129
130         if (!perms) {
131             return;
132         }
133
134         // handle lazy clients that pass null perm names
135         perms = perms.filter(p => p !== null && p !== undefined);
136
137         if (perms.length === 0) {
138             return;
139         }
140
141         // NOTE: If permLimitOrgs is useful in a non-staff context
142         // we need to change this to support non-staff perm checks.
143         this.perm.hasWorkPermAt(perms, true).then(permMap => {
144             this.permLimitOrgs =
145                 // safari-friendly version of Array.flat()
146                 Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
147         });
148     }
149
150     // Format for display in the selector drop-down and input.
151     formatForDisplay(org: IdlObject): OrgDisplay {
152         let label = org[this.displayField]();
153         if (!this.readOnly) {
154             label = PAD_SPACE.repeat(org.ou_type().depth()) + label;
155         }
156         return {
157             id : org.id(),
158             label : label,
159             disabled : false
160         };
161     }
162
163     // Fired by the typeahead to inform us of a change.
164     // TODO: this does not fire when the value is cleared :( -- implement
165     // change detection on this.selected to look specifically for NULL.
166     orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
167         // console.debug('org unit change occurred ' + selEvent.item);
168         this.onChange.emit(this.org.get(selEvent.item.id));
169     }
170
171     // Remove the tree-padding spaces when matching.
172     formatter = (result: OrgDisplay) => result.label.trim();
173
174     filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
175         return text$.pipe(
176             debounceTime(200),
177             distinctUntilChanged(),
178             merge(
179                 // Inject a specifier indicating the source of the
180                 // action is a user click
181                 this.click$.pipe(filter(() => !this.instance.isPopupOpen()))
182                 .pipe(mapTo('_CLICK_'))
183             ),
184             map(term => {
185
186                 let orgs = this.org.list().filter(org =>
187                     this.hidden.filter(id => org.id() === id).length === 0
188                 );
189
190                 if (this.permLimitOrgs) {
191                     // Avoid showing org units where the user does
192                     // not have the requested permission.
193                     orgs = orgs.filter(org =>
194                         this.permLimitOrgs.includes(org.id()));
195                 }
196
197                 if (term !== '_CLICK_') {
198                     // For search-driven events, limit to the matching
199                     // org units.
200                     orgs = orgs.filter(org => {
201                         return term === '' || // show all
202                             org[this.displayField]()
203                                 .toLowerCase().indexOf(term.toLowerCase()) > -1;
204
205                     });
206                 }
207
208                 return orgs.map(org => this.formatForDisplay(org));
209             })
210         );
211     }
212 }
213
214