LP#626157 Ang2 experiments
authorBill Erickson <berickxx@gmail.com>
Fri, 8 Dec 2017 04:28:08 +0000 (23:28 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 11 Dec 2017 17:39:51 +0000 (12:39 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/webby-src/src/app/share/catalog/catalog.service.ts
Open-ILS/webby-src/src/app/share/unapi.ts
Open-ILS/webby-src/src/app/share/util/pager.ts
Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.ts
Open-ILS/webby-src/src/app/staff/catalog/result/record.component.html
Open-ILS/webby-src/src/app/staff/catalog/result/results.component.ts
Open-ILS/webby-src/src/app/staff/catalog/search-form.component.html
Open-ILS/webby-src/src/app/staff/catalog/search-form.component.ts
Open-ILS/webby-src/src/app/staff/catalog/staff-catalog.service.ts

index 7f3e376..02e67e0 100644 (file)
@@ -18,6 +18,27 @@ export const CATALOG_CCVM_FILTERS = [
     '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 {
 
@@ -163,20 +184,20 @@ 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);
             });
@@ -190,47 +211,59 @@ export class EgCatalogService {
      * 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;
     }
 }
index 6a7ea1f..3de344b 100644 (file)
@@ -1,7 +1,6 @@
 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
@@ -27,40 +26,31 @@ export class EgUnapiService {
         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();
         });
     }
 }
index 5f33d4a..1c21a8d 100644 (file)
@@ -4,7 +4,7 @@
  */
 export class Pager {
     offset: number = 0;
-    limit: number = 15;
+    limit: number = null;
     resultCount: number;
 
     isFirstPage(): boolean {
index ff5aabc..ff1746c 100644 (file)
@@ -37,7 +37,6 @@ export class ResultPaginationComponent implements OnInit {
         this.searchContext.pager.setPage(page);
         this.staffCat.search();
     }
-
 }
 
 
index 98baa0a..63571f3 100644 (file)
     <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">
@@ -76,7 +83,7 @@
       <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()"
index a30d6f5..f2d1b30 100644 (file)
@@ -4,7 +4,7 @@ import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
 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';
@@ -18,6 +18,8 @@ import {EgIdlObject} from '@eg/core/idl';
 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.
@@ -58,7 +60,16 @@ export class ResultsComponent implements OnInit {
         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();
         });
     }
index e0135e5..a51d6c8 100644 (file)
@@ -117,7 +117,7 @@ TODO focus search input
           </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"/>
@@ -125,7 +125,7 @@ TODO focus search input
           </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"/>
@@ -134,11 +134,11 @@ TODO focus search input
         </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>
index db2dbaf..0d70807 100644 (file)
@@ -1,8 +1,9 @@
 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({
@@ -82,6 +83,11 @@ export class SearchFormComponent implements OnInit {
     searchByForm(): void {
         this.staffCat.search();
     }
+
+    searchIsActive(): boolean {
+        return this.searchContext.searchState == CatalogSearchState.SEARCHING;
+    }
+
 }
 
 
index 2a70760..31512d5 100644 (file)
@@ -33,6 +33,8 @@ export class StaffCatalogService {
 
         this.searchContext.org = this.org;
         this.searchContext.isStaff = true;
+        if (!this.searchContext.pager.limit)
+          this.searchContext.pager.limit = 20;
     }
 
     /**