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';
10 interface PermEntry { id: number; label: string; }
13 selector: 'eg-perm-group-map-dialog',
14 templateUrl: './perm-group-map-dialog.component.html'
18 * Ask the user which part is the lead part then merge others parts in.
20 export class PermGroupMapDialogComponent
21 extends DialogComponent implements OnInit, OnDestroy {
23 @Input() permGroup: IdlObject;
25 @Input() permissions: IdlObject[];
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[];
31 @Input() orgDepths: number[];
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[] = [];
39 // Permissions the user may apply to the current group.
40 trimmedPerms: IdlObject[] = [];
42 permMapsForm = this.fb.group({ newPermMaps: this.fb.array([]) });
44 return this.permMapsForm.controls.newPermMaps as FormArray;
47 onCreate = new Subject<void>();
48 onDestroy = new Subject<void>();
51 private idl: IdlService,
52 private pcrud: PcrudService,
53 private modal: NgbModal,
54 private renderer: Renderer2,
55 private fb: FormBuilder) {
61 this.permissions = this.permissions
62 .sort((a, b) => a.code() < b.code() ? -1 : 1);
65 tap(() => this.reset()),
66 takeUntil(this.onDestroy)
67 ).subscribe(() => this.focusPermSelector());
70 exhaustMap(() => this.create()),
71 takeUntil(this.onDestroy)
72 ).subscribe(success => this.close(success));
76 // Find entries whose code or description match the search term
77 private permEntriesOperator(): OperatorFunction<string, PermEntry[]> {
78 return term$ => term$.pipe(
80 map(term => (term ?? '').toLowerCase()),
81 distinctUntilChanged(),
82 map(term => this.permEntryResults(term))
86 private permEntryResults(term: string): PermEntry[] {
87 if (/^\s*$/.test(term)) return [];
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() });
99 this.permMapsForm = this.fb.group({
100 newPermMaps: this.fb.array([])
102 this.selectedPermEntries = [];
103 this.trimmedPerms = [];
105 this.permissions.forEach(p => {
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()) {
116 this.trimmedPerms.push(p);
120 private focusPermSelector(): void {
121 const el = this.renderer.selectRootElement(
127 select(event: NgbTypeaheadSelectItemEvent<PermEntry>): void {
128 event.preventDefault();
129 this.newPermMaps.push(this.fb.group({
130 ...event.item, depth: 0, grantable: false
132 this.selectedPermEntries.push({ ...event.item });
135 remove(index: number): void {
136 this.newPermMaps.removeAt(index);
137 this.selectedPermEntries.splice(index, 1);
138 if (!this.selectedPermEntries.length)
139 this.focusPermSelector();
142 create(): Observable<boolean> {
143 const maps: IdlObject[] = this.newPermMaps.getRawValue().map(
144 ({ id, depth, grantable }) => {
145 const map = this.idl.create('pgpm');
147 map.grp(this.permGroup.id());
149 map.grantable(grantable ? 't' : 'f');
155 return this.pcrud.create(maps).pipe(
156 catchError(() => of(false)),
158 map(newMaps => !newMaps.includes(false))
162 ngOnDestroy(): void {
163 this.onDestroy.next();