LH#17: warn if attempting to leave provider edits in flight
authorGalen Charlton <gmc@equinoxinitiative.org>
Tue, 1 Sep 2020 02:40:52 +0000 (22:40 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Tue, 1 Sep 2020 02:40:52 +0000 (22:40 -0400)
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.html
Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-record.service.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/routing.module.ts

index a480646..c96092f 100644 (file)
@@ -3,6 +3,12 @@
 <eg-string #createString i18n-text text="New Provider Added"></eg-string>
 <eg-string #createErrString i18n-text text="Failed to Create New Provider"></eg-string>
 
+<eg-confirm-dialog #leaveConfirm
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Unsaved Changes Warning"
+  dialogBody="There are unsaved changes. Are you sure you want to leave?">
+</eg-confirm-dialog>
+
 <div><div class="row">
 
 <div class="col">
@@ -92,7 +98,7 @@
                 (click)="setDefaultTab()" i18n>Set Default View</button>
             </div>
           </div>
-          <eg-provider-holdings></eg-provider-holdings>
+          <eg-provider-holdings #providerHoldings></eg-provider-holdings>
         </ng-template>
       </ngb-tab>
       <ngb-tab title="EDI" i18n-title id="edi_accounts" [disabled]="!id || !this.providerRecord.currentProvider.canAdmin">
index a3aa44f..708819c 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, AfterViewInit, ViewChild, ChangeDetectorRef, OnDestroy} from '@angular/core';
 import {filter, takeUntil} from 'rxjs/operators';
-import {Subject} from 'rxjs';
+import {Subject, Observable, of} from 'rxjs';
 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {Router, ActivatedRoute, ParamMap, RouterEvent, NavigationEnd} from '@angular/router';
 import {StaffCommonModule} from '@eg/staff/common.module';
@@ -8,6 +8,7 @@ import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {AcqProviderSummaryPaneComponent} from './summary-pane.component';
 import {ProviderDetailsComponent} from './provider-details.component';
+import {ProviderHoldingsComponent} from './provider-holdings.component';
 import {ProviderResultsComponent} from './provider-results.component';
 import {ProviderRecordService} from './provider-record.service';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
