From cd4c4c971164708710f9afe2f9b15f8f15780d3b Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 17 Nov 2020 17:55:20 -0500 Subject: [PATCH] LP1904788 Staff catalog browse results paging 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 Heading / Next Heading buttons to step through the headings and view the linked bib records. Signed-off-by: Bill Erickson Signed-off-by: Gina Monti Signed-off-by: Galen Charlton --- .../app/staff/catalog/browse/results.component.ts | 4 +- .../eg2/src/app/staff/catalog/catalog.module.ts | 4 +- .../eg2/src/app/staff/catalog/catalog.service.ts | 4 + .../catalog/result/browse-pager.component.html | 51 +++++ .../staff/catalog/result/browse-pager.component.ts | 229 +++++++++++++++++++++ .../staff/catalog/result/results.component.html | 7 +- .../app/staff/catalog/result/results.component.ts | 8 +- .../src/app/staff/catalog/search-form.component.ts | 8 +- 8 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts index 3aeaf6b32c..3ae935a59f 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts @@ -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); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts index cff15a18af..4df2ffd2ad 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts @@ -32,6 +32,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: [ @@ -58,7 +59,8 @@ import {PreferencesComponent} from './prefs.component'; CnBrowseComponent, OpacViewComponent, PreferencesComponent, - CnBrowseResultsComponent + CnBrowseResultsComponent, + BrowsePagerComponent ], imports: [ StaffCommonModule, diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts index e763b35ad2..9c2155fdcc 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts @@ -48,6 +48,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. + browsePagerData: 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 index 0000000000..8d592da36a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.html @@ -0,0 +1,51 @@ + +
+ + +
+ +
+
+ + +
+
+
+ +
+
+ {{prevEntry.value}} +
+
+
+
+
+ + Title: + Author: + Subject: + Series: + + + {{searchContext.termSearch.browseEntry.value()}} + +
+
+
+
+
+
+ {{nextEntry.value}} +
+
+ +
+
+
+
+
+ +
+ 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 index 0000000000..006ad8ca2b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/browse-pager.component.ts @@ -0,0 +1,229 @@ +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'; + +interface BrowsePage { + leftPivot: number; + rightPivot: number; + entries: any[]; +} + +@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()); + } + + pageEntryId(): number { + return Number( + this.searchContext.termSearch.hasBrowseEntry.split(',')[0] + ); + } + + getEntryPageIndex(mbeId: number): number { + let idx = null; + this.staffCat.browsePagerData.forEach((page, index) => { + page.entries.forEach(entry => { + if (entry.browse_entry === mbeId) { + idx = index; + } + }); + }); + return idx; + } + + + getEntryPage(mbeId: number): BrowsePage { + return this.staffCat.browsePagerData[this.getEntryPageIndex(mbeId)]; + } + + fetchPageData(): Promise { + + if (this.getEntryPage(this.pageEntryId())) { + // We have this page's data already + return Promise.resolve(); + } + + return this.fetchBrowsePage(null); + } + + // Grab a page of browse results + fetchBrowsePage(prev: boolean): Promise { + const ctx = this.searchContext.clone(); + ctx.pager.limit = this.searchContext.pager.limit; + ctx.termSearch.hasBrowseEntry = null; // avoid term search + + if (prev !== null) { + // Fetching data for a prev/next page which is not the + // current page. + const page = this.getEntryPage(this.pageEntryId()); + const pivot = prev ? page.leftPivot : page.rightPivot; + if (pivot === null) { + console.debug('Browse has reached the end of the rainbow'); + return; + } + ctx.browseSearch.pivot = pivot; + } + + const results = []; + this.browseLoading = true; + + return this.cat.browse(ctx) + .pipe(tap(result => results.push(result))) + .toPromise().then(_ => { + if (results.length === 0) { return; } + + // At the end of the data set, final pivots are not present + let leftPivot = null; + let rightPivot = null; + if (results[0].pivot_point) { + leftPivot = results.shift().pivot_point; + } + if (results[results.length - 1].pivot_point) { + rightPivot = results.pop().pivot_point; + } + + // We only care about entries with bib record sources + let keepEntries = results.filter(e => Boolean(e.sources)); + + if (leftPivot === null || rightPivot === null) { + // When you reach the edge of the data set, you can get + // the same browse entries from different API calls. + // From what I can tell, the last page will always have + // a half page of entries, even if you've already seen some + // of them in the previous page. Trim the dupes since they + // affect the logic. + const keep = []; + keepEntries.forEach(e => { + if (!this.getEntryPage(e.browse_entry)) { + keep.push(e); + } + }); + keepEntries = keep; + } + + const page: BrowsePage = { + leftPivot: leftPivot, + rightPivot: rightPivot, + entries: keepEntries + }; + + if (prev) { + this.staffCat.browsePagerData.unshift(page); + } else { + this.staffCat.browsePagerData.push(page); + } + this.browseLoading = false; + }); + } + + // Collect enough browse data to display previous, current, and + // next heading. This can mean fetching an additional page of data. + setPrevNext(take2: boolean = false): Promise { + + let previous: any; + const mbeId = this.pageEntryId(); + + this.staffCat.browsePagerData.forEach(page => { + page.entries.forEach(entry => { + + if (previous) { + if (entry.browse_entry === mbeId) { + this.prevEntry = previous; + } + if (previous.browse_entry === mbeId) { + this.nextEntry = entry; + } + } + previous = entry; + }); + }); + + if (take2) { + // If we have to call this more than twice it means we've + // reached the boundary of the full data set and there's + // no more data to fetch. + return Promise.resolve(); + } + + let promise; + + if (!this.prevEntry) { + promise = this.fetchBrowsePage(true); + + } else if (!this.nextEntry) { + promise = this.fetchBrowsePage(false); + } + + if (promise) { + return promise.then(_ => this.setPrevNext(true)); + } + + return Promise.resolve(); + } + + setSearchPivot(prev?: boolean) { + // When traversing browse result page boundaries, modify the + // search pivot to keep up. + + const targetMbe = Number( + prev ? this.prevEntry.browse_entry : this.nextEntry.browse_entry + ); + + const curPageIdx = this.getEntryPageIndex(this.pageEntryId()); + const targetPageIdx = this.getEntryPageIndex(targetMbe); + + if (targetPageIdx !== curPageIdx) { + // We are crossing a page boundary + + const curPage = this.getEntryPage(this.pageEntryId()); + + if (prev) { + this.searchContext.browseSearch.pivot = curPage.leftPivot; + + } else { + this.searchContext.browseSearch.pivot = curPage.rightPivot; + } + } + } + + // 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(prev); + + // 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; + } +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html index 96953ee54a..5f584a52da 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html @@ -31,11 +31,12 @@
+ + + +
- -

Results for browse "{{searchContext.termSearch.browseEntry.value()}}"

-

Search Results ({{searchContext.result.count}})

diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts index afdcd931a0..61be085848 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts @@ -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'; @@ -44,6 +44,7 @@ export class ResultsComponent implements OnInit, OnDestroy { ngOnInit() { this.searchContext = this.staffCat.searchContext; + this.staffCat.browsePagerData = []; // Our search context is initialized on page load. Once // ResultsComponent is active, it will not be reinitialized, @@ -65,8 +66,9 @@ 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( diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts index 16a124d4cc..d416f16a60 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts @@ -116,7 +116,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'; -- 2.11.0