LP#1967328 Add multiple new permission group mappings at once
authorDan Briem <dbriem@wlsmail.org>
Sat, 5 Nov 2022 03:15:50 +0000 (03:15 +0000)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Apr 2023 11:13:38 +0000 (11:13 +0000)
Signed-off-by: Dan Briem <dbriem@wlsmail.org>
Signed-off-by: Susan Morrison <smorrison@georgialibraries.org>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>

Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts

index 8893086..00cbd13 100644 (file)
       <div class="col-lg-7">{{permGroup.name()}}</div>
     </div>
     <div class="row mt-1 pt-1">
-      <div class="col-lg-5" i18n>New Permission</div>
-      <div class="col-lg-7">
-        <eg-combobox [asyncDataSource]="permEntries"
-          (onChange)="perm = $event ? $event.id : null">
-        </eg-combobox>
+      <div class="col-lg-5">
+        <label for="select-perms" i18n>New Permission</label>
       </div>
-    </div>
-    <div class="row mt-1 pt-1">
-      <div class="col-lg-5" i18n>Depth</div>
-      <div class="col-lg-7">
-        <select [(ngModel)]="depth" class="p-1">
-          <option *ngFor="let d of orgDepths" value="{{d}}">{{d}}</option>
-        </select>
-      </div>
-    </div>
-    <div class="row mt-1 pt-1">
-      <div class="col-lg-5" i18n>Grantable</div>
       <div class="col-lg-7">
-        <input type="checkbox" [(ngModel)]="grantable"/>
+        <input type="text" id="select-perms" #selectPerms
+          [ngbTypeahead]="permEntries"
+          [inputFormatter]="permEntriesFormatter"
+          [resultFormatter]="permEntriesFormatter"
+          [editable]="false"
+          (selectItem)="select($event); selectPerms.value=''">
       </div>
     </div>
+    <ng-container *ngFor="let map of newPermMaps.controls; let i = index">
+      <ng-container [formGroup]="map">
+        <div class="row mt-1 pt-1">
+          <div class="col-lg-12">
+            <hr>
+            <h5 i18n>{{map.controls.label.value}}</h5>
+          </div>
+        </div>
+        <div class="row row-cols-4 mt-1 pt-1">
+          <div class="col">
+            <label [attr.for]="'depth-'+map.controls.id.value"
+              i18n>Depth
+            </label>
+          </div>
+          <div class="col">
+            <select formControlName="depth" class="p-1"
+              id="depth-{{map.controls.id.value}}">
+              <option *ngFor="let d of orgDepths" value="{{d}}">{{d}}</option>
+            </select>
+          </div>
+          <div class="col">
+            <label [attr.for]="'grantable-'+map.controls.id.value"
+              i18n>Grantable
+            </label>
+          </div>
+          <div class="col">
+            <input type="checkbox" formControlName="grantable"
+              id="grantable-{{map.controls.id.value}}">
+          </div>
+        </div>
+        <div class="row mt-1 pt-1">
+          <div class="col-lg-12">
+            <button type="button" class="btn btn-danger"
+              (click)="remove(i)"
+              i18n>Remove
+            </button>
+          </div>
+        </div>
+      </ng-container>
+    </ng-container>
   </div>
   <div class="modal-footer">
     <button type="button" class="btn btn-success" 
-      (click)="create()" i18n>Create</button>
+      [disabled]="!selectedPermEntries.length"
+      (click)="onCreate.next()" i18n>Create</button>
     <button type="button" class="btn btn-warning" 
       (click)="close()" i18n>Cancel</button>
   </div>
