]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/patron/profile-select.component.ts
LP 2061136 follow-up: ng lint --fix
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / patron / profile-select.component.ts
1 import {Component, Input, Output, OnInit,
2     EventEmitter, ViewChild, forwardRef} from '@angular/core';
3 import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
4 import {Observable, of} from 'rxjs';
5 import {map} from 'rxjs/operators';
6 import {IdlService, IdlObject} from '@eg/core/idl.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {ComboboxEntry, ComboboxComponent
11 } from '@eg/share/combobox/combobox.component';
12
13 /* User permission group select comoboxbox.
14  *
15  * <eg-profile-select
16  *  [(ngModel)]="pgtObject" [useDisplayEntries]="true">
17  * </eg-profile-select>
18  */
19
20 // Use a unicode char for spacing instead of ASCII=32 so the browser
21 // won't collapse the nested display entries down to a single space.
22 const PAD_SPACE = ' '; // U+2007
23
24 @Component({
25     selector: 'eg-profile-select',
26     templateUrl: './profile-select.component.html',
27     providers: [{
28         provide: NG_VALUE_ACCESSOR,
29         useExisting: forwardRef(() => ProfileSelectComponent),
30         multi: true
31     }]
32 })
33 export class ProfileSelectComponent implements ControlValueAccessor, OnInit {
34
35     // If true, attempt to build the selector from
36     // permission.grp_tree_display_entry's for the current org unit.
37     // If false OR if no permission.grp_tree_display_entry's exist
38     // build the selector from the full permission.grp_tree
39     @Input() useDisplayEntries: boolean;
40
41     // Emits the selected 'pgt' object or null if the selector is cleared.
42     @Output() profileChange: EventEmitter<IdlObject>;
43
44     @ViewChild('combobox', {static: false}) cbox: ComboboxComponent;
45
46     // Set the initial value by ID
47     @Input() initialGroupId: number;
48
49     @Input() required = false;
50
51     cboxEntries: ComboboxEntry[] = [];
52     profiles: {[id: number]: IdlObject} = {};
53
54     // Stub functions required by ControlValueAccessor
55     propagateChange = (_: any) => {};
56     propagateTouch = () => {};
57
58     constructor(
59         private org: OrgService,
60         private idl: IdlService,
61         private auth: AuthService,
62         private pcrud: PcrudService) {
63         this.profileChange = new EventEmitter<IdlObject>();
64     }
65
66     ngOnInit() {
67         this.collectGroups().then(grps => this.sortGroups(grps))
68             .then(_ => this.fetchInitialGroup())
69             .then(_ => this.cbox.selectedId = this.initialGroupId);
70     }
71
72     // If the initial group is not included in our set of groups because
73     // we are using a custom display tree, fetch the group so we can
74     // add it to our tree.
75     fetchInitialGroup(): Promise<any> {
76         if (!this.initialGroupId || this.profiles[this.initialGroupId]) {
77             return Promise.resolve();
78         }
79
80         return this.pcrud.retrieve('pgt', this.initialGroupId).toPromise()
81             .then(grp => {
82                 this.profiles[grp.id()] = grp;
83                 grp.parent(null);
84                 this.cboxEntries.push(
85                     {id: grp.id(), label: this.grpLabel([], grp)});
86             });
87
88     }
89
90     collectGroups(): Promise<IdlObject[]> {
91
92         if (!this.useDisplayEntries) {
93             return this.fetchPgt();
94         }
95
96         return this.pcrud.search('pgtde',
97             {org: this.org.ancestors(this.auth.user().ws_ou(), true)},
98             {flesh: 1, flesh_fields: {'pgtde': ['grp', 'children']}, 'order_by':{'pgtde':'position desc'}},
99             {atomic: true}
100
101         ).toPromise().then(groups => {
102
103             if (groups.length === 0) { return this.fetchPgt(); }
104
105             // In the query above, we fetch display entries for our org
106             // unit plus ancestors.  However, we only want to use one
107             // collection of display entries, those owned at our org
108             // unit or our closest ancestor.
109             let closestOrg = this.org.get(groups[0].org());
110             groups.forEach(g => {
111                 const org = this.org.get(g.org());
112                 if (closestOrg.ou_type().depth() < org.ou_type().depth()) {
113                     closestOrg = org;
114                 }
115             });
116
117             groups = groups.filter(g => g.org() === closestOrg.id());
118
119             // Translate the display entries into a 'pgt' tree
120
121             const tree: IdlObject[] = [];
122
123             groups.forEach(display => {
124                 const grp = display.grp();
125                 const displayParent = groups.filter(g => g.id() === display.parent())[0];
126                 grp.parent(displayParent ? displayParent.grp().id() : null);
127                 tree.push(grp);
128             });
129
130             return tree;
131         });
132     }
133
134     fetchPgt(): Promise<IdlObject[]> {
135         return this.pcrud.retrieveAll('pgt', {}, {atomic: true}).toPromise();
136     }
137
138     grpLabel(groups: IdlObject[], grp: IdlObject): string {
139         let tmp = grp;
140         let depth = 0;
141
142         do {
143             const pid = tmp.parent();
144             if (!pid) { break; } // top of the tree
145
146             // Should always produce a value unless a perm group
147             // display tree is poorly structured.
148             tmp = groups.filter(g => ((g._display) ? g._display.id() : g.id()) === pid)[0];
149
150             depth++;
151
152         } while (tmp);
153
154         return PAD_SPACE.repeat(depth) + grp.name();
155     }
156
157     sortGroups(groups: IdlObject[]) {
158
159         // When using display entries, there can be multiple groups
160         // with no parent.
161
162         groups.forEach(grp => {
163             if (grp.parent() === null) {
164                 this.sortOneGroup(groups, grp);
165             }
166         });
167     }
168
169     sortOneGroup(groups: IdlObject[], grp: IdlObject) {
170
171         if (!grp) {
172             grp = groups.filter(g => g.parent() === null)[0];
173         }
174
175         this.profiles[grp.id()] = grp;
176         this.cboxEntries.push(
177             {id: grp.id(), label: this.grpLabel(groups, grp)});
178
179         groups
180             .filter(g => g.parent() === grp.id())
181             .sort((a, b) => {
182                 if (a._display) {
183                     return a._display.position() < b._display.position() ? -1 : 1;
184                 } else {
185                     return a.name() < b.name() ? -1 : 1;
186                 }
187             })
188             .forEach(child => this.sortOneGroup(groups, child));
189     }
190
191     writeValue(pgt: IdlObject) {
192         const id = pgt ? pgt.id() : null;
193         if (this.cbox) {
194             this.cbox.selectedId = id;
195         } else {
196             // Will propagate to cbox after its instantiated.
197             this.initialGroupId = id;
198         }
199     }
200
201     registerOnChange(fn) {
202         this.propagateChange = fn;
203     }
204
205     registerOnTouched(fn) {
206         this.propagateTouch = fn;
207     }
208
209     propagateCboxChange(entry: ComboboxEntry) {
210         if (entry) {
211             const grp = this.profiles[entry.id];
212             this.propagateChange(grp);
213             this.profileChange.emit(grp);
214         } else {
215             this.profileChange.emit(null);
216             this.propagateChange(null);
217         }
218     }
219 }
220