LP1860460 Copy delete override repairs, perm failed handler
authorBill Erickson <berickxx@gmail.com>
Tue, 21 Jan 2020 21:06:01 +0000 (16:06 -0500)
committerJane Sandberg <sandbej@linnbenton.edu>
Sun, 23 Feb 2020 15:36:06 +0000 (07:36 -0800)
* Teach the Angular holdings module vol/copy delete dialog to correctly
  report failure events to the user and handle permission overrides.

* Add support for automatically launching the op-change dialog when a
  permission failed event is returned by an API call for any /eg2/staff/
  interface.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jennifer Weston <jennifer.weston@equinoxinitiative.org>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/core/auth.service.ts
Open-ILS/src/eg2/src/app/core/net.service.ts
Open-ILS/src/eg2/src/app/staff/nav.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/delete-volcopy-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/holdings/delete-volcopy-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts

index a173d63..9ad471f 100644 (file)
@@ -130,6 +130,11 @@ export class AuthService {
         let service = 'open-ils.auth';
         let method = 'open-ils.auth.login';
 
+        if (isOpChange && this.opChangeIsActive()) {
+            // Enforce one op-change at a time.
+            this.undoOpChange();
+        }
+
         return this.net.request(
             'open-ils.auth_proxy',
             'open-ils.auth_proxy.enabled')
index dd2bebb..42dae19 100644 (file)
@@ -73,7 +73,7 @@ export class NetService {
     permFailed$: EventEmitter<NetRequest>;
     authExpired$: EventEmitter<AuthExpiredEvent>;
 
-    // If true, permission failures are emitted via permFailed$
+    // If true, permission failures are emitted via permFailed
     // and the active request is marked as superseded.
     permFailedHasHandler: Boolean = false;
 
index f143727..5627f76 100644 (file)
@@ -1,12 +1,15 @@
-import {Component, OnInit, ViewChild} from '@angular/core';
+import {Component, OnInit, OnDestroy, ViewChild} from '@angular/core';
 import {ActivatedRoute, Router} from '@angular/router';
 import {Location} from '@angular/common';
+import {Subscription} from 'rxjs';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {LocaleService} from '@eg/core/locale.service';
 import {PrintService} from '@eg/share/print/print.service';
 import {StoreService} from '@eg/core/store.service';
+import {NetRequest, NetService} from '@eg/core/net.service';
+import {OpChangeComponent} from '@eg/staff/share/op-change/op-change.component';
 
 @Component({
     selector: 'eg-staff-nav-bar',
@@ -14,7 +17,7 @@ import {StoreService} from '@eg/core/store.service';
     templateUrl: 'nav.component.html'
 })
 
-export class StaffNavComponent implements OnInit {
+export class StaffNavComponent implements OnInit, OnDestroy {
 
     // Locales that have Angular staff translations
     locales: any[];
@@ -23,9 +26,13 @@ export class StaffNavComponent implements OnInit {
     // When active, show a link to the experimental Angular staff catalog
     showAngularCatalog: boolean;
 
+    @ViewChild('navOpChange', {static: false}) opChange: OpChangeComponent;
+    permFailedSub: Subscription;
+
     constructor(
         private router: Router,
         private store: StoreService,
+        private net: NetService,
         private org: OrgService,
         private auth: AuthService,
         private pcrud: PcrudService,
@@ -54,6 +61,19 @@ export class StaffNavComponent implements OnInit {
             .then(settings => this.showAngularCatalog =
                 Boolean(settings['ui.staff.angular_catalog.enabled']));
         }
+
+        // Wire up our op-change component as the general purpose
+        // permission failed handler.
+        this.net.permFailedHasHandler = true;
+        this.permFailedSub =
+            this.net.permFailed$.subscribe(
+                (req: NetRequest) => this.opChange.escalateRequest(req));
+    }
+
+    ngOnDestroy() {
+        if (this.permFailedSub) {
+            this.permFailedSub.unsubscribe();
+        }
     }
 
     user() {
index 5bde3a7..f7709cd 100644 (file)
@@ -1,33 +1,36 @@
 
-
 <eg-string #successMsg
     text="Successfully Holdings" i18n-text></eg-string>
 <eg-string #errorMsg 
     text="Failed To Delete Holdings" i18n-text></eg-string>
 
+<eg-confirm-dialog #confirmOverride
+  i18n-dialogTitle dialogTitle="One or more items could not be deleted. Override?"
+  i18n-dialogBody dialogBody="Reason(s) include: {{deleteEventDesc}}">
+</eg-confirm-dialog>
 
 <ng-template #dialogContent>
-    <div class="modal-header bg-info">
-      <h4 class="modal-title">
-        <span i18n>Delete Holdings</span>
-      </h4>
-      <button type="button" class="close" 
-        i18n-aria-label aria-label="Close" (click)="close()">
-        <span aria-hidden="true">&times;</span>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>Delete Holdings</span>
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <p i18n>Delete {{numCallNums}} call numbers and {{numCopies}} copies?</p>
+  </div>
+  <div class="modal-footer">
+    <ng-container>
+      <button type="button" class="btn btn-warning" 
+        (click)="close()" i18n>Cancel</button>
+      <button type="button" class="btn btn-success" 
+        (click)="deleteHoldings()" i18n>
+        Delete Holdings
       </button>
-    </div>
-    <div class="modal-body">
-      <p i18n>Delete {{numCallNums}} call numbers and {{numCopies}} copies?</p>
-    </div>
-    <div class="modal-footer">
-      <ng-container>
-        <button type="button" class="btn btn-warning" 
-          (click)="close()" i18n>Cancel</button>
-        <button type="button" class="btn btn-success" 
-          (click)="deleteHoldings()" i18n>
-          Delete Holdings
-        </button>
-      </ng-container>
-    </div>
-  </ng-template>
-  
+    </ng-container>
+  </div>
+</ng-template>
+
index 3941d34..76fbd58 100644 (file)
@@ -2,13 +2,14 @@ import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core';
 import {Observable, throwError} from 'rxjs';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
-import {EventService} from '@eg/core/event.service';
+import {EgEvent, EventService} from '@eg/core/event.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {AuthService} from '@eg/core/auth.service';
 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {StringComponent} from '@eg/share/string/string.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 
 
 /**
@@ -38,6 +39,7 @@ export class DeleteHoldingDialogComponent
     numCopies: number;
     numSucceeded: number;
     numFailed: number;
+    deleteEventDesc: string;
 
     @ViewChild('successMsg', { static: true })
         private successMsg: StringComponent;
@@ -45,6 +47,9 @@ export class DeleteHoldingDialogComponent
     @ViewChild('errorMsg', { static: true })
         private errorMsg: StringComponent;
 
+    @ViewChild('confirmOverride', {static: false})
+        private confirmOverride: ConfirmDialogComponent;
+
     constructor(
         private modal: NgbModal, // required for passing to parent
         private toast: ToastService,
@@ -89,23 +94,28 @@ export class DeleteHoldingDialogComponent
         return super.open(args);
     }
 
-    deleteHoldings() {
+    deleteHoldings(override?: boolean) {
+
+        this.deleteEventDesc = '';
 
-        const flags = {
+        const flags: any = {
             force_delete_copies: this.forceDeleteCopies
         };
 
+        let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
+        if (override) {
+            method = `${method}.override`;
+            flags.events = ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'];
+        }
+
         this.net.request(
-            'open-ils.cat',
-            'open-ils.cat.asset.volume.fleshed.batch.update.override',
+            'open-ils.cat', method,
             this.auth.token(), this.callNums, 1, flags
         ).toPromise().then(
             result => {
                 const evt = this.evt.parse(result);
                 if (evt) {
-                    console.warn(evt);
-                    this.errorMsg.current().then(msg => this.toast.warning(msg));
-                    this.numFailed++;
+                    this.handleDeleteEvent(evt, override);
                 } else {
                     this.numSucceeded++;
                     this.close(this.numSucceeded > 0);
@@ -118,6 +128,29 @@ export class DeleteHoldingDialogComponent
             }
         );
     }
+
+    handleDeleteEvent(evt: EgEvent, override?: boolean): Promise<any> {
+
+        if (override) { // override failed
+            console.warn(evt);
+            this.numFailed++;
+            return this.errorMsg.current().then(msg => this.toast.warning(msg));
+        }
+
+        this.deleteEventDesc = evt.desc;
+
+        return this.confirmOverride.open().toPromise().then(confirmed => {
+            if (confirmed) {
+                return this.deleteHoldings(true);
+
+            } else {
+                // User canceled the delete confirmation dialog
+                this.numFailed++;
+                this.errorMsg.current().then(msg => this.toast.warning(msg));
+                this.close(this.numSucceeded > 0);
+            }
+        });
+    }
 }
 
 
index 95d4db8..7b854ab 100644 (file)
@@ -3,6 +3,7 @@ import {ToastService} from '@eg/share/toast/toast.service';
 import {AuthService} from '@eg/core/auth.service';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {NetRequest, NetService} from '@eg/core/net.service';
 
 @Component({
   selector: 'eg-op-change',
@@ -19,16 +20,18 @@ export class OpChangeComponent
     @Input() successMessage: string;
     @Input() failMessage: string;
 
+    requestToEscalate: NetRequest;
+
     constructor(
         private modal: NgbModal, // required for passing to parent
         private renderer: Renderer2,
         private toast: ToastService,
+        private net: NetService,
         private auth: AuthService) {
         super(modal);
     }
 
     ngOnInit() {
-
         // Focus the username any time the dialog is opened.
         this.onOpen$.subscribe(
             val => this.renderer.selectRootElement('#username').focus()
@@ -56,6 +59,10 @@ export class OpChangeComponent
                     ok2 => {
                         this.close();
                         this.toast.success(this.successMessage);
+                        if (this.requestToEscalate) {
+                            // Allow a breath for the dialog to clean up.
+                            setTimeout(() => this.sendEscalatedRequest());
+                        }
                     }
                 );
             },
@@ -72,6 +79,37 @@ export class OpChangeComponent
             err => this.toast.danger(this.failMessage)
         );
     }
+
+    escalateRequest(req: NetRequest) {
+        this.requestToEscalate = req;
+        this.open({});
+    }
+
+    // Resend a net request using the credentials just created
+    // via operator change.
+    sendEscalatedRequest() {
+        const sourceReq = this.requestToEscalate;
+        delete this.requestToEscalate;
+
+        console.debug('Op-Change escalating request', sourceReq);
+
+        // Clone the source request, modifying the params to
+        // use the op-change'd authtoken
+        const req = new NetRequest(
+            sourceReq.service,
+            sourceReq.method,
+            [this.auth.token()].concat(sourceReq.params.splice(1))
+        );
+
+        // Relay responses received for our escalated request to
+        // the caller via the original request observer.
+        this.net.requestCompiled(req)
+        .subscribe(
+            res => sourceReq.observer.next(res),
+            err => sourceReq.observer.error(err),
+            ()  => sourceReq.observer.complete()
+        ).add(_ => this.auth.undoOpChange());
+    }
 }