LP1904788 Browse paging show next/prev WIP
authorBill Erickson <berickxx@gmail.com>
Thu, 19 Nov 2020 18:02:19 +0000 (13:02 -0500)
committerBill Erickson <berickxx@gmail.com>
Thu, 19 Nov 2020 18:02:19 +0000 (13:02 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts

index 9b7d57a..14c52e9 100644 (file)
@@ -31,6 +31,7 @@ import {CnBrowseResultsComponent} from './cnbrowse/results.component';
 import {SearchTemplatesComponent} from './search-templates.component';
 import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
 import {PreferencesComponent} from './prefs.component';
+import {BrowsePagerComponent} from './result/browse-pager.component';
 
 @NgModule({
   declarations: [
@@ -56,7 +57,8 @@ import {PreferencesComponent} from './prefs.component';
     CnBrowseComponent,
     OpacViewComponent,
     PreferencesComponent,
-    CnBrowseResultsComponent
+    CnBrowseResultsComponent,
+    BrowsePagerComponent
   ],
   imports: [
     StaffCommonModule,
index a065274..52e1173 100644 (file)
@@ -44,6 +44,10 @@ export class StaffCatalogService {
     // Add digital bookplate to search options.
     enableBookplates = false;
 
+    // Cache of browse results so the browse pager is not forced to
+    // re-run the browse search on each navigation.
+    browsePagerResults: any[];
+
     constructor(
         private router: Router,
         private route: ActivatedRoute,
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.html
new file mode 100644 (file)
index 0000000..6b46379
--- /dev/null
@@ -0,0 +1,45 @@
+
+<div class="row mb-2 p-2 border border-info rounded">
+
+  <ng-container *ngIf="browseLoading">
+    <div class="col-lg-6 offset-lg-3">
+      <eg-progress-inline></eg-progress-inline>
+    </div>
+  </ng-container>
+
+  <ng-container *ngIf="!browseLoading">
+    <div class="col-lg-4">
+      <div *ngIf="prevEntry" class="d-flex">
+        <div class="">
+          <button class="btn btn-sm btn-outline-dark mr-2" 
+            (click)="goToBrowsePage(true)" i18n>Previous Heading</button>
+        </div>
+        <div class="flex-1 pt-1">
+          <span>{{prevEntry.value}}</span>
+        </div>
+      </div>
+    </div>
+    <div class="col-lg-4 d-flex justify-content-center">
+      <div class="pt-1">
+        Heading: <span class="font-weight-bold">
+          {{searchContext.termSearch.browseEntry.value()}}
+        </span>
+      </div>
+    </div>
+    <div class="col-lg-4">
+      <div *ngIf="nextEntry" class="float-right">
+        <div class="d-flex">
+          <div class="flex-1 pt-1">
+            <span>{{nextEntry.value}}</span>
+          </div>
+          <div class="">
+            <button class="btn btn-sm btn-outline-dark ml-2" 
+              (click)="goToBrowsePage(false)" i18n>Next Heading</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </ng-container>
+
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.ts
new file mode 100644 (file)
index 0000000..ed34719
--- /dev/null
@@ -0,0 +1,176 @@
+import {Component, OnInit, OnDestroy, Input} from '@angular/core';
+import {Observable, Subscription} from 'rxjs';
+import {tap, map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {BasketService} from '@eg/share/catalog/basket.service';
+
+@Component({
+  selector: 'eg-catalog-browse-pager',
+  templateUrl: 'browse-pager.component.html'
+})
+export class BrowsePagerComponent implements OnInit {
+
+    searchContext: CatalogSearchContext;
+    browseLoading = false;
+    prevEntry: any;
+    nextEntry: any;
+
+    constructor(
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+        this.fetchPageData().then(_ => this.setPrevNext());
+    }
+
+    fetchPageData(): Promise<any> {
+
+        let found = false;
+        const mbeId = this.pageEntryId();
+        this.staffCat.browsePagerResults.forEach(result => {
+            if (result.browse_entry === mbeId) {
+                found = true;
+            }
+        });
+
+        if (found) {
+            return Promise.resolve();
+        } else {
+            return this.fetchBrowseData(null);
+        }
+    }
+
+    // Grab a page of browse results
+    fetchBrowseData(prev: boolean): Promise<any> {
+        const ctx = this.searchContext.clone();
+        ctx.termSearch.hasBrowseEntry = null; // avoid term search
+
+        if (prev !== null) {
+            ctx.browseSearch.pivot = this.getBoundaryPivot(prev);
+        }
+
+        // Grab a few extras so we can last a bit longer before
+        // having to fetch more browse data.  Could make this bigger.
+        ctx.pager.limit = 20;
+
+        const results = [];
+        this.browseLoading = true;
+
+        return this.cat.browse(ctx)
+        .pipe(tap(result => results.push(result)))
+        .toPromise().then(_ => {
+            if (prev) {
+                this.staffCat.browsePagerResults =
+                    results.concat(this.staffCat.browsePagerResults);
+            } else {
+                this.staffCat.browsePagerResults =
+                    this.staffCat.browsePagerResults.concat(results);
+            }
+            this.browseLoading = false;
+        });
+    }
+
+    pageEntryId(): number {
+        return Number(
+            this.searchContext.termSearch.hasBrowseEntry.split(',')[0]
+        );
+    }
+
+    // Collect enough browse data to display previous, current, and
+    // next heading.  This can mean fetching an additional page of data.
+    setPrevNext(): Promise<any> {
+        let previous: any;
+        const mbeId = this.pageEntryId();
+
+        this.staffCat.browsePagerResults.forEach(result => {
+            // ignore pivot and authority-only entries
+            if (!result.sources) { return; }
+
+            if (previous) {
+                if (result.browse_entry === mbeId) {
+                    this.prevEntry = previous;
+                }
+                if (previous.browse_entry === mbeId) {
+                    this.nextEntry = result;
+                }
+            }
+            previous = result;
+        });
+
+        if (!this.prevEntry) {
+            return this.fetchBrowseData(true);
+        } else if (!this.nextEntry) {
+            return this.fetchBrowseData(false);
+        } else {
+            return Promise.resolve();
+        }
+    }
+
+    getBoundaryPivot(prev?: boolean): number {
+        const results = this.staffCat.browsePagerResults;
+        return prev ?  results[0].pivot_point :
+            results[results.length - 1].pivot_point;
+    }
+
+    setSearchPivot(prev?: boolean) {
+        // When traversing browse result page boundaries, modify the
+        // search pivot to keep up.
+
+        const mbeId = Number( // for this page
+            this.searchContext.termSearch.hasBrowseEntry.split(',')[0]);
+
+        const targetMbe = prev ?
+            this.prevEntry.browse_entry : this.nextEntry.browse_entry;
+
+        let results = [].concat(this.staffCat.browsePagerResults);
+        if (prev) { results.reverse(); }
+
+        let current;
+        let done = false;
+        results.forEach(result => {
+            if (done) { return; }
+
+            if (current) {
+
+                if (result.browse_entry === targetMbe) {
+                    // The target entry is on the current page of results
+                    // No pivot movement required.
+                    done = true;
+
+                } else if (result.pivot) {
+                    // We have crossed a pivot boundary.
+                    this.searchContext.browseSearch.pivot = result.pivot;
+                    done = true;
+                }
+
+            } else if (result.browse_entry === mbeId) {
+                current = result;
+            }
+        });
+    }
+
+    // Find the browse entry for the next/prev page and navigate there
+    // if possible.  Returns false if not enough data is available.
+    goToBrowsePage(prev: boolean): boolean {
+        const ctx = this.searchContext;
+        const target = prev ? this.prevEntry : this.nextEntry;
+
+        if (!target) { return false; }
+
+        this.setSearchPivot();
+
+        // Jump to the selected browse entry's page.
+        ctx.termSearch.hasBrowseEntry = target.browse_entry + ',' + target.fields;
+        ctx.pager.offset = 0; // this is a new records-for-browse-entry search
+        this.staffCat.search();
+
+        return true;
+    }
+}
+
+
index 0c04c9e..a6c27cc 100644 (file)
 
 <!-- header, pager, and list of records -->
 <div id="staff-catalog-results-container" *ngIf="searchHasResults()">
-  <div class="row d-flex mb-2" *ngIf="searchContext.termSearch.browseEntry">
-    <div class="col-lg-10 offset-lg-2">
-      <h3 i18n>Results for browse entry "{{searchContext.termSearch.browseEntry.value()}}"</h3>
-    </div>
-  </div>
+
+  <eg-catalog-browse-pager *ngIf="searchContext.termSearch.browseEntry">
+  </eg-catalog-browse-pager>
+
   <div class="row">
     <div class="col-lg-2" *ngIf="!searchContext.basket">
       <ng-container *ngIf="!searchContext.termSearch.browseEntry">
         </span>
       </label>
     </div>
-
-    <ng-container *ngIf="searchContext.browseSearch.value">
-      <div class="col-lg-3">
-        <ng-container *ngIf="browseLoading">
-          <eg-progress-inline></eg-progress-inline>
-        </ng-container>
-        <ng-container *ngIf="!browseLoading">
-          <button class="btn btn-sm btn-outline-dark" 
-            (click)="browseNav(true)" i18n>Previous Heading</button>
-          <button class="btn btn-sm btn-outline-dark ml-2" 
-            (click)="browseNav()" i18n>Next Heading</button>
-        </ng-container>
+    <div class="col-lg-8">
+      <div class="float-right">
+        <eg-catalog-result-pagination></eg-catalog-result-pagination>
       </div>
-      <div class="col-lg-5">
-        <div class="float-right">
-          <eg-catalog-result-pagination></eg-catalog-result-pagination>
-        </div>
-      </div>
-    </ng-container>
-    <ng-container *ngIf="!searchContext.browseSearch.value">
-      <div class="col-lg-8">
-        <div class="float-right">
-          <eg-catalog-result-pagination></eg-catalog-result-pagination>
-        </div>
-      </div>
-    </ng-container>
+    </div>
   </div>
   <div>
     <div class="row mt-2">
index b73e7b5..b4eb0ad 100644 (file)
@@ -28,8 +28,6 @@ export class ResultsComponent implements OnInit, OnDestroy {
     searchSub: Subscription;
     routeSub: Subscription;
     basketSub: Subscription;
-    browseResults: any[] = [];
-    browseLoading = false;
 
     constructor(
         private route: ActivatedRoute,
@@ -43,6 +41,7 @@ export class ResultsComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.searchContext = this.staffCat.searchContext;
+        this.staffCat.browsePagerResults = [];
 
         // Our search context is initialized on page load.  Once
         // ResultsComponent is active, it will not be reinitialized,
@@ -71,15 +70,6 @@ export class ResultsComponent implements OnInit, OnDestroy {
         // Watch for basket changes applied by other components.
         this.basketSub = this.basket.onChange.subscribe(
             () => this.applyRecordSelection());
-
-
-        // Load browse data to support browse paging.
-        /*
-        if (this.searchContext.termSearch.hasBrowseEntry &&
-            this.searchContext.browseSearch.value) {
-            this.fetchBrowseData().toPromise();
-        }
-        */
     }
 
     ngOnDestroy() {
@@ -138,107 +128,6 @@ export class ResultsComponent implements OnInit, OnDestroy {
             this.basket.removeRecordIds(ids);
         }
     }
-
-    // Grab a page of browse results
-    fetchBrowseData(prev?: boolean): Promise<any> {
-        const ctx = this.searchContext.clone();
-        ctx.termSearch.hasBrowseEntry = null; // avoid term search
-
-        // Grab a few extras so we can last a bit longer before
-        // having to fetch more browse data.  Could make this bigger.
-        ctx.pager.limit = 20;
-
-        const results = [];
-
-        return this.cat.browse(ctx)
-        .pipe(tap(result => results.push(result)))
-        .toPromise().then(_ => {
-            if (prev) {
-                this.browseResults = results.concat(this.browseResults);
-            } else {
-                this.browseResults = this.browseResults.concat(results);
-            }
-        });
-    }
-
-    // Find the browse entry for the next/prev page and navigate there
-    // if possible.  Returns false if not enough data is available.
-    goToBrowsePage(prev?: boolean): boolean {
-        const ctx = this.searchContext;
-
-        // See if we have enough info to direct to the selected page.
-
-        const mbeId = Number( // for this page
-            this.searchContext.termSearch.hasBrowseEntry.split(',')[0]);
-
-        let target, previous;
-
-        this.browseResults.forEach(result => {
-            // ignore pivot and authority-only entries
-            if (!result.sources) { return; }
-
-            if (previous) {
-                if (prev) {
-                    if (result.browse_entry === mbeId) {
-                        target = previous;
-                    }
-                } else {
-                    if (previous.browse_entry === mbeId) {
-                        target = result;
-                    }
-                }
-            }
-            previous = result;
-        });
-
-        if (!target) { return false; }
-
-        // Jump to the selected browse entry's page.
-        ctx.termSearch.hasBrowseEntry = target.browse_entry + ',' + target.fields;
-        ctx.pager.offset = 0; // this is a brand new search
-        this.staffCat.search();
-
-        return true;
-    }
-
-
-    // Navigate to next or previous browse entry page.
-    // take1: navigate without fetching data if possible
-    // take2: fetch data and navigate if possible.
-    // take3: data fetched in take2 contains data for this page, but
-    // not the requested page.  This happens when the user navigates
-    // cold to a browse page boundary and the current page of data
-    // does not yet exist.
-    browseNav(prev?: boolean, take?: number) {
-        if (!take) { take = 1; }
-
-        this.browseLoading = true;
-
-        if (take > 3) {
-            // We tried our best, but could not find the requested
-            // browse data Could happen if we reach the end of the data set.
-            this.browseLoading = false;
-            return;
-        }
-
-        if (this.goToBrowsePage(prev)) {
-            // Navigation successful
-            this.browseLoading = false;
-            return;
-        }
-
-        // We need another page of browse data before we can direct
-        // the user to the requested page.
-
-        const results = this.browseResults;
-        if (results.length > 0) {
-            this.searchContext.browseSearch.pivot = prev ?
-                results[0].pivot_point :
-                results[results.length - 1].pivot_point;
-        }
-
-        this.fetchBrowseData(prev).then(_ => this.browseNav(prev, take + 1));
-    }
 }