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