LP#626157 Ang2 experiments
authorBill Erickson <berickxx@gmail.com>
Sun, 3 Dec 2017 16:06:36 +0000 (11:06 -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>
23 files changed:
Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
Open-ILS/webby-src/package.json
Open-ILS/webby-src/src/app/base.module.ts
Open-ILS/webby-src/src/app/core/README
Open-ILS/webby-src/src/app/core/auth.ts
Open-ILS/webby-src/src/app/core/net.service.ts [deleted file]
Open-ILS/webby-src/src/app/core/net.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/core/pcrud.ts
Open-ILS/webby-src/src/app/share/README [new file with mode: 0644]
Open-ILS/webby-src/src/app/share/unapi.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/admin/workstation/workstations.component.ts
Open-ILS/webby-src/src/app/staff/catalog/catalog.component.html [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/catalog.component.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/catalog.module.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/catalog.service.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/routing.module.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/search.component.html [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/catalog/search.component.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
Open-ILS/webby-src/src/app/staff/resolver.service.ts
Open-ILS/webby-src/src/app/staff/routing.module.ts
Open-ILS/webby-src/src/app/staff/splash.component.html
Open-ILS/webby-src/src/app/staff/staff.component.ts

index 1d97a49..f0e0185 100644 (file)
@@ -253,7 +253,7 @@ function(egCore , $q) {
 
                 var owner_name_list = [];
                 while (owner.parent_ou()) { // we're going to skip the top of the tree...
-                    owner_name_list.unshift(owner.name());
+                    owner_name_list.unshift(owner.shortname());
                     owner = egCore.org.get(owner.parent_ou());
                 }
 
index 88f8edc..41b5925 100644 (file)
@@ -36,6 +36,7 @@
     "@types/jasminewd2": "~2.0.2",
     "@types/jquery": "^3.2.16",
     "@types/node": "~6.0.60",
+    "@types/xml2js": "^0.4.2",
     "codelyzer": "~3.2.0",
     "jasmine-core": "~2.6.2",
     "jasmine-spec-reporter": "~4.1.0",
index 7f2df7c..f462f84 100644 (file)
@@ -8,6 +8,7 @@ import {NgModule} from '@angular/core';
 import {Router} from '@angular/router'; // Debugging
 import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
 import {CookieModule} from 'ngx-cookie'; // import CookieMonster
+import {HttpClientModule} from '@angular/common/http';
 
 import {EgBaseComponent} from './base.component';
 import {EgBaseRoutingModule} from './routing.module';
@@ -17,7 +18,7 @@ import {WelcomeComponent} from './welcome.component';
 import {EgEventService} from '@eg/core/event';
 import {EgStoreService} from '@eg/core/store';
 import {EgIdlService} from '@eg/core/idl';
-import {EgNetService} from '@eg/core/net.service';
+import {EgNetService} from '@eg/core/net';
 import {EgAuthService} from '@eg/core/auth';
 import {EgPcrudService} from '@eg/core/pcrud';
 import {EgOrgService} from '@eg/core/org';
@@ -31,7 +32,8 @@ import {EgOrgService} from '@eg/core/org';
     EgBaseRoutingModule,
     BrowserModule,
     NgbModule.forRoot(),
-    CookieModule.forRoot()
+    CookieModule.forRoot(),
+    HttpClientModule
   ],
   providers: [
     EgEventService,
index 68dff84..58828be 100644 (file)
@@ -1,5 +1,8 @@
-Core types (classes) and Angular services used by all modules.
+Core Angular services and assocated types/classes.
 
-NOTES:
+Core services are imported and exported by the base module, which means
+they are automatically added as dependencies to ALL applications.
+
+1. Only add services here that are universally required!
+2. Avoid path navigation in the core services as paths will vary by application.
 
-* Avoid path navigation in the core services as paths will vary by application.
index b544ae9..87d5c70 100644 (file)
@@ -3,7 +3,7 @@
  */
 import { Injectable, EventEmitter } from '@angular/core';
 import { Observable } from 'rxjs/Rx';
-import { EgNetService } from './net.service';
+import { EgNetService } from './net';
 import { EgEventService, EgEvent } from './event';
 import { EgIdlService, EgIdlObject } from './idl';
 import { EgStoreService } from './store';
diff --git a/Open-ILS/webby-src/src/app/core/net.service.ts b/Open-ILS/webby-src/src/app/core/net.service.ts
deleted file mode 100644 (file)
index b037de1..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-/**
- * 
- * constructor(private net : EgNetService) {
- *   ...
- *   egNet.request(service, method, param1 [, param2, ...])
- *     .subscribe(
- *       (res) => console.log('received one resopnse: ' + res),
- *       (err) => console.error('recived request error: ' + err),
- *       ()    => console.log('request complete')
- *     )
- *   );
- *   ...
- * }
- *
- * Each response is relayed via Observable onNext().  The interface is 
- * the same for streaming and atomic requests.
- */
-import { Injectable, EventEmitter } from '@angular/core';
-import { Observable, Observer } from 'rxjs/Rx';
-import { EgEventService, EgEvent } from './event';
-
-// Global vars from opensrf.js
-// These are availavble at runtime, but are not exported.
-declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
-
-export class EgNetRequest {
-    service    : String;
-    method     : String;
-    params     : any[];
-    observer   : Observer<any>;
-    superseded : Boolean = false;
-    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
-    session?   : any;
-
-    // Last EgEvent encountered by this request.
-    // Most callers will not need to import EgEvent since the parsed
-    // event will be available here.
-    evt: EgEvent;
-
-    constructor(service: String, method: String, params: any[], session?: any) {
-        this.service = service;
-        this.method = method;
-        this.params = params;
-        if (session) {
-            this.session = session;
-        } else {
-            this.session = new OpenSRF.ClientSession(service);
-        }
-    }
-}
-
-@Injectable()
-export class EgNetService {
-
-    permFailed$: EventEmitter<EgNetRequest>;
-    authExpired$: EventEmitter<EgNetRequest>;
-
-    // If true, permission failures are emitted via permFailed$ 
-    // and the active request is marked as superseded.
-    permFailedHasHandler: Boolean = false;
-
-    constructor(
-        private egEvt: EgEventService
-    ) { 
-        this.permFailed$ = new EventEmitter<EgNetRequest>();
-        this.authExpired$ = new EventEmitter<EgNetRequest>();
-    }
-
-    // Standard request call -- Variadic params version
-    request(service: String, method: String, ...params: any[]): Observable<any> {
-        return this.requestWithParamList(service, method, params);
-    }
-
-    // Array params version
-    requestWithParamList(service: String, 
-        method: String, params: any[]): Observable<any> {
-        return this.requestCompiled(
-            new EgNetRequest(service, method, params));
-    }
-
-    requestCompiled(request: EgNetRequest): Observable<any> {
-        return Observable.create(
-            observer => {
-                request.observer = observer;
-                this.sendCompiledRequest(request);
-            }
-        );
-    }
-
-    // Version with pre-compiled EgNetRequest object
-    sendCompiledRequest(request: EgNetRequest): void {
-        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
-        var this_ = this;
-
-        request.session.request({
-            async  : true,
-            method : request.method,
-            params : request.params,
-            oncomplete : function() {
-                // A superseded request will be complete()'ed by the 
-                // superseder at a later time.
-                if (!request.superseded)
-                    request.observer.complete();
-            },
-            onresponse : function(r) {
-                this_.dispatchResponse(request, r.recv().content());
-            },
-            onerror : function(errmsg) {
-                let msg = `${request.method} failed! See server logs. ${errmsg}`;
-                console.error(msg);
-                request.observer.error(msg);
-            },
-            onmethoderror : function(req, statCode, statMsg) { 
-                let msg = 
-                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
-                console.error(msg);
-
-                if (request.service == 'open-ils.pcrud' && statCode == 401) {
-                    // 401 is the PCRUD equivalent of a NO_SESSION event
-                    this_.authExpired$.emit(request);
-                }
-
-                request.observer.error(msg);
-            }
-
-        }).send();
-    }
-
-    // Relay response object to the caller for typical/successful responses.  
-    // Applies special handling to response events that require global attention.
-    private dispatchResponse = function(request, response) {
-        request.evt = this.egEvt.parse(response);
-
-        if (request.evt) {
-            switch(request.evt.textcode) {
-
-                case 'NO_SESSION':
-                    console.debug(`EgNet emitting event: ${request.evt}`);
-                    request.observer.error(request.evt.toString());
-                    this.authExpired$.emit(request);
-                    return;
-
-                case 'PERM_FAILURE':
-                    if (this.permFailedHasHandler) {
-                        console.debug(`EgNet emitting event: ${request.evt}`);
-                        request.superseded = true;
-                        this.permFailed$.emit(request);
-                        return;
-                    }
-            } 
-        }
-
-        // Pass the response to the caller.
-        request.observer.next(response);
-    };
-}
diff --git a/Open-ILS/webby-src/src/app/core/net.ts b/Open-ILS/webby-src/src/app/core/net.ts
new file mode 100644 (file)
index 0000000..b037de1
--- /dev/null
@@ -0,0 +1,156 @@
+/**
+ * 
+ * constructor(private net : EgNetService) {
+ *   ...
+ *   egNet.request(service, method, param1 [, param2, ...])
+ *     .subscribe(
+ *       (res) => console.log('received one resopnse: ' + res),
+ *       (err) => console.error('recived request error: ' + err),
+ *       ()    => console.log('request complete')
+ *     )
+ *   );
+ *   ...
+ * }
+ *
+ * Each response is relayed via Observable onNext().  The interface is 
+ * the same for streaming and atomic requests.
+ */
+import { Injectable, EventEmitter } from '@angular/core';
+import { Observable, Observer } from 'rxjs/Rx';
+import { EgEventService, EgEvent } from './event';
+
+// Global vars from opensrf.js
+// These are availavble at runtime, but are not exported.
+declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
+
+export class EgNetRequest {
+    service    : String;
+    method     : String;
+    params     : any[];
+    observer   : Observer<any>;
+    superseded : Boolean = false;
+    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
+    session?   : any;
+
+    // Last EgEvent encountered by this request.
+    // Most callers will not need to import EgEvent since the parsed
+    // event will be available here.
+    evt: EgEvent;
+
+    constructor(service: String, method: String, params: any[], session?: any) {
+        this.service = service;
+        this.method = method;
+        this.params = params;
+        if (session) {
+            this.session = session;
+        } else {
+            this.session = new OpenSRF.ClientSession(service);
+        }
+    }
+}
+
+@Injectable()
+export class EgNetService {
+
+    permFailed$: EventEmitter<EgNetRequest>;
+    authExpired$: EventEmitter<EgNetRequest>;
+
+    // If true, permission failures are emitted via permFailed$ 
+    // and the active request is marked as superseded.
+    permFailedHasHandler: Boolean = false;
+
+    constructor(
+        private egEvt: EgEventService
+    ) { 
+        this.permFailed$ = new EventEmitter<EgNetRequest>();
+        this.authExpired$ = new EventEmitter<EgNetRequest>();
+    }
+
+    // Standard request call -- Variadic params version
+    request(service: String, method: String, ...params: any[]): Observable<any> {
+        return this.requestWithParamList(service, method, params);
+    }
+
+    // Array params version
+    requestWithParamList(service: String, 
+        method: String, params: any[]): Observable<any> {
+        return this.requestCompiled(
+            new EgNetRequest(service, method, params));
+    }
+
+    requestCompiled(request: EgNetRequest): Observable<any> {
+        return Observable.create(
+            observer => {
+                request.observer = observer;
+                this.sendCompiledRequest(request);
+            }
+        );
+    }
+
+    // Version with pre-compiled EgNetRequest object
+    sendCompiledRequest(request: EgNetRequest): void {
+        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
+        var this_ = this;
+
+        request.session.request({
+            async  : true,
+            method : request.method,
+            params : request.params,
+            oncomplete : function() {
+                // A superseded request will be complete()'ed by the 
+                // superseder at a later time.
+                if (!request.superseded)
+                    request.observer.complete();
+            },
+            onresponse : function(r) {
+                this_.dispatchResponse(request, r.recv().content());
+            },
+            onerror : function(errmsg) {
+                let msg = `${request.method} failed! See server logs. ${errmsg}`;
+                console.error(msg);
+                request.observer.error(msg);
+            },
+            onmethoderror : function(req, statCode, statMsg) { 
+                let msg = 
+                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
+                console.error(msg);
+
+                if (request.service == 'open-ils.pcrud' && statCode == 401) {
+                    // 401 is the PCRUD equivalent of a NO_SESSION event
+                    this_.authExpired$.emit(request);
+                }
+
+                request.observer.error(msg);
+            }
+
+        }).send();
+    }
+
+    // Relay response object to the caller for typical/successful responses.  
+    // Applies special handling to response events that require global attention.
+    private dispatchResponse = function(request, response) {
+        request.evt = this.egEvt.parse(response);
+
+        if (request.evt) {
+            switch(request.evt.textcode) {
+
+                case 'NO_SESSION':
+                    console.debug(`EgNet emitting event: ${request.evt}`);
+                    request.observer.error(request.evt.toString());
+                    this.authExpired$.emit(request);
+                    return;
+
+                case 'PERM_FAILURE':
+                    if (this.permFailedHasHandler) {
+                        console.debug(`EgNet emitting event: ${request.evt}`);
+                        request.superseded = true;
+                        this.permFailed$.emit(request);
+                        return;
+                    }
+            } 
+        }
+
+        // Pass the response to the caller.
+        request.observer.next(response);
+    };
+}
index b6185cb..0cee7d3 100644 (file)
@@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
 import {Observable, Observer} from 'rxjs/Rx';
 //import {toPromise} from 'rxjs/operators';
 import {EgIdlService, EgIdlObject} from './idl';
-import {EgNetService, EgNetRequest} from './net.service';
+import {EgNetService, EgNetRequest} from './net';
 import {EgAuthService} from './auth';
 
 // Used for debugging.
diff --git a/Open-ILS/webby-src/src/app/share/README b/Open-ILS/webby-src/src/app/share/README
new file mode 100644 (file)
index 0000000..1a8b6e1
--- /dev/null
@@ -0,0 +1,7 @@
+Common Angular services and associated types/classes.  
+
+This collection of services MIGHT be used by practically all applications.
+They are NOT automatically imported/exported by the base module and should
+be loaded within the requesting application as needed.
+
+
diff --git a/Open-ILS/webby-src/src/app/share/unapi.ts b/Open-ILS/webby-src/src/app/share/unapi.ts
new file mode 100644 (file)
index 0000000..bf2f44c
--- /dev/null
@@ -0,0 +1,66 @@
+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
+https://library.biz/opac/extras/unapi?id=tag::U2@bre/1{bre.extern,holdings_xml,mra}/BR1/0&format=mods32
+*/
+
+const UNAPI_PATH = '/opac/extras/unapi?id=tag::U2@';
+
+interface EgUnapiParams {
+    target: string; // bre, ...
+    id: number | string; // 1 | 1,2,3,4,5
+    extras: string; // {holdings_xml,mra,...}
+    format: string; // mods32, marxml, ...
+    orgId?: number; // org unit ID
+    depth?: number; // org unit depth
+};
+
+@Injectable()
+export class EgUnapiService {
+
+    constructor(
+        private org: EgOrgService,
+        private http: HttpClient
+    ) {}
+
+    /**
+     * Retrieve an UNAPI document and return it as an XML string
+     */
+    getAsXmlString(params: EgUnapiParams): Promise<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}/` +
+            `${org.shortname()}/${depth}&format=${params.format}`;
+
+        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> {
+        return new Promise((resolve, reject) => {
+            this.getAsXmlString(params)
+            .then(xmlStr => {
+                new Parser().parseString(xmlStr, (err, unapiBlob) => {
+                    if (err) {
+                        reject(err);
+                    } else {
+                        resolve(unapiBlob);
+                    }
+                });
+            });
+        });
+    }
+}
+
+
index 861d388..27574b3 100644 (file)
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
 import {ActivatedRoute} from '@angular/router';
 import {EgStoreService} from '@eg/core/store';
 import {EgIdlObject} from '@eg/core/idl';
-import {EgNetService} from '@eg/core/net.service';
+import {EgNetService} from '@eg/core/net';
 import {EgAuthService} from '@eg/core/auth';
 import {EgOrgService} from '@eg/core/org';
 
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/catalog.component.html b/Open-ILS/webby-src/src/app/staff/catalog/catalog.component.html
new file mode 100644 (file)
index 0000000..024bf0b
--- /dev/null
@@ -0,0 +1,4 @@
+
+<h1>CATALOG</h1>
+
+<router-outlet></router-outlet>
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/catalog.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/catalog.component.ts
new file mode 100644 (file)
index 0000000..5b66182
--- /dev/null
@@ -0,0 +1,14 @@
+import {Component, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Rx';
+import {ActivatedRoute} from '@angular/router';
+
+@Component({
+  templateUrl: 'catalog.component.html'
+})
+export class EgCatalogComponent implements OnInit {
+    constructor() {}
+
+    ngOnInit() { }
+
+}
+
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/catalog.module.ts b/Open-ILS/webby-src/src/app/staff/catalog/catalog.module.ts
new file mode 100644 (file)
index 0000000..37d3131
--- /dev/null
@@ -0,0 +1,28 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {EgStaffModule} from '../staff.module';
+import {EgCatalogRoutingModule} from './routing.module';
+import {EgUnapiService} from '@eg/share/unapi';
+import {EgCatalogComponent} from './catalog.component';
+import {EgCatalogSearchComponent} from './search.component';
+import {EgCatalogService} from './catalog.service';
+
+@NgModule({
+  declarations: [
+    EgCatalogComponent,
+    EgCatalogSearchComponent
+  ],
+  imports: [
+    EgStaffModule,
+    CommonModule,
+    EgCatalogRoutingModule
+  ],
+  providers: [
+    EgUnapiService,
+    EgCatalogService
+  ]
+})
+
+export class EgCatalogModule { 
+
+}
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/catalog.service.ts b/Open-ILS/webby-src/src/app/staff/catalog/catalog.service.ts
new file mode 100644 (file)
index 0000000..d4f3581
--- /dev/null
@@ -0,0 +1,316 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {EgOrgService} from '@eg/core/org';
+import {EgUnapiService} from '@eg/share/unapi';
+import {EgIdlObject} from '@eg/core/idl';
+import {EgNetService} from '@eg/core/net';
+import {EgPcrudService} from '@eg/core/pcrud';
+
+const CCVM_FILTER_TYPES = [
+    'item_type',
+    'item_form',
+    'item_lang',
+    'audience',
+    'audience_group',
+    'vr_format',
+    'bib_level',
+    'lit_form',
+    'search_format'
+];
+
+class Paginator {
+    offset: number = 0;
+    limit: number = 15;
+    resultCount: number;
+
+    isFirstPage(): boolean {
+        return this.offset == 0;
+    }
+
+    isLastPage(): boolean {
+        return this.currentPage() == this.pageCount();
+    }
+
+    currentPage(): number {
+        return Math.floor(this.offset / this.limit) + 1
+    }
+
+    pageCount(): number {
+        let pages = this.resultCount / this.limit;
+        if (Math.floor(pages) < pages)
+            pages = Math.floor(pages) + 1;
+        return pages;
+    }
+
+    pageList(): number[] {
+        let list = [];
+        for(let i = 1; i <= this.pageCount(); i++)
+            list.push(i);
+        return list;
+    }
+}
+
+export class FacetFilter {
+    facetClass: string;
+    facetName: string;
+    facetValue: string;
+}
+
+export class CatalogContext {
+    available: boolean = false;
+    global: boolean = false;
+    sort: string;
+    searchClass: string[];
+    query: string[];
+    joiner: string[];
+    match: string[];
+    format: string;
+    searchOrg: EgIdlObject;
+    ccvmFilters: {[ccvm:string] : string} = {};
+    facetFilters: FacetFilter[] = [];
+    paginator: Paginator = new Paginator();
+    result: any;
+    org: EgOrgService;
+
+    compileSearch(): string {
+
+        let str: string = '';
+
+        if (this.available) str += ' #available';
+
+        if (this.sort) {
+            // e.g. title, title.descending
+            let parts = this.sort.split(/\./);
+            if (parts[1]) str += ' #descending';
+            str += ' sort(' + parts[0] + ')';
+        }
+
+        // -------
+        // Compile boolean sub-query components
+        if (str.length) str += ' ';
+        let qcount = this.query.length;
+
+        // if we multiple boolean query components, wrap them in parens.
+        if (qcount > 1) str += '(';
+        this.query.forEach((q, idx) => {
+            str += this.compileBoolQuerySet(idx)
+        });
+        if (qcount > 1) str += ')';
+        // -------
+
+        if (this.format) {
+            str += ' format(' + this.format + ')';
+        }
+
+        if (this.global) {
+            str += ' depth(' +
+                this.org.root().ou_type().depth() + ')';
+        }
+
+        str += ' site(' + this.searchOrg.shortname() + ')';
+
+        Object.keys(this.ccvmFilters).forEach(field => {
+            str += ' ' + field + '(' + this.ccvmFilters[field] + ')';
+        });
+
+        this.facetFilters.forEach(f => {
+            str += ' ' + f.facetClass + '|'
+                + f.facetName + '[' + f.facetValue + ']';
+        });
+
+        return str;
+    }
+
+    stripQuotes(query: string): string {
+        return query.replace(/"/g, ''); 
+    }
+
+    stripAnchors(query: string): string {
+        return query.replace(/[\^\$]/g, ''); 
+    }
+
+    addQuotes(query: string): string {
+        if (query.match(/ /))
+            return '"' + query + '"'
+        return query;
+    };
+
+    compileBoolQuerySet(idx: number): string {
+        let query = this.query[idx];
+        let joiner = this.joiner[idx];
+        let match = this.match[idx];
+        let searchClass = this.searchClass[idx];
+
+        let str = '';
+        if (!query) return str;
+
+        if (idx > 0) str += ' ' + joiner + ' ';
+
+        str += '(';
+        if (searchClass) str += searchClass + ':';
+
+        switch(match) {
+            case 'phrase':
+                query = this.addQuotes(this.stripQuotes(query));
+                break;
+            case 'nocontains':
+                query = '-' + this.addQuotes(this.stripQuotes(query));
+                break;
+            case 'exact':
+                query = '^' + this.stripAnchors(query) + '$';
+                break;
+            case 'starts':
+                query = this.addQuotes('^' + 
+                    this.stripAnchors(this.stripQuotes(query)));
+                break;
+        }
+
+        return str + query + ')';
+    }
+}
+
+
+@Injectable()
+export class EgCatalogService {
+
+    ccvmMap: {[ccvm:string] : EgIdlObject[]} = {};
+    cmfMap: {[cmf:string] : EgIdlObject[]} = {};
+
+    constructor(
+        private net: EgNetService,
+        private org: EgOrgService,
+        private unapi: EgUnapiService,
+        private pcrud: EgPcrudService
+    ) {}
+
+
+    search(context: CatalogContext): Promise<void> {
+
+        var fullQuery = context.compileSearch();
+
+        console.debug(`search query: ${fullQuery}`);
+
+        return new Promise((resolve, reject) => {
+
+            this.net.request(
+                'open-ils.search',
+                'open-ils.search.biblio.multiclass.query.staff', {
+                    limit : context.paginator.limit, 
+                    offset : context.paginator.offset
+                }, fullQuery, true
+            ).subscribe(result => {
+                context.result = result;
+                context.result.records = [];
+                context.paginator.resultCount = result.count;
+
+                let promises = [];
+                result.ids.forEach(blob => {
+                    promises.push(
+                        this.getBibSummary(blob[0], 
+                            context.searchOrg.id(), 
+                            context.global ? 
+                                context.org.root().ou_typ().depth() :
+                                context.searchOrg.ou_type().depth()
+                        ).then(
+                            summary => context.result.records.push(summary)
+                        )
+                    );
+                });
+
+                Promise.all(promises).then(ok => resolve());
+            });
+        })
+    }
+
+    fetchCcvms(): Promise<void> {
+
+        if (Object.keys(this.ccvmMap).length) 
+            return Promise.resolve();
+
+        return new Promise((resolve, reject) => {
+            this.pcrud.search('ccvm', 
+                {ctype : CCVM_FILTER_TYPES}, {}, {atomic: true}
+            ).subscribe(list => {
+                console.debug(list);
+                this.compileCcvms(list);
+                resolve();
+            })
+        });
+    }
+
+    compileCcvms(ccvms : EgIdlObject[]): void {
+        ccvms.forEach(ccvm => {
+            if (!this.ccvmMap[ccvm.ctype()])
+                this.ccvmMap[ccvm.ctype()] = [];
+            this.ccvmMap[ccvm.ctype()].push(ccvm);
+        });
+
+        Object.keys(this.ccvmMap).forEach(cType => {
+            this.ccvmMap[cType] = 
+                this.ccvmMap[cType].sort((a, b) => {
+                    return a.value() < b.value() ? -1 : 1;
+                });
+        });
+    }
+
+
+    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}
+            ).subscribe(
+                cmfs => {
+                    cmfs.forEach(c => this.cmfMap[c.id()] = c);
+                    resolve();
+                }
+            )
+        });
+    }
+
+    /**
+     * Probably don't want to require navigating the bare UNAPI
+     * blob in the template, plus that's quite a lot of stuff
+     * to sit in the scope / watch for changes.  Translate the 
+     * UNAPI content into a more digestable form.
+     * TODO: Add display field support
+     */
+    translateBibSummary(summary: any): any { // TODO: bib summary type
+        const UNAPI_PATHS = {
+            title : 'titleInfo[0].title[0]',
+            author: 'name[0].namePart[0]',
+            genre:  'genre[0]._'
+        }
+
+        let response = {};
+        for (let key in UNAPI_PATHS) {
+            try {
+                response[key] = eval(`summary.mods.${UNAPI_PATHS[key]}`);
+            } catch(E) {
+                response[key] ='';
+            }
+        }
+
+        return response;
+    }
+
+    getBibSummary(bibId: number, orgId: number, depth: number): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.unapi.getAsObject({
+                target: 'bre', 
+                id: bibId, 
+                extras: '{bre.extern,holdings_xml,mra}', 
+                format: 'mods32', 
+                orgId: orgId,
+                depth: depth
+            }).then(summary => {
+                summary = this.translateBibSummary(summary);
+                summary.id = bibId;
+                resolve(summary);
+            });
+        });
+    }
+
+}
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/routing.module.ts b/Open-ILS/webby-src/src/app/staff/catalog/routing.module.ts
new file mode 100644 (file)
index 0000000..3cafbb4
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {EgCatalogComponent} from './catalog.component';
+import {EgCatalogSearchComponent} from './search.component';
+
+const routes: Routes = [{ 
+  path: '',
+  component: EgCatalogComponent,
+  children : [{
+    path: 'search',
+    component: EgCatalogSearchComponent,
+  }]
+}];
+
+@NgModule({
+  imports: [ RouterModule.forChild(routes) ],
+  exports: [ RouterModule ]
+})
+
+export class EgCatalogRoutingModule {}
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/search.component.html b/Open-ILS/webby-src/src/app/staff/catalog/search.component.html
new file mode 100644 (file)
index 0000000..3e5aebc
--- /dev/null
@@ -0,0 +1,12 @@
+
+<h2>Search Sample</h2>
+
+<div *ngIf="context.result">
+  <div class="row" *ngFor="let record of context.result.records; let idx = index">
+    <div class="col-1">{{idx + 1}}</div>
+    <div class="col-3">{{record.title}}</div>
+    <div class="col-3">{{record.author}}</div>
+    <div class="col-3">{{record.genre}}</div>
+  </div>
+</div>
+
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/search.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/search.component.ts
new file mode 100644 (file)
index 0000000..39a6ec8
--- /dev/null
@@ -0,0 +1,40 @@
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {EgAuthService} from '@eg/core/auth';
+import {EgOrgService} from '@eg/core/org';
+import {EgCatalogService, CatalogContext} from './catalog.service';
+
+@Component({
+  templateUrl: 'search.component.html'
+})
+export class EgCatalogSearchComponent implements OnInit {
+
+    context: CatalogContext;
+
+    constructor(
+        private auth: EgAuthService,
+        private org: EgOrgService,
+        private cat: EgCatalogService
+    ) {}
+
+    ngOnInit() { 
+        this.context = new CatalogContext();
+
+        this.cat.fetchCcvms().then(ok => { // TODO: catalog resolver
+            console.log(this.cat.ccvmMap);
+
+            this.context.searchClass = ['keyword'];
+            this.context.query = ['piano'];
+            this.context.joiner = ['&&'];
+            this.context.match = ['contains'];
+            this.context.format = null;
+            this.context.org = this.org; // hmm, refactor maybe
+            this.context.searchOrg = this.org.get(4); // BR1
+
+            this.cat.search(this.context).then(ok => {
+                console.log('ALL DONE SEARCH');
+            });
+        });
+    }
+}
+
index 3cc225e..cfc2ee2 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
-import { EgNetService } from '@eg/core/net.service';
+import { EgNetService } from '@eg/core/net';
 import { EgAuthService } from '@eg/core/auth';
 
 @Component({
index a811ff9..492663f 100644 (file)
@@ -4,7 +4,7 @@ import { Observable, Observer } from 'rxjs/Rx';
 import { Router, Resolve, RouterStateSnapshot,
          ActivatedRouteSnapshot } from '@angular/router';
 import { EgStoreService } from '@eg/core/store';
-import { EgNetService } from '@eg/core/net.service';
+import { EgNetService } from '@eg/core/net';
 import { EgAuthService } from '@eg/core/auth';
 
 /**
index 31a42c3..eaeaafd 100644 (file)
@@ -26,6 +26,9 @@ const routes: Routes = [{
     path: 'circ',
     loadChildren : '@eg/staff/circ/circ.module#EgCircModule'
   }, {
+    path: 'catalog',
+    loadChildren : '@eg/staff/catalog/catalog.module#EgCatalogModule'
+  }, {
     path: 'admin',
     loadChildren : '@eg/staff/admin/admin.module#EgAdminModule'
   }]
index 85b2d35..0259031 100644 (file)
@@ -4,4 +4,7 @@
 Some links to test...
 
 <a routerLink="/staff/admin/workstation/workstations">Workstation Admin</a>
+<br/>
+<br/>
+<a routerLink="/staff/catalog/search">Catalog Test</a>
 
index 6989cd0..e4808a9 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, OnInit } from '@angular/core';
 import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
 import { EgAuthService, EgAuthWsState } from '@eg/core/auth';
-import { EgNetService } from '@eg/core/net.service';
+import { EgNetService } from '@eg/core/net';
 
 @Component({
   templateUrl: 'staff.component.html'