LP#1967328 Add multiple new permission group mappings at once
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / perm-group-map-dialog.component.ts
1 import {Component, Input, OnDestroy, OnInit, Renderer2} from '@angular/core';
2 import {Observable, Subject, of, OperatorFunction} from 'rxjs';
3 import {DialogComponent} from '@eg/share/dialog/dialog.component';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {PcrudService} from '@eg/core/pcrud.service';
6 import {NgbModal, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
7 import {FormArray, FormBuilder} from '@angular/forms';
8 import {catchError, debounceTime, distinctUntilChanged, exhaustMap, map, takeUntil, tap, toArray} from 'rxjs/operators';
9
10 interface PermEntry { id: number; label: string; }
11
12 @Component({
13   selector: 'eg-perm-group-map-dialog',
14   templateUrl: './perm-group-map-dialog.component.html'
15 })
16
17 /**
18  * Ask the user which part is the lead part then merge others parts in.
19  */
20 export class PermGroupMapDialogComponent
21     extends DialogComponent implements OnInit, OnDestroy {
22
23     @Input() permGroup: IdlObject;
24
25     @Input() permissions: IdlObject[];
26
27     // List of grp-perm-map objects that relate to the selected permission
28     // group or are linked to a parent group.
29     @Input() permMaps: IdlObject[];
30
31     @Input() orgDepths: number[];
32
33     // Note we have all of the permissions on hand, but rendering the
34     // full list of permissions can caus sluggishness.  Render async instead.
35     permEntries = this.permEntriesOperator();
36     permEntriesFormatter = (entry: PermEntry): string => entry.label;
37     selectedPermEntries: PermEntry[] = [];
38
39     // Permissions the user may apply to the current group.
40     trimmedPerms: IdlObject[] = [];
41
42     permMapsForm = this.fb.group({ newPermMaps: this.fb.array([]) });
43     get newPermMaps() {
44         return this.permMapsForm.controls.newPermMaps as FormArray;
45     }
46
47     onCreate = new Subject<void>();
48     onDestroy = new Subject<void>();
49
50     constructor(
51         private idl: IdlService,
52         private pcrud: PcrudService,
53         private modal: NgbModal,
54         private renderer: Renderer2,
55         private fb: FormBuilder) {
56         super(modal);
57     }
58
59     ngOnInit() {
60
61         this.permissions = this.permissions
62             .sort((a, b) => a.code() < b.code() ? -1 : 1);
63
64         this.onOpen$.pipe(
65             tap(() => this.reset()),
66             takeUntil(this.onDestroy)
67         ).subscribe(() => this.focusPermSelector());
68
69         this.onCreate.pipe(
70             exhaustMap(() => this.create()),
71             takeUntil(this.onDestroy)
72         ).subscribe(success => this.close(success));
73
74     }
75
76     // Find entries whose code or description match the search term
77     private permEntriesOperator(): OperatorFunction<string, PermEntry[]> {
78         return term$ => term$.pipe(
79             debounceTime(300),
80             map(term => (term ?? '').toLowerCase()),
81             distinctUntilChanged(),
82             map(term => this.permEntryResults(term))
83         );
84     }
85
86     private permEntryResults(term: string): PermEntry[] {
87         if (/^\s*$/.test(term)) return [];
88
89         return this.trimmedPerms.reduce<PermEntry[]>((entries, p) => {
90             if ((p.code().toLowerCase().includes(term) ||
91                 (p.description() || '').toLowerCase().includes(term)) &&
92                 !this.selectedPermEntries.find(s => s.id === p.id())
93             ) entries.push({ id: p.id(), label: p.code() });
94             return entries;
95         }, []);
96     }
97
98     private reset() {
99         this.permMapsForm = this.fb.group({
100             newPermMaps: this.fb.array([])
101         });
102         this.selectedPermEntries = [];
103         this.trimmedPerms = [];
104
105         this.permissions.forEach(p => {
106
107             // Prevent duplicate permissions, for-loop for early exit.
108             for (let idx = 0; idx < this.permMaps.length; idx++) {
109                 const map = this.permMaps[idx];
110                 if (map.perm().id() === p.id() &&
111                     map.grp().id() === this.permGroup.id()) {
112                     return;
113                 }
114             }
115
116             this.trimmedPerms.push(p);
117         });
118     }
119
120     private focusPermSelector(): void {
121         const el = this.renderer.selectRootElement(
122             '#select-perms'
123         );
124         if (el) el.focus();
125     }
126
127     select(event: NgbTypeaheadSelectItemEvent<PermEntry>): void {
128         event.preventDefault();
129         this.newPermMaps.push(this.fb.group({
130             ...event.item, depth: 0, grantable: false
131         }));
132         this.selectedPermEntries.push({ ...event.item });
133     }
134
135     remove(index: number): void {
136         this.newPermMaps.removeAt(index);
137         this.selectedPermEntries.splice(index, 1);
138         if (!this.selectedPermEntries.length)
139             this.focusPermSelector();
140     }
141
142     create(): Observable<boolean> {
143         const maps: IdlObject[] = this.newPermMaps.getRawValue().map(
144             ({ id, depth, grantable }) => {
145                 const map = this.idl.create('pgpm');
146
147                 map.grp(this.permGroup.id());
148                 map.perm(id);
149                 map.grantable(grantable ? 't' : 'f');
150                 map.depth(depth);
151
152                 return map;
153             });
154
155         return this.pcrud.create(maps).pipe(
156             catchError(() => of(false)),
157             toArray(),
158             map(newMaps => !newMaps.includes(false))
159         );
160     }
161
162     ngOnDestroy(): void {
163         this.onDestroy.next();
164     }
165 }   
166
167