From 0df5d1f8f7e0d907489feaa0156b966440310fb3 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 16 Jan 2020 13:23:15 -0500 Subject: [PATCH] LP1860044 Angular catalog search result highlights Support search field highlighting in the Angular staff catalog search result and record detail pages. Adds a new component for rendering the highlighted content. Move the catalog-common module import into the staff common module so the bib-summary component has access to the new display-field component. Drop the default search result page size to 10 for consistency with other catalogs (and to speed up rendering). Note users can still set the page size of their choice via user settings. Signed-off-by: Bill Erickson Signed-off-by: Ruth Frasur --- .../share/catalog/bib-display-field.component.css | 11 +++ .../share/catalog/bib-display-field.component.html | 7 ++ .../share/catalog/bib-display-field.component.ts | 62 +++++++++++++++ .../src/app/share/catalog/bib-record.service.ts | 1 + .../src/app/share/catalog/catalog-common.module.ts | 7 +- .../eg2/src/app/share/catalog/catalog.service.ts | 90 ++++++++++++++++++---- .../eg2/src/app/share/catalog/search-context.ts | 7 ++ .../eg2/src/app/staff/catalog/catalog.module.ts | 2 - .../eg2/src/app/staff/catalog/catalog.service.ts | 2 +- .../staff/catalog/record/pagination.component.ts | 20 +++-- .../app/staff/catalog/record/record.component.html | 4 +- .../app/staff/catalog/record/record.component.ts | 17 ++++ .../app/staff/catalog/result/record.component.css | 4 +- .../app/staff/catalog/result/record.component.html | 50 +++++++----- Open-ILS/src/eg2/src/app/staff/common.module.ts | 5 +- .../share/bib-summary/bib-summary.component.html | 5 +- 16 files changed, 241 insertions(+), 53 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.css create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.html create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.ts diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.css b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.css new file mode 100644 index 0000000000..f4dfc111e5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.css @@ -0,0 +1,11 @@ + +.oils_SH { + font-weight: bolder; + background-color: #99ff99; +} + +.oils_SH.identifier { + font-weight: bolder; + background-color: #42b0f4; +} + diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.html b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.html new file mode 100644 index 0000000000..021e451ecc --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.html @@ -0,0 +1,7 @@ + + + {{joiner}} + + + diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.ts new file mode 100644 index 0000000000..abcbb4630a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-display-field.component.ts @@ -0,0 +1,62 @@ +import {Component, OnInit, Input, ViewEncapsulation} from '@angular/core'; +import {NetService} from '@eg/core/net.service'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.service'; +import {BibRecordService, BibRecordSummary + } from '@eg/share/catalog/bib-record.service'; + +/* Display content from a bib summary display field. If highlight + * data is avaialble, it will be used in lieu of the plan display string. + * + * + */ + +// non-collapsing space +const PAD_SPACE = ' '; // U+2007 + +@Component({ + selector: 'eg-bib-display-field', + templateUrl: 'bib-display-field.component.html', + styleUrls: ['bib-display-field.component.css'], + encapsulation: ViewEncapsulation.None // required for search highlighting +}) +export class BibDisplayFieldComponent implements OnInit { + + @Input() summary: BibRecordSummary; + @Input() field: string; // display field name + + // Used to join multi fields + @Input() joiner: string; + + // If true, replace empty values with a non-collapsing space. + @Input() usePlaceholder: boolean; + + constructor() {} + + ngOnInit() {} + + // Returns an array of display values which may either be + // plain string values or strings with embedded HTML markup + // for search results highlighting. + getDisplayStrings(): string[] { + const replacement = this.usePlaceholder ? PAD_SPACE : ''; + + if (!this.summary) { return [replacement]; } + + const scrunch = (value) => { + if (Array.isArray(value)) { + return value; + } else { + return [value || replacement]; + } + }; + + return scrunch( + this.summary.displayHighlights[this.field] || + this.summary.display[this.field] + ); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts index b2058e7aed..83d66c0654 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts @@ -31,6 +31,7 @@ export class BibRecordSummary { holdCount: number; bibCallNumber: string; net: NetService; + displayHighlights: {[name: string]: string | string[]} = {}; constructor(record: IdlObject, orgId: number, orgDepth: number) { this.id = Number(record.id()); diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts index f9e628ed0e..ba8c915884 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts @@ -6,17 +6,20 @@ import {CatalogUrlService} from './catalog-url.service'; import {BibRecordService} from './bib-record.service'; import {UnapiService} from './unapi.service'; import {MarcHtmlComponent} from './marc-html.component'; +import {BibDisplayFieldComponent} from './bib-display-field.component'; @NgModule({ declarations: [ - MarcHtmlComponent + MarcHtmlComponent, + BibDisplayFieldComponent ], imports: [ EgCommonModule ], exports: [ - MarcHtmlComponent + MarcHtmlComponent, + BibDisplayFieldComponent ], providers: [ CatalogService, diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index 2aaaf1f3ae..3b50f61e43 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -135,20 +135,18 @@ export class CatalogService { method += '.staff'; } - return new Promise((resolve, reject) => { - this.net.request( - 'open-ils.search', method, { - limit : ctx.pager.limit + 1, - offset : ctx.pager.offset - }, fullQuery, true - ).subscribe(result => { - this.applyResultData(ctx, result); - ctx.searchState = CatalogSearchState.COMPLETE; - this.onSearchComplete.emit(ctx); - resolve(); - }); + return this.net.request( + 'open-ils.search', method, { + limit : ctx.pager.limit + 1, + offset : ctx.pager.offset + }, fullQuery, true + ).toPromise() + .then(result => this.applyResultData(ctx, result)) + .then(_ => this.fetchFieldHighlights(ctx)) + .then(_ => { + ctx.searchState = CatalogSearchState.COMPLETE; + this.onSearchComplete.emit(ctx); }); - } // When showing titles linked to a browse entry, fetch @@ -212,6 +210,67 @@ export class CatalogService { // May be reset when quickly navigating results. ctx.result.records[idx] = summary; } + + if (ctx.highlightData[summary.id]) { + summary.displayHighlights = ctx.highlightData[summary.id]; + } + })).toPromise(); + } + + fetchFieldHighlights(ctx: CatalogSearchContext): Promise { + + let hlMap; + + // Extract the highlight map. Not all searches have them. + if ((hlMap = ctx.result) && + (hlMap = hlMap.global_summary) && + (hlMap = hlMap.query_struct) && + (hlMap = hlMap.additional_data) && + (hlMap = hlMap.highlight_map) && + (Object.keys(hlMap).length > 0)) { + } else { return Promise.resolve(); } + + let ids; + if (ctx.getHighlightsFor) { + ids = [ctx.getHighlightsFor]; + } else { + // ctx.currentResultIds() returns bib IDs or metabib IDs + // depending on the search type. If we have metabib IDs, map + // them to bib IDs for highlighting. + ids = ctx.currentResultIds(); + if (ctx.termSearch.groupByMetarecord) { + ids = ids.map(mrId => + ctx.result.records.filter(r => mrId === r.metabibId)[0].id + ); + } + } + + return this.net.requestWithParamList( // API is list-based + 'open-ils.search', + 'open-ils.search.fetch.metabib.display_field.highlight', + [hlMap].concat(ids) + ).pipe(map(fields => { + + if (fields.length === 0) { return; } + + // Each 'fields' collection is an array of display field + // values whose text is augmented with highlighting markup. + const highlights = ctx.highlightData[fields[0].source] = {}; + + fields.forEach(field => { + const dfMap = this.cmfMap[field.field].display_field_map(); + if (!dfMap) { return; } // pretty sure this can't happen. + + if (dfMap.multi() === 't') { + if (!highlights[dfMap.name()]) { + highlights[dfMap.name()] = []; + } + (highlights[dfMap.name()] as string[]).push(field.highlight); + } else { + highlights[dfMap.name()] = field.highlight; + } + }); + })).toPromise(); } @@ -312,14 +371,15 @@ export class CatalogService { } fetchCmfs(): Promise { - // At the moment, we only need facet CMFs. if (Object.keys(this.cmfMap).length) { return Promise.resolve(); } return new Promise((resolve, reject) => { this.pcrud.search('cmf', - {facet_field : 't'}, {}, {atomic: true, anonymous: true} + {'-or': [{facet_field : 't'}, {display_field: 't'}]}, + {flesh: 1, flesh_fields: {cmf: ['display_field_map']}}, + {atomic: true, anonymous: true} ).subscribe( cmfs => { cmfs.forEach(c => this.cmfMap[c.id()] = c); diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts index 041d710a4b..f993b8ccbc 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts @@ -347,6 +347,12 @@ export class CatalogSearchContext { // List of IDs in page/offset context. resultIds: number[]; + // If a bib ID is provided, instruct the search code to + // only fetch field highlight data for a single record instead + // of all search results. + getHighlightsFor: number; + highlightData: {[id: number]: {[field: string]: string | string[]}} = {}; + // Utility stuff pager: Pager; org: OrgService; @@ -403,6 +409,7 @@ export class CatalogSearchContext { this.showBasket = false; this.result = new CatalogSearchResults(); this.resultIds = []; + this.highlightData = {}; this.searchState = CatalogSearchState.PENDING; this.termSearch.reset(); this.marcSearch.reset(); 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 d9e2143ed8..3ad00a9942 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 @@ -1,7 +1,6 @@ import {NgModule} from '@angular/core'; import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module'; import {StaffCommonModule} from '@eg/staff/common.module'; -import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module'; import {CatalogRoutingModule} from './routing.module'; import {HoldsModule} from '@eg/staff/share/holds/holds.module'; import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module'; @@ -61,7 +60,6 @@ import {PreferencesComponent} from './prefs.component'; imports: [ StaffCommonModule, FmRecordEditorModule, - CatalogCommonModule, CatalogRoutingModule, HoldsModule, HoldingsModule, 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 e6da35879b..05f58814bf 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 @@ -64,7 +64,7 @@ export class StaffCatalogService { } if (!this.searchContext.pager.limit) { - this.searchContext.pager.limit = this.defaultSearchLimit || 20; + this.searchContext.pager.limit = this.defaultSearchLimit || 10; } } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts index 88214cd805..038679a5e4 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts @@ -137,17 +137,23 @@ export class RecordPaginationComponent implements OnInit { return Promise.resolve(); } - const origPager = this.searchContext.pager; + const ctx = this.searchContext; + + const origPager = ctx.pager; const tmpPager = new Pager(); tmpPager.limit = limit || 1000; - this.searchContext.pager = tmpPager; + ctx.pager = tmpPager; + + // Avoid fetching highlight data for a potentially large + // list of record IDs + ctx.getHighlightsFor = this.id; - return this.cat.search(this.searchContext) - .then( - ok => this.searchContext.pager = origPager, - notOk => this.searchContext.pager = origPager - ); + return this.cat.search(ctx) + .then(_ => { + ctx.pager = origPager; + ctx.getHighlightsFor = null; + }); } returnToSearch(): void { diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html index cf082f9668..b82dd74003 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html @@ -10,8 +10,8 @@
-
- +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts index dc8d8dfce8..b900ea8734 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts @@ -151,6 +151,23 @@ export class RecordComponent implements OnInit { }); } + // Lets us intercept the summary object and augment it with + // search highlight data if/when it becomes available from + // an externally executed search. + summaryForDisplay(): BibRecordSummary { + if (!this.summary) { return null; } + const sum = this.summary; + const ctx = this.searchContext; + + if (Object.keys(sum.displayHighlights).length === 0) { + if (ctx.highlightData[sum.id]) { + sum.displayHighlights = ctx.highlightData[sum.id]; + } + } + + return this.summary; + } + currentSearchOrg(): IdlObject { if (this.staffCat && this.staffCat.searchContext) { return this.staffCat.searchContext.searchOrg; diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.css b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.css index 3d753f4e42..2930138605 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.css +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.css @@ -1,7 +1,7 @@ /** - * Force the jacket image column to consume a consistent amount of - * horizontal space, while allowing some room for the browser to + * Force the jacket image column to consume a consistent amount of + * horizontal space, while allowing some room for the browser to * render the correct aspect ratio. */ .record-jacket-div { diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html index a27c1bdf34..65209a0b4f 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html @@ -1,8 +1,3 @@ -
@@ -18,8 +13,6 @@ -
@@ -88,32 +82,48 @@
Phys. Desc.: - {{[].concat(summary.display.physical_description).join(', ')}} +
-
Edition: {{summary.display.edition}}
+
Edition: + +
-
Publisher: {{summary.display.publisher}}
+
Publisher: + + +
-
Pub Date: {{summary.display.pubdate}}
+
Pub Date: + + +
ISBN: - {{[].concat(summary.display.isbn).join(', ')}}
+ +
UPC: - {{[].concat(summary.display.upc).join(', ')}}
+ +
ISSN: - {{[].concat(summary.display.issn).join(', ')}}
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index c0fe41b621..1d641e435a 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -3,6 +3,7 @@ import {EgCommonModule} from '@eg/common.module'; import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; import {AudioService} from '@eg/share/util/audio.service'; import {GridModule} from '@eg/share/grid/grid.module'; +import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module'; import {StaffBannerComponent} from './share/staff-banner.component'; import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive'; import {AccessKeyService} from '@eg/share/accesskey/accesskey.service'; @@ -39,12 +40,14 @@ import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barco imports: [ EgCommonModule, CommonWidgetsModule, - GridModule + GridModule, + CatalogCommonModule ], exports: [ EgCommonModule, CommonWidgetsModule, GridModule, + CatalogCommonModule, StaffBannerComponent, AccessKeyDirective, AccessKeyInfoComponent, diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html index 45345d2501..3fd955888f 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html @@ -47,7 +47,10 @@
  • Title:
    -
    {{summary.display.title}}
    +
    + + +
    Edition:
    {{summary.display.edition}}
    TCN:
    -- 2.11.0