--- /dev/null
+
+.oils_SH {
+ font-weight: bolder;
+ background-color: #99ff99;
+}
+
+.oils_SH.identifier {
+ font-weight: bolder;
+ background-color: #42b0f4;
+}
+
--- /dev/null
+
+<ng-container
+ *ngFor="let val of getDisplayStrings(); let first = first">
+ <ng-container *ngIf="joiner && !first">{{joiner}} </ng-container>
+ <span [innerHTML]="val"></span>
+</ng-container>
+
--- /dev/null
+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.
+ *
+ * <eg-bib-display-field field="title" [summary]="summary"
+ * [usePlaceholder]="true"></eg-bib-display-field>
+ */
+
+// 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]
+ );
+ }
+}
+
+
holdCount: number;
bibCallNumber: string;
net: NetService;
+ displayHighlights: {[name: string]: string | string[]} = {};
constructor(record: IdlObject, orgId: number, orgDepth: number) {
this.id = Number(record.id());
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,
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
// 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<any> {
+
+ 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();
}
}
fetchCmfs(): Promise<void> {
- // 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);
// 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;
this.showBasket = false;
this.result = new CatalogSearchResults();
this.resultIds = [];
+ this.highlightData = {};
this.searchState = CatalogSearchState.PENDING;
this.termSearch.reset();
this.marcSearch.reset();
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';
imports: [
StaffCommonModule,
FmRecordEditorModule,
- CatalogCommonModule,
CatalogRoutingModule,
HoldsModule,
HoldingsModule,
}
if (!this.searchContext.pager.limit) {
- this.searchContext.pager.limit = this.defaultSearchLimit || 20;
+ this.searchContext.pager.limit = this.defaultSearchLimit || 10;
}
}
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 {
</eg-confirm-dialog>
<div id="staff-catalog-record-container">
- <div id='staff-catalog-bib-summary-container' class='mb-1'>
- <eg-bib-summary [bibSummary]="summary">
+ <div id='staff-catalog-bib-summary-container' class='mt-1'>
+ <eg-bib-summary [bibSummary]="summaryForDisplay()">
</eg-bib-summary>
</div>
<div class="row ml-0 mr-0">
});
}
+ // 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;
/**
- * 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 {
-<!--
- TODO
- routerLink's
- egDateFilter's
--->
<div class="col-lg-12 card tight-card mb-2 bg-light">
<div class="card-body">
<input class="pl-1" type='checkbox' [(ngModel)]="isRecordSelected"
(change)="toggleBasketEntry()"/>
</label>
- <!-- XXX hard-coded width so columns align vertically regardless
- of the presence of a jacket image -->
<div class="pl-2 record-jacket-div" >
<ng-container *ngIf="hasMrConstituentRecords(summary)">
<a routerLink="/staff/catalog/search"
<div class="flex-1 pl-2">
<div class="row">
<div class="col-lg-12 font-weight-bold">
- <!-- nbsp allows the column to take shape when no value exists -->
<ng-container *ngIf="hasMrConstituentRecords(summary)">
<a routerLink="/staff/catalog/search"
[queryParams]="appendFromMrParam(summary)">
- {{summary.display.title || ' '}}
+ <eg-bib-display-field [summary]="summary" field="title"
+ [usePlaceholder]="true"></eg-bib-display-field>
</a>
</ng-container>
<ng-container *ngIf="!hasMrConstituentRecords(summary)">
<a routerLink="/staff/catalog/record/{{summary.id}}"
[queryParams]="currentParams()">
- {{summary.display.title || ' '}}
+ <eg-bib-display-field [summary]="summary" field="title"
+ [usePlaceholder]="true"></eg-bib-display-field>
</a>
</ng-container>
</div>
</div>
<div class="row pt-2">
<div class="col-lg-12">
- <!-- nbsp allows the column to take shape when no value exists -->
<a routerLink="/staff/catalog/search"
- [queryParams]="getAuthorSearchParams(summary)">
- {{summary.display.author || ' '}}
+ [queryParams]="getAuthorSearchParams(summary)">
+ <eg-bib-display-field [summary]="summary" field="author"
+ [usePlaceholder]="true"></eg-bib-display-field>
</a>
</div>
</div>
<ng-container *ngIf="summary.display.physical_description">
<!-- [].concat() to avoid modifying the summary arrays -->
<div class="pb-1" i18n>Phys. Desc.:
- {{[].concat(summary.display.physical_description).join(', ')}}
+ <eg-bib-display-field [summary]="summary"
+ field="physical_description" joiner=","></eg-bib-display-field>
</div>
</ng-container>
<ng-container *ngIf="summary.display.edition">
- <div class="pb-1" i18n>Edition: {{summary.display.edition}}</div>
+ <div class="pb-1" i18n>Edition:
+ <eg-bib-display-field [summary]="summary"
+ field="edition" joiner=","></eg-bib-display-field>
+ </div>
</ng-container>
<ng-container *ngIf="summary.display.publisher || summary.display.pubdate">
<!-- note publisher typically includes pubdate -->
<ng-container *ngIf="summary.display.publisher; else pubDate">
- <div class="pb-1" i18n>Publisher: {{summary.display.publisher}}</div>
+ <div class="pb-1" i18n>Publisher:
+ <eg-bib-display-field [summary]="summary" field="publisher">
+ </eg-bib-display-field>
+ </div>
</ng-container>
<ng-template #pubDate>
- <div class="pb-1" i18n>Pub Date: {{summary.display.pubdate}}</div>
+ <div class="pb-1" i18n>Pub Date:
+ <eg-bib-display-field [summary]="summary" field="pubdate">
+ </eg-bib-display-field>
+ </div>
</ng-template>
</ng-container>
<ng-container *ngIf="summary.display.isbn">
<div class="pb-1" i18n>ISBN:
- {{[].concat(summary.display.isbn).join(', ')}}</div>
+ <eg-bib-display-field [summary]="summary"
+ field="isbn" joiner=","></eg-bib-display-field>
+ </div>
</ng-container>
<ng-container *ngIf="summary.display.upc">
<div class="pb-1" i18n>UPC:
- {{[].concat(summary.display.upc).join(', ')}}</div>
+ <eg-bib-display-field [summary]="summary"
+ field="upc" joiner=","></eg-bib-display-field>
+ </div>
</ng-container>
<ng-container *ngIf="summary.display.issn">
<div i18n>ISSN:
- {{[].concat(summary.display.issn).join(', ')}}</div>
+ <eg-bib-display-field [summary]="summary"
+ field="issn" joiner=","></eg-bib-display-field>
+ </div>
</ng-container>
</div>
</div>
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';
imports: [
EgCommonModule,
CommonWidgetsModule,
- GridModule
+ GridModule,
+ CatalogCommonModule
],
exports: [
EgCommonModule,
CommonWidgetsModule,
GridModule,
+ CatalogCommonModule,
StaffBannerComponent,
AccessKeyDirective,
AccessKeyInfoComponent,
<li class="list-group-item">
<div class="d-flex">
<div class="flex-1 font-weight-bold" i18n>Title:</div>
- <div class="flex-3">{{summary.display.title}}</div>
+ <div class="flex-3">
+ <eg-bib-display-field [summary]="summary" field="title">
+ </eg-bib-display-field>
+ </div>
<div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
<div class="flex-1">{{summary.display.edition}}</div>
<div class="flex-1 font-weight-bold" i18n>TCN:</div>