LP1904036 user perm editor
authorBill Erickson <berickxx@gmail.com>
Tue, 13 Apr 2021 16:19:26 +0000 (12:19 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:31 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/perms.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/perms.component.ts

index 7962dee..ae759fb 100644 (file)
@@ -5,56 +5,85 @@
   </div>
 </div>
 
-<ng-container *ngIf="!loading">
-  <h3 i18n>Working Location(s)</h3>
-
-  <div class="striped-rows-odd mt-3 mb-3">
-    <div class="row mt-2 pt-2 pb-2" *ngFor="let org of workableOrgs">
-      <div class="col-lg-12">
-        <div class="form-check form-check-inline">
-          <input class="form-check-input" type="checkbox" id="work-org-{{org.id()}}" 
-            [disabled]="!canAssignWorkOrgs[org.id()]" [(ngModel)]="workOuSelector[org.id()]">
-          <label class="form-check-label" for="work-org-{{org.id()}}" i18n>
-            {{org.shortname()}} ({{org.name()}})
-          </label>
+<ul ngbNav #permsNav="ngbNav" class="nav-tabs">
+
+  <li ngbNavItem>
+    <a ngbNavLink i18n>Working Location(s)</a>
+    <ng-template ngbNavContent>
+
+      <div class="striped-rows-odd mt-3 mb-3">
+        <div class="row mt-2 pt-2 pb-2" *ngFor="let org of workableOrgs">
+          <div class="col-lg-12">
+            <div class="form-check form-check-inline">
+              <input class="form-check-input" type="checkbox" id="work-org-{{org.id()}}" 
+                [ngModel]="userHasWorkOu(org.id())"
+                (ngModelChange)="userWorkOuChange(org.id(), $event)"
+                [disabled]="!canAssignWorkOrgs[org.id()]"/>
+              <label class="form-check-label" for="work-org-{{org.id()}}" i18n>
+                {{org.shortname()}} ({{org.name()}})
+              </label>
+            </div>
+          </div>
         </div>
       </div>
-    </div>
-  </div>
-
-  <hr class="mt-2 mb-2"/>
+    </ng-template>
+  </li>
+  <li ngbNavItem>
+    <a ngbNavLink i18n>User Permissions</a>
+    <ng-template ngbNavContent>
 
-  <h3 i18n>User Permissions</h3>
+      <div class="striped-rows-odd mt-3">
+        <div class="row pt-1 pb-1">
+          <div class="col-lg-5 font-weight-bold" i18n>Permission</div>
+          <div class="col-lg-2 font-weight-bold" i18n>Applied</div>
+          <div class="col-lg-2 font-weight-bold" i18n>Depth</div>
+          <div class="col-lg-2 font-weight-bold" i18n>Grantable</div>
+        </div>
+        <div class="row pt-1 pb-1" *ngFor="let perm of allPerms">
+          <div class="col-lg-5">{{perm.code()}}</div>
+          <div class="col-lg-2">
+            <input class="form-check-input ml-0 pl-0" type="checkbox"
+              [disabled]="!canGrantPerm(perm)"
+              [ngModel]="userHasPerm(perm)"
+              (ngModelChange)="permApplyChanged(perm, $event)"/>
+          </div>
+          <div class="col-lg-2">
+            <select class="form-control" 
+              [ngModel]="userHasPermAtDepth(perm)"
+              (ngModelChange)="permDepthChanged(perm, $event)">
+              <ng-container *ngFor="let depth of orgDepths">
+                <option [disabled]="depth < canGrantPermAtDepth(perm)" 
+                  [value]="depth">{{depth}}</option>
+              </ng-container>
+            </select>
+          </div>
+          <div class="col-lg-2">
+            <input class="form-check-input ml-0 pl-0" type="checkbox"
+              [disabled]="!canGrantPerm(perm)"
+              [ngModel]="userPermIsGrantable(perm)"
+              (ngModelChange)="grantableChanged(perm, $event)"/>
+          </div>
+        </div>
+      </div>
+    </ng-template>
+  </li>
+</ul>
 
-  <div class="striped-rows-odd mt-3">
-    <div class="row pt-1 pb-1">
-      <div class="col-lg-5 font-weight-bold" i18n>Permission</div>
-      <div class="col-lg-1 font-weight-bold" i18n>Applied</div>
-      <div class="col-lg-2 font-weight-bold" i18n>Depth</div>
-      <div class="col-lg-2 font-weight-bold" i18n>Grantable</div>
+<ng-container *ngIf="!loading">
+  <div class="d-flex w-100 mt-2 mb-2">
+    <div class="ml-auto">
+      <button class="btn btn-success" (click)="save()" 
+        [disabled]="cannotSave()" i18n>Apply Changes</button>
     </div>
-    <div class="row pt-1 pb-1" *ngFor="let perm of allPerms">
-      <div class="col-lg-5">{{perm.code()}}</div>
-      <div class="col-lg-1">
-        <input class="form-check-input ml-0 pl-0" type="checkbox"
-          [disabled]="!canGrantPerm(perm)"
-          [(ngModel)]="permsApplied[perm.id()]"/>
-      </div>
-      <div class="col-lg-2">
-        <select class="form-control" [(ngModel)]="permDepths[perm.id()]">
-          <ng-container *ngFor="let depth of orgDepths">
-            <option [disabled]="depth < canGrantPermAtDepth(perm)" 
-              [value]="depth">{{depth}}</option>
-          </ng-container>
-        </select>
-      </div>
-      <div class="col-lg-2">
-        <input class="form-check-input ml-0 pl-0" type="checkbox"
-          [disabled]="!canGrantPerm(perm)"
-          [(ngModel)]="permsGrantable[perm.id()]"/>
-      </div>
+  </div>
+  <div [ngbNavOutlet]="permsNav"></div>
+  <div class="d-flex w-100 mb-2 mt-2">
+    <div class="ml-auto">
+      <button class="btn btn-success" (click)="save()" 
+        [disabled]="cannotSave()" i18n>Apply Changes</button>
     </div>
   </div>
 </ng-container>
 
 
+
index cad6c8b..0e8eec6 100644 (file)
@@ -1,6 +1,7 @@
 import {Component, ViewChild, Input, OnInit, AfterViewInit} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {tap} from 'rxjs/operators';
+import {Observable} from 'rxjs';
+import {concatMap, tap, finalize} from 'rxjs/operators';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
@@ -20,23 +21,16 @@ import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.componen
 export class PatronPermsComponent implements OnInit, AfterViewInit {
 
     @Input() patronId: number;
-    workOuMaps: IdlObject[];
-    workOuSelector: {[orgId: number]: boolean} = {};
+
+    myPermMaps:  {[permId: number]: IdlObject} = {}
+    userPermMaps: {[permId: number]: IdlObject} = {};
+    userWorkOuMaps: {[orgId: number]: IdlObject} = {};
+
     workableOrgs: IdlObject[] = [];
     canAssignWorkOrgs: {[orgId: number]: boolean} = {};
-    myPermMaps: IdlObject[] = [];
-    userPermMaps: IdlObject[] = [];
     allPerms: IdlObject[] = [];
     loading = true;
 
-    permsApplied: {[id: number]: boolean} = {};
-    permDepths: {[id: number]: number} = {};
-    permsGrantable: {[id: number]: boolean} = {};
-
-    myPermsApplied: {[id: number]: boolean} = {};
-    myPermDepths: {[id: number]: number} = {};
-    myPermsGrantable: {[id: number]: boolean} = {};
-
     orgDepths: number[];
 
     @ViewChild('progress') private progress: ProgressInlineComponent;
@@ -59,23 +53,11 @@ export class PatronPermsComponent implements OnInit, AfterViewInit {
         const depths = {};
         this.org.list().forEach(org => depths[org.ou_type().depth()] = true);
         this.orgDepths = Object.keys(depths).map(d => Number(d)).sort();
-
     }
 
     ngAfterViewInit() {
 
-        this.progress.update({max: 9, value: 0});
-
-        this.net.request(
-            'open-ils.actor',
-            'open-ils.actor.user.get_work_ous',
-            this.auth.token(), this.patronId).toPromise()
-
-        .then(maps => {
-            this.progress.increment();
-            this.workOuMaps = maps;
-            maps.forEach(map => this.workOuSelector[map.work_ou()] = true);
-        })
+        return this.reload().toPromise()
 
         .then(_ => { // All permissions
             this.progress.increment();
@@ -83,24 +65,6 @@ export class PatronPermsComponent implements OnInit, AfterViewInit {
             .pipe(tap(perm => this.allPerms.push(perm))).toPromise();
         })
 
-        .then(_ => { // Target user permissions
-            this.progress.increment();
-            return this.net.request(
-                'open-ils.actor',
-                'open-ils.actor.permissions.user_perms.retrieve',
-                this.auth.token(), this.patronId).toPromise()
-            .then(maps => {
-                this.progress.increment();
-                this.userPermMaps = maps;
-                maps.forEach(m => {
-                    this.permsApplied[m.perm()] = true;
-                    this.permDepths[m.perm()] = m.depth();
-                    this.permsGrantable[m.perm()] = m.grantable() === 't';
-
-                });
-            });
-        })
-
         .then(_ => { // My permissions
             this.progress.increment();
             return this.net.request(
@@ -108,12 +72,8 @@ export class PatronPermsComponent implements OnInit, AfterViewInit {
                 'open-ils.actor.permissions.user_perms.retrieve',
                 this.auth.token()).toPromise()
             .then(maps => {
-                this.myPermMaps = maps
-                maps.forEach(m => {
-                    this.myPermsApplied[m.perm()] = true;
-                    this.myPermDepths[m.perm()] = m.depth();
-                    this.myPermsGrantable[m.perm()] = m.grantable() === 't';
-                });
+                this.progress.increment();
+                maps.forEach(m => this.myPermMaps[m.perm()] = m);
             });
         })
 
@@ -131,21 +91,158 @@ export class PatronPermsComponent implements OnInit, AfterViewInit {
         .then(_ => this.loading = false);
     }
 
+    reload(): Observable<any> {
+
+        this.userWorkOuMaps = {};
+        this.userPermMaps = {};
+        this.progress.increment();
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.get_work_ous',
+            this.auth.token(), this.patronId
+
+        ).pipe(tap(maps => {
+            this.progress.increment();
+            maps.forEach(map => this.userWorkOuMaps[map.work_ou()] = map);
+
+        })).pipe(concatMap(_ => { // User perm maps
+            return this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.permissions.user_perms.retrieve',
+                this.auth.token(), this.patronId
+            );
+
+        })).pipe(tap(maps => {
+            this.progress.increment();
+            maps.forEach(m => this.userPermMaps[m.perm()] = m);
+        }));
+    }
+
+    userHasWorkOu(orgId: number): boolean {
+        return (
+            this.userWorkOuMaps[orgId] &&
+            !this.userWorkOuMaps[orgId].isdeleted()
+        );
+    }
+
+    userWorkOuChange(orgId: number, applied: boolean) {
+        const map = this.userWorkOuMaps[orgId];
+
+        if (map) {
+            map.isdeleted(!applied);
+        } else {
+            const newMap = this.idl.create('puwoum');
+            newMap.isnew(true);
+            newMap.usr(this.patronId);
+            newMap.work_ou(orgId);
+            this.userWorkOuMaps[orgId] = newMap;
+        }
+    }
+
     canGrantPerm(perm: IdlObject): boolean {
-        return this.myPermsGrantable[perm.id()]
-            || this.auth.user().super_user() === 't';
+        if (this.auth.user().super_user() === 't') { return true; }
+        const map = this.myPermMaps[perm.id()];
+        return map && Number(map.grantable()) === 1;
     }
 
     canGrantPermAtDepth(perm: IdlObject): number {
         if (this.auth.user().super_user() === 't') {
             return this.org.root().ou_type().depth();
         } else {
-            return this.myPermDepths[perm.id()];
+            const map = this.myPermMaps[perm.id()];
+            return map ? map.depth() : NaN;
         }
     }
 
+    userHasPerm(perm: IdlObject): boolean {
+        return (
+            this.userPermMaps[perm.id()] &&
+            !this.userPermMaps[perm.id()].isdeleted()
+        );
+    }
+
+    findOrCreatePermMap(perm: IdlObject): IdlObject {
+        if (this.userPermMaps[perm.id()]) {
+            return this.userPermMaps[perm.id()];
+        }
+
+        const map = this.idl.create('pupm');
+        map.isnew(true);
+        map.usr(this.patronId);
+        map.perm(perm.id());
+        return this.userPermMaps[perm.id()] = map;
+    }
+
+    permApplyChanged(perm: IdlObject, applied: boolean) {
+        const map = this.findOrCreatePermMap(perm);
+        map.isdeleted(!applied);
+        map.ischanged(true);
+    }
+
+    userHasPermAtDepth(perm: IdlObject): number {
+        if (this.userPermMaps[perm.id()]) {
+            return this.userPermMaps[perm.id()].depth();
+        } else {
+            return null;
+        }
+    }
+
+    permDepthChanged(perm: IdlObject, depth: number) {
+        const map = this.findOrCreatePermMap(perm);
+        map.depth(depth);
+        map.ischanged(true);
+    }
+
+    userPermIsGrantable(perm: IdlObject): boolean {
+        return (
+            this.userPermMaps[perm.id()] &&
+            Number(this.userPermMaps[perm.id()].grantable()) === 1
+        );
+    }
+
+    grantableChanged(perm: IdlObject, grantable: boolean) {
+        const map = this.findOrCreatePermMap(perm);
+        map.grantable(grantable ? 1 : 0); // API uses 1/0 not t/f
+        map.ischanged(true);
+    }
+
     save() {
-        // open-ils.actor.user.work_ous.update
+        this.loading = true;
+
+        // Scrub the unmodified values to avoid sending a huge pile
+        // of perms especially.
+
+        const ouMaps = Object.values(this.userWorkOuMaps)
+            .filter(map => map.isnew() || map.ischanged() || map.isdeleted());
+
+        const permMaps = Object.values(this.userPermMaps)
+            .filter(map => map.isnew() || map.ischanged() || map.isdeleted());
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.work_ous.update',
+            this.auth.token(), ouMaps
+
+        ).pipe(concatMap(_ => {
+
+            this.progress.reset();
+            this.progress.increment();
+
+            return this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.user.permissions.update',
+                this.auth.token(), permMaps
+            )
+        }))
+        .pipe(concatMap(_ => this.reload()))
+        .pipe(finalize(() => this.loading = false)).subscribe();
+    }
+
+    cannotSave(): boolean {
+        return Object.values(this.userPermMaps)
+            .filter(map => map.depth() === null || map.depth() === undefined)
+            .length > 0;
     }
 }