1 import {Component, OnInit, AfterViewInit, Input, Output, ViewChild,
2 EventEmitter, forwardRef} from '@angular/core';
3 import {ControlValueAccessor, FormGroup, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
4 import {Observable, from, of} from 'rxjs';
5 import {tap, map, switchMap} from 'rxjs/operators';
6 import {IdlObject} from '@eg/core/idl.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PermService} from '@eg/core/perm.service';
10 import {PcrudService} from '@eg/core/pcrud.service';
11 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
12 import {StringComponent} from '@eg/share/string/string.component';
13 import {ItemLocationService} from './item-location-select.service';
16 * Item (Copy) Location Selector.
18 * <eg-item-location-select [(ngModel)]="myAcplId"
19 [contextOrgId]="anOrgId" permFilter="ADMIN_STUFF">
20 * </eg-item-location-select>
24 selector: 'eg-item-location-select',
25 templateUrl: './item-location-select.component.html',
27 provide: NG_VALUE_ACCESSOR,
28 useExisting: forwardRef(() => ItemLocationSelectComponent),
32 export class ItemLocationSelectComponent
33 implements OnInit, AfterViewInit, ControlValueAccessor {
36 // Limit copy locations to those owned at or above org units where
37 // the user has work permissions for the provided permission code.
38 @Input() permFilter: string;
40 // Limit copy locations to those owned at or above this org unit.
41 private _contextOrgId: number;
42 @Input() set contextOrgId(value: number) {
43 this._contextOrgId = value;
47 // ... though if includeDescendants is true, shelving
48 // locations at the descendants of the context OU are
49 // also included; this is a special case for the
51 @Input() set includeDescendants(value: boolean) {
52 this._includeDescendants = value;
55 get includeDescendants(): boolean {
56 return this._includeDescendants;
59 get contextOrgId(): number {
60 return this._contextOrgId;
63 // Load locations for multiple context org units.
64 private _contextOrgIds = [];
65 private _includeDescendants = false;
66 @Input() set contextOrgIds(value: number[]) {
67 this._contextOrgIds = value;
70 get contextOrgIds(): number[] {
71 return this._contextOrgIds;
74 @Input() orgUnitLabelField = 'shortname';
76 // Emits an acpl object or null on combobox value change
77 @Output() valueChange: EventEmitter<IdlObject>;
78 // Emits the combobox entry or null on value change
79 @Output() entryChange: EventEmitter<ComboboxEntry>;
81 @Input() required: boolean;
83 @Input() domId = 'eg-item-location-select-' +
84 ItemLocationSelectComponent.domIdAuto++;
86 // If false, selector will be click-able
87 @Input() loadAsync = false;
89 @Input() disabled = false;
91 // Display the selected value as text instead of within
93 @Input() readOnly = false;
96 @Input() startsWith = false;
98 // Show <Unset> when no value is applied.
99 // This only applies to non-required fields, since <Unset> would
100 // trick the combobox into thinking a valid value had been applied
101 @Input() showUnsetString = true;
103 @ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
104 @ViewChild('unsetString', {static: false}) unsetString: StringComponent;
106 @Input() startId: number = null;
107 filterOrgs: number[] = [];
108 filterOrgsApplied = false;
110 initDone = false; // true after first data load
111 propagateChange = (id: number) => {};
112 propagateTouch = () => {};
114 getLocationsAsyncHandler = term => this.getLocationsAsync(term);
117 private org: OrgService,
118 private auth: AuthService,
119 private perm: PermService,
120 private pcrud: PcrudService,
121 private loc: ItemLocationService
123 this.valueChange = new EventEmitter<IdlObject>();
124 this.entryChange = new EventEmitter<ComboboxEntry>();
128 if (this.loadAsync) {
129 this.initDone = true;
132 .then(_ => this.getLocations())
133 .then(_ => this.initDone = true);
139 // Format the display of locations to include the org unit
140 this.comboBox.formatDisplayString = (result: ComboboxEntry) => {
141 let display = result.label || result.id;
142 display = (display + '').trim();
143 if (result.userdata) {
145 this.orgName(result.userdata.owning_lib()) + ')';
151 getLocations(): Promise<any> {
153 if (this.filterOrgs.length === 0) {
154 this.comboBox.entries = [];
155 return Promise.resolve();
158 const search: any = {deleted: 'f'};
161 // Guarantee we have the load-time copy location, which
162 // may not be included in the org-scoped set of locations
163 // we fetch by default.
166 {owning_lib: this.filterOrgs}
169 search.owning_lib = this.filterOrgs;
172 const entries: ComboboxEntry[] = [];
174 if (!this.required && this.showUnsetString) {
175 entries.push({id: null, label: this.unsetString.text});
178 return this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
180 this.loc.locationCache[loc.id()] = loc;
181 entries.push({id: loc.id(), label: loc.name(), userdata: loc});
182 })).toPromise().then(_ => {
183 this.comboBox.entries = entries;
187 getLocationsAsync(term: string): Observable<ComboboxEntry> {
188 // "1" is ignored, but a value is needed for pipe() below
191 if (!this.filterOrgsApplied) {
192 // Apply filter orgs the first time they are needed.
193 obs = from(this.setFilterOrgs());
196 return obs.pipe(switchMap(_ => this.getLocationsAsync2(term)));
199 getLocationsAsync2(term: string): Observable<ComboboxEntry> {
201 if (this.filterOrgs.length === 0) {
205 const ilike = this.startsWith ? `${term}%` : `%${term}%`;
207 const search: any = {
209 name: {'ilike': ilike}
213 // Guarantee we have the load-time copy location, which
214 // may not be included in the org-scoped set of locations
215 // we fetch by default.
218 {owning_lib: this.filterOrgs}
221 search.owning_lib = this.filterOrgs;
224 return new Observable<ComboboxEntry>(observer => {
225 if (!this.required && this.showUnsetString) {
226 observer.next({id: null, label: this.unsetString.text});
229 this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
232 this.loc.locationCache[loc.id()] = loc;
233 observer.next({id: loc.id(), label: loc.name(), userdata: loc});
235 (err: unknown) => {},
236 () => observer.complete()
242 registerOnChange(fn) {
243 this.propagateChange = fn;
246 registerOnTouched(fn) {
247 this.propagateTouch = fn;
250 cboxChanged(entry: ComboboxEntry) {
251 const id = entry ? entry.id : null;
252 this.propagateChange(id);
253 this.valueChange.emit(id ? this.loc.locationCache[id] : null);
254 this.entryChange.emit(entry ? entry : null);
257 writeValue(id: number) {
259 this.getOneLocation(id).then(_ => this.comboBox.selectedId = id);
265 getOneLocation(id: number) {
266 if (!id) { return Promise.resolve(); }
268 const promise = this.loc.locationCache[id] ?
269 Promise.resolve(this.loc.locationCache[id]) :
270 this.pcrud.retrieve('acpl', id).toPromise();
272 return promise.then(loc => {
274 this.loc.locationCache[loc.id()] = loc;
275 const entry: ComboboxEntry = {
276 id: loc.id(), label: loc.name(), userdata: loc};
278 if (this.comboBox.entries) {
279 this.comboBox.entries.push(entry);
281 this.comboBox.entries = [entry];
286 setFilterOrgs(): Promise<number[]> {
287 let contextOrgIds: number[] = [];
289 if (this.contextOrgIds.length) {
290 contextOrgIds = this.contextOrgIds;
292 contextOrgIds = [this.contextOrgId || this.auth.user().ws_ou()];
296 contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.ancestors(id, true)));
297 if (this.includeDescendants) {
298 contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.descendants(id, true)));
301 this.filterOrgsApplied = true;
303 if (!this.permFilter) {
304 return Promise.resolve(this.filterOrgs = [...new Set(orgIds)]);
307 const orgsFromCache = this.loc.filterOrgsCache[this.permFilter];
308 if (orgsFromCache && !this._contextOrgId) {
309 // if we're using contextOrgId, it may well change, so
310 // don't use the cache
311 return Promise.resolve(this.filterOrgs = orgsFromCache);
314 return this.perm.hasWorkPermAt([this.permFilter], true)
316 // Include ancestors of perm-approved org units (shared item locations)
318 const permOrgIds = values[this.permFilter];
319 let trimmedOrgIds = [];
320 permOrgIds.forEach(orgId => {
321 if (orgIds.includes(orgId)) {
322 trimmedOrgIds = trimmedOrgIds.concat(this.org.ancestors(orgId, true));
323 if (this.includeDescendants) {
324 trimmedOrgIds = trimmedOrgIds.concat(this.org.descendants(orgId, true));
329 this.filterOrgs = [...new Set(trimmedOrgIds)];
330 this.loc.filterOrgsCache[this.permFilter] = this.filterOrgs;
332 return this.filterOrgs;
336 orgName(orgId: number): string {
337 return this.org.get(orgId)[this.orgUnitLabelField]();