@@ -15,6 +16,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {AuthService} from '@eg/core/auth.service';
 import {StoreService} from '@eg/core/store.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 
 @Component({
   templateUrl: './acq-provider.component.html'
@@ -30,9 +32,11 @@ export class AcqProviderComponent implements OnInit, AfterViewInit {
     @ViewChild('acqSearchProviderSummary', { static: true }) providerSummaryPane: AcqProviderSummaryPaneComponent;
     @ViewChild('acqProviderResults', { static: true }) acqProviderResults: ProviderResultsComponent;
     @ViewChild('providerDetails', { static: false }) providerDetails: ProviderDetailsComponent;
+    @ViewChild('providerHoldings', { static: false }) providerHoldings: ProviderHoldingsComponent;
     @ViewChild('createDialog', { static: true }) createDialog: FmRecordEditorComponent;
     @ViewChild('createString', { static: false }) createString: StringComponent;
     @ViewChild('createErrString', { static: false }) createErrString: StringComponent;
+    @ViewChild('leaveConfirm', { static: true }) leaveConfirm: ConfirmDialogComponent;
 
     onTabChange: ($event: NgbTabChangeEvent) => void;
 
@@ -41,6 +45,7 @@ export class AcqProviderComponent implements OnInit, AfterViewInit {
 
     previousUrl: string = null;
     public destroyed = new Subject<any>();
+    _alreadyDeactivated = false;
 
     constructor(
         private router: Router,
@@ -99,11 +104,16 @@ export class AcqProviderComponent implements OnInit, AfterViewInit {
         }
 
         this.onTabChange = ($event) => {
-            if (this.validTabTypes.includes($event.nextId)) {
-                this.activeTab = $event.nextId;
-                const id = this.route.snapshot.paramMap.get('id');
-                this.router.navigate(['/staff', 'acq', 'provider', this.id, $event.nextId]);
-            }
+            $event.preventDefault();
+            this.canDeactivate().subscribe(canLeave => {
+                if (!canLeave) { return; }
+                this._alreadyDeactivated = true; // don't trigger again on the route change
+                if (this.validTabTypes.includes($event.nextId)) {
+                    this.activeTab = $event.nextId;
+                    const id = this.route.snapshot.paramMap.get('id');
+                    this.router.navigate(['/staff', 'acq', 'provider', this.id, $event.nextId]);
+                }
+            });
         };
 
         this.onDesireSummarize = ($event, updateSummaryOnly = false) => {
@@ -165,4 +175,18 @@ export class AcqProviderComponent implements OnInit, AfterViewInit {
             }
         );
     }
+
+    canDeactivate(): Observable<boolean> {
+        if (this._alreadyDeactivated) {
+            // one freebie
+            this._alreadyDeactivated = false;
+            return of(true);
+        }
+        if ((this.providerHoldings && this.providerHoldings.isDirty()) ||
+            (this.providerDetails && this.providerDetails.isDirty())) {
+            return this.leaveConfirm.open();
+        } else {
+            return of(true);
+        }
+    }
 }
index df3158c..2261a16 100644 (file)
@@ -20,6 +20,7 @@ export class ProviderDetailsComponent implements OnInit {
     @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
     @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
     @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
+    @ViewChild('editDialog', { static: false}) editDialog: FmRecordEditorComponent;
 
     provider: IdlObject;
 
@@ -75,4 +76,7 @@ export class ProviderDetailsComponent implements OnInit {
         return this.providerRecord.currentProviderRecord().canAdmin ? 'update' : 'view';
     }
 
+    isDirty(): boolean {
+        return (this.editDialog) ? this.editDialog.isDirty() : false;
+    }
 }
index d79a954..8923c94 100644 (file)
@@ -1,4 +1,5 @@
 import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core';
+import {NgForm} from '@angular/forms';
 import {empty, throwError, Observable, from, Subscription} from 'rxjs';
 import {map} from 'rxjs/operators';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
@@ -35,6 +36,7 @@ export class ProviderHoldingsComponent implements OnInit, AfterViewInit {
     @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
     @ViewChild('successTagString', { static: true }) successTagString: StringComponent;
     @ViewChild('updateFailedTagString', { static: false }) updateFailedTagString: StringComponent;
+    @ViewChild('holdingTagForm', { static: false}) holdingTagForm: NgForm;
 
     cellTextGenerator: GridCellTextGenerator;
     provider: IdlObject;
@@ -94,7 +96,12 @@ export class ProviderHoldingsComponent implements OnInit, AfterViewInit {
     }
 
     ngAfterViewInit() {
-        console.log('this.providerRecord',this.providerRecord);
+        if (this.providerRecord.current()) {
+            // sometimes needs to force a refresh in case we updated that tag,
+            // navigated away (and confirmed that we wanted to abandon the change),
+            // then navigated back
+            this.providerRecord.current()['_holding_tag'] = this.providerRecord.current().holding_tag();
+        }
     }
 
  
@@ -209,4 +216,9 @@ export class ProviderHoldingsComponent implements OnInit, AfterViewInit {
             }
         );
     }
+
+    isDirty() : boolean {
+        return (this.providerRecord.current()['_holding_tag'] == this.providerRecord.current().holding_tag()) ? false :
+               (this.holdingTagForm && this.holdingTagForm.dirty) ? this.holdingTagForm.dirty : false;
+    }
 }
index be775f0..e16a9d5 100644 (file)
@@ -72,6 +72,8 @@ export class ProviderRecordService {
             {}
         ).pipe(map(acqpro => {
             const provider = new ProviderRecord(acqpro);
+            // make a copy of holding_tag for use by the holdings definitions tab
+            acqpro['_holding_tag'] = acqpro.holding_tag();
             acqpro.edi_accounts().forEach(acct => {
                 acct['_is_default'] = false;
                 if (acqpro.edi_default()) {
index f1f0de5..3020091 100644 (file)
@@ -1,6 +1,7 @@
 import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
 import {Router, Resolve, RouterStateSnapshot,
-        ActivatedRouteSnapshot} from '@angular/router';
+        ActivatedRouteSnapshot, CanDeactivate} from '@angular/router';
 import {ProviderRecordService} from './provider-record.service';
 
 @Injectable()
@@ -36,3 +37,14 @@ export class ProviderResolver implements Resolve<Promise<any[]>> {
 
 }
 
+// following example of https://www.concretepage.com/angular-2/angular-candeactivate-guard-example
+export interface DeactivationGuarded {
+    canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
+}
+
+@Injectable()
+export class CanLeaveAcqProviderGuard implements CanDeactivate<DeactivationGuarded> {
+    canDeactivate(component: DeactivationGuarded):  Observable<boolean> | Promise<boolean> | boolean {
+        return component.canDeactivate ? component.canDeactivate() : true;
+    }
+}
index 8bd2b6a..c8e0686 100644 (file)
@@ -1,7 +1,7 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {AcqProviderComponent} from './acq-provider.component';
-import {ProviderResolver} from './resolver.service';
+import {ProviderResolver, CanLeaveAcqProviderGuard} from './resolver.service';
 
 const routes: Routes = [
   { path: '',
@@ -16,6 +16,7 @@ const routes: Routes = [
   { path: ':id/:tab',
     component: AcqProviderComponent,
     resolve: { providerResolver : ProviderResolver },
+    canDeactivate: [CanLeaveAcqProviderGuard],
     runGuardsAndResolvers: 'always'
   }
 ];
@@ -23,7 +24,7 @@ const routes: Routes = [
 @NgModule({
   imports: [RouterModule.forChild(routes)],
   exports: [RouterModule],
-  providers: [ProviderResolver]
+  providers: [ProviderResolver, CanLeaveAcqProviderGuard]
 })
 
 export class AcqProviderRoutingModule {}