index 8095a65..3ac357c 100644 (file)
@@ -1,10 +1,13 @@
-import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
-import {Observable, from, EMPTY, throwError} from 'rxjs';
+import {Component, Input, OnDestroy, OnInit, Renderer2} from '@angular/core';
+import {Observable, Subject, of, OperatorFunction} from 'rxjs';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
-import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
-import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {NgbModal, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
+import {FormArray, FormBuilder} from '@angular/forms';
+import {catchError, debounceTime, distinctUntilChanged, exhaustMap, map, takeUntil, tap, toArray} from 'rxjs/operators';
+
+interface PermEntry { id: number; label: string; }
 
 @Component({
   selector: 'eg-perm-group-map-dialog',
@@ -15,7 +18,7 @@ import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
  * Ask the user which part is the lead part then merge others parts in.
  */
 export class PermGroupMapDialogComponent
-    extends DialogComponent implements OnInit {
+    extends DialogComponent implements OnInit, OnDestroy {
 
     @Input() permGroup: IdlObject;
 
@@ -29,51 +32,74 @@ export class PermGroupMapDialogComponent
 
     // Note we have all of the permissions on hand, but rendering the
     // full list of permissions can caus sluggishness.  Render async instead.
-    permEntries: (term: string) => Observable<ComboboxEntry>;
+    permEntries = this.permEntriesOperator();
+    permEntriesFormatter = (entry: PermEntry): string => entry.label;
+    selectedPermEntries: PermEntry[] = [];
 
     // Permissions the user may apply to the current group.
-    trimmedPerms: IdlObject[];
+    trimmedPerms: IdlObject[] = [];
+
+    permMapsForm = this.fb.group({ newPermMaps: this.fb.array([]) });
+    get newPermMaps() {
+        return this.permMapsForm.controls.newPermMaps as FormArray;
+    }
 
-    depth: number;
-    grantable: boolean;
-    perm: number;
+    onCreate = new Subject<void>();
+    onDestroy = new Subject<void>();
 
     constructor(
         private idl: IdlService,
         private pcrud: PcrudService,
-        private modal: NgbModal) {
+        private modal: NgbModal,
+        private renderer: Renderer2,
+        private fb: FormBuilder) {
         super(modal);
     }
 
     ngOnInit() {
-        this.depth = 0;
-        this.grantable = false;
 
         this.permissions = this.permissions
             .sort((a, b) => a.code() < b.code() ? -1 : 1);
 
-        this.onOpen$.subscribe(() => this.trimPermissions());
+        this.onOpen$.pipe(
+            tap(() => this.reset()),
+            takeUntil(this.onDestroy)
+        ).subscribe(() => this.focusPermSelector());
 
+        this.onCreate.pipe(
+            exhaustMap(() => this.create()),
+            takeUntil(this.onDestroy)
+        ).subscribe(success => this.close(success));
 
-        this.permEntries = (term: string) => {
-            if (term === null || term === undefined) { return EMPTY; }
-            term = ('' + term).toLowerCase();
+    }
 
-            // Find entries whose code or description match the search term
+    // Find entries whose code or description match the search term
+    private permEntriesOperator(): OperatorFunction<string, PermEntry[]> {
+        return term$ => term$.pipe(
+            debounceTime(300),
+            map(term => (term ?? '').toLowerCase()),
+            distinctUntilChanged(),
+            map(term => this.permEntryResults(term))
+        );
+    }
 
-            const entries: ComboboxEntry[] =  [];
-            this.trimmedPerms.forEach(p => {
-                if (p.code().toLowerCase().includes(term) ||
-                    (p.description() || '').toLowerCase().includes(term)) {
-                    entries.push({id: p.id(), label: p.code()});
-                }
-            });
+    private permEntryResults(term: string): PermEntry[] {
+        if (/^\s*$/.test(term)) return [];
 
-            return from(entries);
-        };
+        return this.trimmedPerms.reduce<PermEntry[]>((entries, p) => {
+            if ((p.code().toLowerCase().includes(term) ||
+                (p.description() || '').toLowerCase().includes(term)) &&
+                !this.selectedPermEntries.find(s => s.id === p.id())
+            ) entries.push({ id: p.id(), label: p.code() });
+            return entries;
+        }, []);
     }
 
-    trimPermissions() {
+    private reset() {
+        this.permMapsForm = this.fb.group({
+            newPermMaps: this.fb.array([])
+        });
+        this.selectedPermEntries = [];
         this.trimmedPerms = [];
 
         this.permissions.forEach(p => {
@@ -91,19 +117,51 @@ export class PermGroupMapDialogComponent
         });
     }
 
-    create() {
-        const map = this.idl.create('pgpm');
+    private focusPermSelector(): void {
+        const el = this.renderer.selectRootElement(
+            '#select-perms'
+        );
+        if (el) el.focus();
+    }
+
+    select(event: NgbTypeaheadSelectItemEvent<PermEntry>): void {
+        event.preventDefault();
+        this.newPermMaps.push(this.fb.group({
+            ...event.item, depth: 0, grantable: false
+        }));
+        this.selectedPermEntries.push({ ...event.item });
+    }
+
+    remove(index: number): void {
+        this.newPermMaps.removeAt(index);
+        this.selectedPermEntries.splice(index, 1);
+        if (!this.selectedPermEntries.length)
+            this.focusPermSelector();
+    }
+
+    create(): Observable<boolean> {
+        const maps: IdlObject[] = this.newPermMaps.getRawValue().map(
+            ({ id, depth, grantable }) => {
+                const map = this.idl.create('pgpm');
+
+                map.grp(this.permGroup.id());
+                map.perm(id);
+                map.grantable(grantable ? 't' : 'f');
+                map.depth(depth);
 
-        map.grp(this.permGroup.id());
-        map.perm(this.perm);
-        map.grantable(this.grantable ? 't' : 'f');
-        map.depth(this.depth);
+                return map;
+            });
 
-        this.pcrud.create(map).subscribe(
-            newMap => this.close(newMap),
-            err => throwError(err)
+        return this.pcrud.create(maps).pipe(
+            catchError(() => of(false)),
+            toArray(),
+            map(newMaps => !newMaps.includes(false))
         );
     }
-}
+
+    ngOnDestroy(): void {
+        this.onDestroy.next();
+    }
+}   
 
 
index 9e6438c..1b45e21 100644 (file)
@@ -363,8 +363,12 @@ export class PermGroupTreeComponent implements OnInit {
     openAddDialog() {
         this.addMappingDialog.open().subscribe(
             modified => {
-                this.createMapString.current().then(msg => this.toast.success(msg));
-                this.loadPermMaps();
+                if (modified) {
+                    this.createMapString.current().then(msg => this.toast.success(msg));
+                    this.loadPermMaps();
+                } else {
+                    this.errorMapString.current().then(msg => this.toast.danger(msg));
+                }
             }
         );
     }