'search_format'
];
+const MODS_XPATH_AUTO = {
+ title : '/mods:mods/mods:titleInfo/mods:title',
+ author: '/mods:mods/mods:name/mods:namePart',
+ edition: '/mods:mods/mods:originInfo/mods:edition',
+ pubdate: '/mods:mods/mods:originInfo/mods:dateIssued',
+ genre: '/mods:mods/mods:genre'
+};
+
+const MODS_XPATH = {
+ extern: '/mods:mods/biblio:extern',
+ copyCounts: '/mods:mods/holdings:holdings/holdings:counts/holdings:count',
+ attributes: '/mods:mods/indexing:attributes/indexing:field'
+};
+
+const NAMESPACE_MAPS = {
+ 'mods': 'http://www.loc.gov/mods/v3',
+ 'biblio': 'http://open-ils.org/spec/biblio/v1',
+ 'holdings': 'http://open-ils.org/spec/holdings/v1',
+ 'indexing': 'http://open-ils.org/spec/indexing/v1'
+};
+
@Injectable()
export class EgCatalogService {
/**
- * Bib record via UNAPI as mods32 (for now) with holdings summary
+ * Bib record via UNAPI as mods (for now) with holdings summary
* and record attributes.
*/
getBibSummary(bibId: number, orgId: number, depth: number): Promise<any> {
return new Promise((resolve, reject) => {
- this.unapi.getAsObject({
+ this.unapi.getAsXmlDocument({
target: 'bre',
id: bibId,
extras: '{bre.extern,holdings_xml,mra}',
format: 'mods32',
orgId: orgId,
depth: depth
- }).then(summary => {
- summary = this.translateBibSummary(summary);
+ }).then(xmlDoc => {
+ let summary = this.translateBibSummary(xmlDoc);
summary.id = bibId;
resolve(summary);
});
* UNAPI content into a more digestable form.
* TODO: Add display field support
*/
- translateBibSummary(summary: any): any { // TODO: bib summary interface
- const UNAPI_PATHS = {
- title : 'titleInfo[0].title[0]',
- author: 'name[0].namePart[0]',
- edition: 'originInfo[0].edition[0]',
- pubdate: 'originInfo[0].dateIssued[0]',
- genre: 'genre[0]._'
- }
+ translateBibSummary(xmlDoc: XMLDocument): any { // TODO: bib summary interface
let response = {
copyCounts : [],
ccvms : {}
};
- Object.keys(UNAPI_PATHS).forEach(key => {
- try {
- response[key] = eval(`summary.mods.${UNAPI_PATHS[key]}`);
- } catch(E) {
- response[key] = '';
- }
- });
+ let resolver:any = (prefix: string): string => {
+ return NAMESPACE_MAPS[prefix] || null;
+ };
- Object.keys(summary.mods.extern[0]['$']).forEach(field => {
- if (field == 'xmlns') return;
- response[field] = summary.mods.extern[0]['$'][field];
- });
+ Object.keys(MODS_XPATH_AUTO).forEach(key => {
+ let result = xmlDoc.evaluate(MODS_XPATH_AUTO[key], xmlDoc,
+ resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
- summary.mods.attributes[0].field.forEach(attrField => {
- response.ccvms[attrField['$'].name] = {
- code: attrField['_'],
- label: attrField['$']['coded-value']
- };
+ let node = result.singleNodeValue;
+ if (node) response[key] = node.textContent;
});
- summary.mods.holdings[0].counts[0].count.forEach(count => {
- response.copyCounts.push(count['$']);
- });
+ let result = xmlDoc.evaluate(MODS_XPATH.extern, xmlDoc,
+ resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
- //console.log(summary);
- //console.log(response);
+ let node:any = result.singleNodeValue;
+ if (node) {
+ let attrs = node.attributes;
+ for(let i = attrs.length - 1; i >= 0; i--) {
+ response[attrs[i].name] = attrs[i].value;
+ }
+ }
+ result = xmlDoc.evaluate(MODS_XPATH.attributes, xmlDoc,
+ resolver, XPathResult.ANY_TYPE, null);
+
+ while(node = result.iterateNext()) {
+ response.ccvms[node.getAttribute('name')] = {
+ code : node.getAttribute('value'),
+ label : node.getAttribute('coded-value')
+ }
+ }
+
+ result = xmlDoc.evaluate(MODS_XPATH.copyCounts, xmlDoc,
+ resolver, XPathResult.ANY_TYPE, null);
+
+ while(node = result.iterateNext()) {
+ let counts = {};
+ ['type', 'depth', 'org_unit', 'transcendant',
+ 'available', 'count', 'unshadow'].forEach(field => {
+ counts[field] = node.getAttribute(field);
+ });
+ response.copyCounts.push(counts);
+ }
+
+ //console.log(response);
return response;
}
}
import {Injectable, EventEmitter} from '@angular/core';
import {EgOrgService} from '@eg/core/org';
import {HttpClient} from '@angular/common/http';
-import {Parser} from 'xml2js';
/*
TODO: Add Display Fields to UNAPI
private http: HttpClient
) {}
- /**
- * Retrieve an UNAPI document and return it as an XML string
- */
- getAsXmlString(params: EgUnapiParams): Promise<string> {
+ createUrl(params: EgUnapiParams): string {
let depth = params.depth || 0;
let org = params.orgId ? this.org.get(params.orgId) : this.org.root();
- let url = `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` +
+ return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` +
`${org.shortname()}/${depth}&format=${params.format}`;
-
- //console.debug(`UNAPI: ${url}`);
-
- return new Promise((resolve, reject) => {
- this.http.get(url, {responseType: 'text'})
- .subscribe(xmlStr => resolve(xmlStr));
- });
}
- /**
- * Retrieve an UNAPI document and return its object form, as
- * generated by xml2js.
- */
- getAsObject(params: EgUnapiParams): Promise<any> {
+ getAsXmlDocument(params: EgUnapiParams): Promise<XMLDocument> {
+ // XReq creates an XML document for us. Seems like the right
+ // tool for the job.
+ let url = this.createUrl(params);
return new Promise((resolve, reject) => {
- this.getAsXmlString(params)
- .then(xmlStr => {
- new Parser().parseString(xmlStr, (err, unapiBlob) => {
- if (err) {
- reject(err);
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4) {
+ if (this.status == 200) {
+ resolve(xhttp.responseXML);
} else {
- resolve(unapiBlob);
+ reject(`UNAPI request failed for ${url}`);
}
- });
- });
+ }
+ }
+ xhttp.open("GET", url, true);
+ xhttp.send();
});
}
}
*/
export class Pager {
offset: number = 0;
- limit: number = 15;
+ limit: number = null;
resultCount: number;
isFirstPage(): boolean {
this.searchContext.pager.setPage(page);
this.staffCat.search();
}
-
}
<div class="col-2">
<div class="row" [ngClass]="{'pt-2':copyIndex > 0}"
*ngFor="let copyCount of bibSummary.copyCounts; let copyIdx = index">
- <div *ngIf="copyCount.type == 'staff'" class="col-12">
- {{copyCount.available}} / {{copyCount.count}}
- items @ {{orgName(copyCount.org_unit)}}
+ <div class="w-100" *ngIf="copyCount.type == 'staff'">
+ <div class="float-left text-left w-50">
+ <span class="pr-1">
+ {{copyCount.available}} / {{copyCount.count}} items
+ </span>
+ </div>
+ <div class="float-left w-50">
+ @ {{orgName(copyCount.org_unit)}}
+ </div>
</div>
</div>
</div>
- <div class="col-4">
+ <div class="col-1"></div>
+ <div class="col-3">
<div class="row">
<div class="col-4">
<div class="float-right weak-text-1">
- Holds: {{bibSummary.holdCount}}
+ TCN: {{bibSummary.tcn_value}}
</div>
</div>
<div class="col-8">
<div class="row pt-2">
<div class="col-4">
<div class="float-right weak-text-1">
- TCN: {{bibSummary.tcn_value}}
+ Holds: {{bibSummary.holdCount}}
</div>
</div>
<div class="col-8">
</div>
</div>
<div class="row pt-2">
- <div class="col-4">
- <div class="float-right weak-text-1">
- <span i18n>ID: {{bibSummary.id}}</span>
- </div>
- </div>
- <div class="col-8">
+ <div class="col-12">
<div class="float-right">
<span>
<button (click)="placeHold()"
import {ActivatedRoute, ParamMap} from '@angular/router';
import {EgCatalogService} from '@eg/share/catalog/catalog.service';
import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service';
-import {CatalogSearchContext, CatalogSearchState}
+import {CatalogSearchContext, CatalogSearchState}
from '@eg/share/catalog/search-context';
import {EgPcrudService} from '@eg/core/pcrud';
import {StaffCatalogService} from '../staff-catalog.service';
export class ResultsComponent implements OnInit {
searchContext: CatalogSearchContext;
+ facetCache: any;
+ facetKey: string;
// Cache record creator/editor since this will likely be a
// reasonably small set of data w/ lots of repitition.
if (!this.searchContext.query[0]) return;
this.cat.search(this.searchContext).then(ok => {
- this.cat.fetchFacets(this.searchContext);
+ // Only need to fetch facets once per search
+ // TODO: move last-facet cache to this.cat.fetchFacets;
+ if (this.facetKey == this.searchContext.result.facet_key) {
+ this.searchContext.result.facetData = this.facetCache;
+ } else {
+ this.cat.fetchFacets(this.searchContext).then(ok => {;
+ this.facetKey = this.searchContext.result.facet_key;
+ this.facetCache = this.searchContext.result.facetData;
+ });
+ }
this.fleshSearchResults();
});
}
</optgroup>
</select>
</div>
- <div class="flex-2 pl-1 align-self-end">
+ <div class="flex-2 pl-2 align-self-end">
<div class="checkbox">
<label>
<input type="checkbox" [(ngModel)]="searchContext.available"/>
</label>
</div>
</div>
- <div class="flex-4 pl-1 align-self-end">
+ <div class="flex-4 pl-2 align-self-end">
<div class="checkbox">
<label>
<input type="checkbox" [(ngModel)]="searchContext.global"/>
</div>
</div>
<div class="flex-2 pl-1">
- <div *ngIf="searchContext.searchInProgress">
+ <div *ngIf="searchIsActive()">
<div class="progress">
- <div class="progress-bar progress-bar-striped active"
- role="progressbar" aria-valuenow="100" aria-valuemin="0"
- aria-valuemax="100" style="width: 100%">
+ <div class="progress-bar progress-bar-striped active w-100"
+ role="progressbar" aria-valuenow="100"
+ aria-valuemin="0" aria-valuemax="100">
<span class="sr-only" i18n>Searching..</span>
</div>
</div>
import {Component, OnInit} from '@angular/core';
import {EgIdlObject} from '@eg/core/idl';
import {EgOrgService} from '@eg/core/org';
-import {EgCatalogService} from '@eg/share/catalog/catalog.service';
-import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {EgCatalogService,} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, CatalogSearchState}
+ from '@eg/share/catalog/search-context';
import {StaffCatalogService} from './staff-catalog.service';
@Component({
searchByForm(): void {
this.staffCat.search();
}
+
+ searchIsActive(): boolean {
+ return this.searchContext.searchState == CatalogSearchState.SEARCHING;
+ }
+
}
this.searchContext.org = this.org;
this.searchContext.isStaff = true;
+ if (!this.searchContext.pager.limit)
+ this.searchContext.pager.limit = 20;
}
/**