LP1904788 Staff catalog browse results paging
authorBill Erickson <berickxx@gmail.com>
Tue, 17 Nov 2020 22:55:20 +0000 (17:55 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 18 Nov 2020 20:08:17 +0000 (15:08 -0500)
Adds the ability to step through browse headings directly from the
heading record list page without having to return to the original browse
search

To test:

1. Navigate to the staff catalog and perform a Browse search.

2. Click on one of the headings and you'll be taken to the page which
lists the bib records that use the selected heading.

3. Click the Previous Headings and Next Headings buttons to load
the prev/next heading and its linked records.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts

index 3aeaf6b..3ae935a 100644 (file)
@@ -119,9 +119,7 @@ export class BrowseResultsComponent implements OnInit, OnDestroy {
 
     searchByBrowseEntryParams(result) {
         const ctx = this.searchContext.clone();
-        ctx.browseSearch.reset(); // we're done browsing
-        ctx.termSearch.hasBrowseEntry =
-            result.browse_entry + ',' + result.fields;
+        ctx.termSearch.hasBrowseEntry = result.browse_entry + ',' + result.fields;
         return this.catUrl.toUrlParams(ctx);
     }
 
index 515a376..0c04c9e 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>
   <div class="row">
     <div class="col-lg-2" *ngIf="!searchContext.basket">
-      <ng-container *ngIf="searchContext.termSearch.browseEntry">
-        <h3 i18n>Results for browse "{{searchContext.termSearch.browseEntry.value()}}"</h3>
-      </ng-container>
       <ng-container *ngIf="!searchContext.termSearch.browseEntry">
         <h3 i18n>Search Results ({{searchContext.result.count}})</h3>
       </ng-container>
         </span>
       </label>
     </div>
-    <div class="col-lg-8">
-      <div class="float-right">
-        <eg-catalog-result-pagination></eg-catalog-result-pagination>
+
+    <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>
-    </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 class="row mt-2">
index edcb381..a7cd2dc 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, OnDestroy, Input} from '@angular/core';
 import {Observable, Subscription} from 'rxjs';
-import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {tap, map, switchMap, distinctUntilChanged} from 'rxjs/operators';
 import {ActivatedRoute, ParamMap} from '@angular/router';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
 import {BibRecordService} from '@eg/share/catalog/bib-record.service';
@@ -28,6 +28,8 @@ export class ResultsComponent implements OnInit, OnDestroy {
     searchSub: Subscription;
     routeSub: Subscription;
     basketSub: Subscription;
+    browseResults: any[] = [];
+    browseLoading = false;
 
     constructor(
         private route: ActivatedRoute,
@@ -62,12 +64,22 @@ export class ResultsComponent implements OnInit, OnDestroy {
         });
 
         // After each completed search, update the record selector.
-        this.searchSub = this.cat.onSearchComplete.subscribe(
-            ctx => this.applyRecordSelection());
+        this.searchSub = this.cat.onSearchComplete.subscribe(ctx => {
+            this.applyRecordSelection();
+        });
 
         // 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() {
@@ -126,6 +138,106 @@ 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 => {
+            if (!result.browse_entry) { return; } // ignore pivot entries
+
+            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));
+    }
 }
 
 
index 9dcca68..2b4faad 100644 (file)
@@ -93,7 +93,13 @@ export class SearchFormComponent implements OnInit, AfterViewInit {
                     this.searchTab = 'marc';
                 } else if (this.context.identSearch.isSearchable()) {
                     this.searchTab = 'ident';
-                } else if (this.context.browseSearch.isSearchable()) {
+
+                // Browse search may remain 'searchable' even though we
+                // are displaying bibs linked to a browse entry.
+                // This is so browse search paging can be added to
+                // the record list page.
+                } else if (this.context.browseSearch.isSearchable()
+                    && !this.context.termSearch.hasBrowseEntry) {
                     this.searchTab = 'browse';
                 } else if (this.context.termSearch.isSearchable()) {
                     this.searchTab = 'term';