LP#1996818 Issues Placing Holds from the Patron Record user/dbriem/lp1996818_broadcast_patron_hold_target
authorDan Briem <dbriem@wlsmail.org>
Fri, 26 May 2023 16:24:54 +0000 (16:24 +0000)
committerDan Briem <dbriem@wlsmail.org>
Fri, 26 May 2023 16:24:54 +0000 (16:24 +0000)
Set the hold target using the same cookie Angular uses when
placing holds from AngularJS patron records.

Clear the cookie and broadcast to all catalog tabs to remove
the hold target when:
- the Clear button for the hold target is pressed
- the hold interface loads a different patron
- a different Angular route loads
- AngularJS app starts (left the Angular context)

When a catalog tab is closed, clear the cookie and broadcast
it so that any open catalog tabs can restore it.

To test:
1. After loading the patch, build Angular and AngularJS
2. Place a hold from AngJS patron record, note target is set
3. Open multiple catalog tabs
5. Close one catalog tab, note target persists in other tabs
6. Click Clear button, note target is cleared in all tabs
7. Repeat steps 2-3, load a different patron in the hold
   interface, note target is cleared in all tabs
8. Repeat steps 2-3, click the home icon in the navbar, note
   target is cleared in all tabs
9. Repeat steps 2-3, click AngJS Check Out in the navbar,
   note target is cleared in all tabs

Signed-off-by: Dan Briem <dbriem@wlsmail.org>
Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
Open-ILS/web/js/ui/default/staff/services/startup.js

index 340a28d..db45f8a 100644 (file)
@@ -1,12 +1,15 @@
-import {Component, OnInit} from '@angular/core';
+import {Component, HostListener, OnDestroy, OnInit} from '@angular/core';
 import {IdlObject} from '@eg/core/idl.service';
 import {StaffCatalogService} from './catalog.service';
 import {BasketService} from '@eg/share/catalog/basket.service';
+import {Subject, takeUntil} from 'rxjs';
 
 @Component({
   templateUrl: 'catalog.component.html'
 })
-export class CatalogComponent implements OnInit {
+export class CatalogComponent implements OnInit, OnDestroy {
+
+    private onDestroy = new Subject<void>();
 
     constructor(
         private basket: BasketService,
@@ -19,6 +22,12 @@ export class CatalogComponent implements OnInit {
         // reset and updated as needed to apply new search parameters.
         this.staffCat.createContext();
 
+        // listen for hold patron target changes from other tabs
+        // until there's a route change
+        this.staffCat.onChangeHoldPatron().pipe(
+            takeUntil(this.onDestroy)
+        ).subscribe();
+
         // Subscribe to these emissions so that we can force
         // change detection in this component even though the
         // hold-for value was modified by a child component.
@@ -34,5 +43,15 @@ export class CatalogComponent implements OnInit {
     clearHoldPatron() {
         this.staffCat.clearHoldPatron();
     }
+
+    @HostListener('window:beforeunload')
+    onBeforeUnload(): void {
+        this.staffCat.onBeforeUnload();
+    }
+
+    ngOnDestroy(): void {
+        this.clearHoldPatron();
+        this.onDestroy.next();
+    }
 }
 
index 24d32df..eaff89e 100644 (file)
@@ -1,4 +1,4 @@
-import {Injectable, EventEmitter} from '@angular/core';
+import {Injectable, EventEmitter, NgZone} from '@angular/core';
 import {Router, ActivatedRoute} from '@angular/router';
 import {IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
@@ -8,6 +8,8 @@ import {CatalogSearchContext} from '@eg/share/catalog/search-context';
 import {BibRecordSummary} from '@eg/share/catalog/bib-record.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
 import {StoreService} from '@eg/core/store.service';
+import {BroadcastService} from '@eg/share/util/broadcast.service';
+import {Observable, tap} from 'rxjs';
 
 const HOLD_FOR_PATRON_KEY = 'eg.circ.patron_hold_target';
 
@@ -70,7 +72,9 @@ export class StaffCatalogService {
         private org: OrgService,
         private cat: CatalogService,
         private patron: PatronService,
-        private catUrl: CatalogUrlService
+        private catUrl: CatalogUrlService,
+        private broadcaster: BroadcastService,
+        private zone: NgZone
     ) { }
 
     createContext(): void {
@@ -99,11 +103,50 @@ export class StaffCatalogService {
         this.applySearchDefaults();
     }
 
-    clearHoldPatron() {
+    clearHoldPatron(broadcast: boolean = true) {
+        const removedTarget = this.holdForBarcode;
+
         this.holdForUser = null;
         this.holdForBarcode = null;
         this.store.removeLoginSessionItem(HOLD_FOR_PATRON_KEY);
         this.holdForChange.emit();
+        if (!broadcast) return;
+
+        // clear hold patron on other tabs
+        this.broadcaster.broadcast(
+            HOLD_FOR_PATRON_KEY, { removedTarget }
+        );
+    }
+
+    onBeforeUnload(): void {
+        const closedTarget = this.holdForBarcode;
+        if (closedTarget) {
+            this.clearHoldPatron(false);
+            this.broadcaster.broadcast(HOLD_FOR_PATRON_KEY,
+                { closedTarget }
+            );
+        }
+    }
+
+    onChangeHoldPatron(): Observable<any> {
+        return this.broadcaster.listen(HOLD_FOR_PATRON_KEY).pipe(
+            tap(({ removedTarget, closedTarget }) => {
+                if (removedTarget && this.holdForBarcode) {
+                    // broadcaster doesn't trigger change detection,
+                    // so trigger it manually
+                    this.zone.run(() => this.clearHoldPatron(false));
+
+                } else if (closedTarget) {
+                    // if hold target was unset by another tab,
+                    // restore the hold target
+                    if (closedTarget === this.holdForBarcode) {
+                        this.store.setLoginSessionItem(
+                            HOLD_FOR_PATRON_KEY, closedTarget
+                        );
+                    }
+                }
+            })
+        );
     }
 
     cloneContext(context: CatalogSearchContext): CatalogSearchContext {
index 3173bfd..795455d 100644 (file)
@@ -146,7 +146,7 @@ function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,
 
     $scope.place_hold = function() {
 
-        egCore.hatch.setLocalItem(
+        egCore.hatch.setLoginSessionItem(
             'eg.circ.patron_hold_target', patronSvc.current.card().barcode());
 
         $window.location.href = '/eg2/staff/catalog';
index 8592d07..4a242c3 100644 (file)
@@ -86,6 +86,29 @@ function($q,  $rootScope,  $location,  $window,  egIDL,  egAuth,  egEnv , egOrg
     // of the startup routines when no valid token exists during startup.
     $rootScope.$on('egAuthExpired', function() {service.expiredAuthHandler()});
 
+    // in case we just left an Angular context, clear the hold target
+    // and notify any open Angular catalog tabs to do the same
+    function clearHoldTarget() {
+        var patronHoldTarget = $cookies.get(
+            'eg.circ.patron_hold_target'
+        );
+        if (patronHoldTarget) {
+            $cookies.remove(
+                'eg.circ.patron_hold_target'
+            )
+            if (typeof BroadcastChannel !== 'undefined') {
+                var broadcaster = new BroadcastChannel(
+                    'eg.circ.patron_hold_target'
+                );
+                broadcaster.postMessage(
+                    { removedTarget: patronHoldTarget }
+                );
+                broadcaster.close();
+            }
+        }
+    }
+    clearHoldTarget();
+
     service.go = function () {
         if (service.promise) {
             // startup already started, return our existing promise