From: Bill Erickson Date: Wed, 13 Dec 2017 19:11:06 +0000 (-0500) Subject: LP#626157 Ang2 experiments X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=4109cf798ed4d72d7cc88961cb3b4cf9627d0030;p=working%2FEvergreen.git LP#626157 Ang2 experiments Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/eg2-src/.angular-cli.json b/Open-ILS/eg2-src/.angular-cli.json new file mode 100644 index 0000000000..a90b80061d --- /dev/null +++ b/Open-ILS/eg2-src/.angular-cli.json @@ -0,0 +1,60 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "project": { + "name": "eg" + }, + "apps": [ + { + "root": "src", + "outDir": "dist", + "assets": [ + "assets", + "favicon.ico" + ], + "index": "index.html", + "main": "main.ts", + "polyfills": "polyfills.ts", + "test": "test.ts", + "tsconfig": "tsconfig.app.json", + "testTsconfig": "tsconfig.spec.json", + "prefix": "app", + "styles": [ + "styles.css" + ], + "scripts": [], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" + } + } + ], + "e2e": { + "protractor": { + "config": "./protractor.conf.js" + } + }, + "lint": [ + { + "project": "src/tsconfig.app.json", + "exclude": "**/node_modules/**" + }, + { + "project": "src/tsconfig.spec.json", + "exclude": "**/node_modules/**" + }, + { + "project": "e2e/tsconfig.e2e.json", + "exclude": "**/node_modules/**" + } + ], + "test": { + "karma": { + "config": "./karma.conf.js" + } + }, + "defaults": { + "styleExt": "css", + "component": {} + } +} diff --git a/Open-ILS/eg2-src/.editorconfig b/Open-ILS/eg2-src/.editorconfig new file mode 100644 index 0000000000..6e87a003da --- /dev/null +++ b/Open-ILS/eg2-src/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/Open-ILS/eg2-src/.gitignore b/Open-ILS/eg2-src/.gitignore new file mode 100644 index 0000000000..54bfd2001e --- /dev/null +++ b/Open-ILS/eg2-src/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +# e2e +/e2e/*.js +/e2e/*.map + +# System Files +.DS_Store +Thumbs.db diff --git a/Open-ILS/eg2-src/README.adoc b/Open-ILS/eg2-src/README.adoc new file mode 100644 index 0000000000..fd58af9902 --- /dev/null +++ b/Open-ILS/eg2-src/README.adoc @@ -0,0 +1,17 @@ += EG Angular2 App = + +=== Apache Configuration === + +[source,conf] +--------------------------------------------------------------------- + + FallbackResource /webby/index.html + +--------------------------------------------------------------------- + +=== Transpile + Deploy in --watch mode for Dev === + +[source,sh] +--------------------------------------------------------------------- +ng build --deploy-url /webby/ --base-href /webby/ --output-path ../web/webby/ --watch +--------------------------------------------------------------------- diff --git a/Open-ILS/eg2-src/e2e/app.e2e-spec.ts b/Open-ILS/eg2-src/e2e/app.e2e-spec.ts new file mode 100644 index 0000000000..c2a69a8a6c --- /dev/null +++ b/Open-ILS/eg2-src/e2e/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('eg App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to app!'); + }); +}); diff --git a/Open-ILS/eg2-src/e2e/app.po.ts b/Open-ILS/eg2-src/e2e/app.po.ts new file mode 100644 index 0000000000..82ea75ba50 --- /dev/null +++ b/Open-ILS/eg2-src/e2e/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/Open-ILS/eg2-src/e2e/tsconfig.e2e.json b/Open-ILS/eg2-src/e2e/tsconfig.e2e.json new file mode 100644 index 0000000000..1d9e5edf09 --- /dev/null +++ b/Open-ILS/eg2-src/e2e/tsconfig.e2e.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/Open-ILS/eg2-src/karma.conf.js b/Open-ILS/eg2-src/karma.conf.js new file mode 100644 index 0000000000..af139fada3 --- /dev/null +++ b/Open-ILS/eg2-src/karma.conf.js @@ -0,0 +1,33 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular/cli'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular/cli/plugins/karma') + ], + client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + reports: [ 'html', 'lcovonly' ], + fixWebpackSourcePaths: true + }, + angularCli: { + environment: 'dev' + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/Open-ILS/eg2-src/package.json b/Open-ILS/eg2-src/package.json new file mode 100644 index 0000000000..41b5925989 --- /dev/null +++ b/Open-ILS/eg2-src/package.json @@ -0,0 +1,54 @@ +{ + "name": "eg", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^5.0.0", + "@angular/common": "^5.0.0", + "@angular/compiler": "^5.0.0", + "@angular/core": "^5.0.0", + "@angular/forms": "^5.0.0", + "@angular/http": "^5.0.0", + "@angular/platform-browser": "^5.0.0", + "@angular/platform-browser-dynamic": "^5.0.0", + "@angular/router": "^5.0.0", + "@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.5", + "core-js": "^2.4.1", + "jquery": "^3.2.1", + "ngx-cookie": "^2.0.1", + "rxjs": "^5.5.2", + "zone.js": "^0.8.14" + }, + "devDependencies": { + "@angular/cli": "1.5.1", + "@angular/compiler-cli": "^5.0.0", + "@angular/language-service": "^5.0.0", + "@types/jasmine": "~2.5.53", + "@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", + "karma": "~1.7.0", + "karma-chrome-launcher": "~2.1.1", + "karma-cli": "~1.0.1", + "karma-coverage-istanbul-reporter": "^1.2.1", + "karma-jasmine": "~1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.1.2", + "ts-node": "~3.2.0", + "tslint": "~5.7.0", + "typescript": "~2.4.2" + } +} diff --git a/Open-ILS/eg2-src/protractor.conf.js b/Open-ILS/eg2-src/protractor.conf.js new file mode 100644 index 0000000000..7ee3b5ee86 --- /dev/null +++ b/Open-ILS/eg2-src/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './e2e/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; diff --git a/Open-ILS/eg2-src/src/app/app.component.ts b/Open-ILS/eg2-src/src/app/app.component.ts new file mode 100644 index 0000000000..d049f7a828 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/app.component.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'eg-root', + template: '' +}) + +export class EgBaseComponent { +} + + diff --git a/Open-ILS/eg2-src/src/app/app.module.ts b/Open-ILS/eg2-src/src/app/app.module.ts new file mode 100644 index 0000000000..d9d06e3b27 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/app.module.ts @@ -0,0 +1,56 @@ +/** + * EgBaseModule is the shared starting point for all apps. + * It provides the root router and a simple welcome page for + * users that end up here accidentally. + */ +import {BrowserModule} from '@angular/platform-browser'; +import {NgModule} from '@angular/core'; +import {Router} from '@angular/router'; // Debugging +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; // ng-bootstrap +import {CookieModule} from 'ngx-cookie'; // import CookieMonster + +import {EgBaseComponent} from './app.component'; +import {EgBaseRoutingModule} from './routing.module'; +import {WelcomeComponent} from './welcome.component'; + +// Import and 'provide' globally required services. +import {EgEventService} from '@eg/core/event'; +import {EgStoreService} from '@eg/core/store'; +import {EgIdlService} from '@eg/core/idl'; +import {EgNetService} from '@eg/core/net'; +import {EgAuthService} from '@eg/core/auth'; +import {EgPcrudService} from '@eg/core/pcrud'; +import {EgOrgService} from '@eg/core/org'; + +@NgModule({ + declarations: [ + EgBaseComponent, + WelcomeComponent + ], + imports: [ + EgBaseRoutingModule, + BrowserModule, + NgbModule.forRoot(), + CookieModule.forRoot() + ], + providers: [ + EgEventService, + EgStoreService, + EgIdlService, + EgNetService, + EgAuthService, + EgPcrudService, + EgOrgService + ], + exports: [], + bootstrap: [EgBaseComponent] +}) + +export class EgBaseModule { + constructor(router: Router) { + /* + console.debug('Routes: ', + JSON.stringify(router.config, undefined, 2)); + */ + } +} diff --git a/Open-ILS/eg2-src/src/app/core/README b/Open-ILS/eg2-src/src/app/core/README new file mode 100644 index 0000000000..58828bed85 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/README @@ -0,0 +1,8 @@ +Core Angular services and assocated types/classes. + +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. + diff --git a/Open-ILS/eg2-src/src/app/core/auth.ts b/Open-ILS/eg2-src/src/app/core/auth.ts new file mode 100644 index 0000000000..611797abeb --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/auth.ts @@ -0,0 +1,240 @@ +/** + * + */ +import { Injectable, EventEmitter } from '@angular/core'; +import { Observable } from 'rxjs/Rx'; +import { EgNetService } from './net'; +import { EgEventService, EgEvent } from './event'; +import { EgIdlService, EgIdlObject } from './idl'; +import { EgStoreService } from './store'; + +// Models a login instance. +class EgAuthUser { + user: EgIdlObject; + workstation: string; // workstation name + token: string; + authtime: number; + + constructor(token: string, authtime: number, workstation?: string) { + this.token = token; + this.workstation = workstation; + this.authtime = authtime; + } +} + +// Params required for calling the login() method. +interface EgAuthLoginArgs { + username: string, + password: string, + type: string, + workstation?: string +} + +export enum EgAuthWsState { + PENDING, + NOT_USED, + NOT_FOUND_SERVER, + NOT_FOUND_LOCAL, + VALID +}; + +@Injectable() +export class EgAuthService { + + private activeUser: EgAuthUser; + + // opChangeUser refers to the user that has been superseded during + // an op-change event. This use will become the activeUser once + // again, when the op-change cycle has completed. + private opChangeUser: EgAuthUser; + + workstationState: EgAuthWsState = EgAuthWsState.PENDING; + + redirectUrl: string; + + constructor( + private egEvt: EgEventService, + private net: EgNetService, + private store: EgStoreService + ) {} + + // - Accessor functions alway refer to the active user. + + user(): EgIdlObject { + return this.activeUser.user + }; + + // Workstation name. + workstation(): string { + return this.activeUser.workstation; + }; + + token(): string { + return this.activeUser ? this.activeUser.token : null; + }; + + authtime(): Number { + return this.activeUser.authtime + }; + + // NOTE: EgNetService emits an event if the auth session has expired. + testAuthToken(): Promise { + + this.activeUser = new EgAuthUser( + this.store.getLoginSessionItem('eg.auth.token'), + this.store.getLoginSessionItem('eg.auth.time') + ); + + if (!this.token()) return Promise.reject('no authtoken'); + + return new Promise( (resolve, reject) => { + this.net.request( + 'open-ils.auth', + 'open-ils.auth.session.retrieve', this.token() + ).subscribe( + user => { + // EgNetService interceps NO_SESSION events. + // We can only get here if the session is valid. + this.activeUser.user = user; + this.sessionPoll(); + resolve(); + }, + err => { reject(); } + ); + }); + } + + checkWorkstation(): void { + // TODO: + // Emits event on invalid workstation. + } + + login(args: EgAuthLoginArgs, isOpChange?: boolean): Promise { + + return new Promise((resolve, reject) => { + this.net.request('open-ils.auth', 'open-ils.auth.login', args) + .subscribe(res => { + this.handleLoginResponse(args, this.egEvt.parse(res), isOpChange) + .then( + ok => resolve(ok), + notOk => reject(notOk) + ); + }); + }); + } + + handleLoginResponse( + args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise { + + switch (evt.textcode) { + case 'SUCCESS': + this.handleLoginOk(args, evt, isOpChange); + return Promise.resolve(); + + case 'WORKSTATION_NOT_FOUND': + console.error(`No such workstation "${args.workstation}"`); + this.workstationState = EgAuthWsState.NOT_FOUND_SERVER; + delete args.workstation; + return this.login(args, isOpChange); + + default: + console.error(`Login returned unexpected event: ${evt}`); + return Promise.reject('login failed'); + } + } + + // Stash the login data + handleLoginOk(args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): void { + + if (isOpChange) { + this.store.setLoginSessionItem('eg.auth.token.oc', this.token()); + this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime()); + this.opChangeUser = this.activeUser; + } + + this.activeUser = new EgAuthUser( + evt.payload.authtoken, + evt.payload.authtime, + args.workstation + ); + + this.store.setLoginSessionItem('eg.auth.token', this.token()); + this.store.setLoginSessionItem('eg.auth.time', this.authtime()); + } + + undoOpChange(): Promise { + if (this.opChangeUser) { + this.deleteSession(); + this.activeUser = this.opChangeUser; + this.opChangeUser = null; + this.store.removeLoginSessionItem('eg.auth.token.oc'); + this.store.removeLoginSessionItem('eg.auth.time.oc'); + this.store.setLoginSessionItem('eg.auth.token', this.token()); + this.store.setLoginSessionItem('eg.auth.time', this.authtime()); + } + return this.testAuthToken(); + } + + sessionPoll(): void { + // TODO + } + + // Resolves if login workstation matches a workstation known to this + // browser instance. + verifyWorkstation(): Promise { + return new Promise((resolve, reject) => { + + if (!this.user()) { + this.workstationState = EgAuthWsState.PENDING; + reject(); + return; + } + + if (!this.user().wsid()) { + this.workstationState = EgAuthWsState.NOT_USED; + reject(); + return; + } + + this.store.getItem('eg.workstation.all') + .then(workstations => { + if (!workstations) workstations = []; + + let ws = workstations.filter( + w => {return w.id == this.user().wsid()})[0]; + + if (ws) { + this.activeUser.workstation = ws.name; + this.workstationState = EgAuthWsState.VALID; + resolve(); + } else { + this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL; + reject(); + } + }); + }); + } + + deleteSession(): void { + if (this.token()) { + this.net.request( + 'open-ils.auth', + 'open-ils.auth.session.delete', this.token()) + .subscribe(x => console.debug('logged out')) + } + } + + logout(broadcast?: boolean) { + console.debug('logging out'); + + if (broadcast) { + // TODO + //this.authChannel.postMessage({action : 'logout'}); + } + + this.deleteSession(); + this.store.clearLoginSessionItems(); + this.activeUser = null; + this.opChangeUser = null; + } +} diff --git a/Open-ILS/eg2-src/src/app/core/event.ts b/Open-ILS/eg2-src/src/app/core/event.ts new file mode 100644 index 0000000000..3f6afc7d30 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/event.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +export class EgEvent { + code : Number; + textcode : String; + payload : any; + desc : String; + debug : String; + note : String; + servertime : String; + ilsperm : String; + ilspermloc : Number; + success : Boolean = false; + + toString(): String { + let s = `Event: ${this.code}:${this.textcode} -> ${this.desc}`; + if (this.ilsperm) + s += ` ${this.ilsperm}@${this.ilspermloc}`; + if (this.note) + s += `\n${this.note}`; + return s; + } +} + +@Injectable() +export class EgEventService { + + /** + * Returns an EgEvent if 'thing' is an event, null otherwise. + */ + parse(thing: any): EgEvent { + + // All events have a textcode + if (thing && typeof thing == 'object' && 'textcode' in thing) { + + let evt = new EgEvent(); + + ['textcode','payload','desc','note','servertime','ilsperm'] + .forEach(field => { evt[field] = thing[field]; }); + + evt.debug = thing.stacktrace; + evt.code = new Number(thing.code); + evt.ilspermloc = new Number(thing.ilspermloc); + evt.success = thing.textcode == 'SUCCESS'; + + return evt; + } + + return null; + } +} + + diff --git a/Open-ILS/eg2-src/src/app/core/idl.ts b/Open-ILS/eg2-src/src/app/core/idl.ts new file mode 100644 index 0000000000..8f46933954 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/idl.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; + +// Added globally by /IDL2js +declare var _preload_fieldmapper_IDL: Object; + +/** + * NOTE: To achieve full type strictness and avoid compile warnings, + * we would likely have to pre-compile the IDL down to a .ts file with all + * of the IDL class and field definitions. + */ + +/** + * Every IDL object class implements this interface. + */ +export interface EgIdlObject { + a: any[]; + classname: String; + _isfieldmapper: Boolean; + // Dynamically appended functions from the IDL. + [fields: string]: any; +} + +@Injectable() +export class EgIdlService { + + classes = {}; // IDL class metadata + constructors = {}; // IDL instance generators + + /** + * Create a new IDL object instance. + */ + create(cls: string, seed?:any[]): EgIdlObject { + if (this.constructors[cls]) + return new this.constructors[cls](seed); + throw new Error(`No such IDL class ${cls}`); + } + + parseIdl(): void { + + try { + this.classes = _preload_fieldmapper_IDL; + } catch (E) { + console.error('IDL (IDL2js) not found. Is the system running?'); + return; + } + + /** + * Creates the class constructor and getter/setter + * methods for each IDL class. + */ + let mkclass = (cls, fields) => { + this.classes[cls].classname = cls; + + // This dance lets us encode each IDL object with the + // EgIdlObject interface. Useful for adding type restrictions + // where desired for functions, etc. + let generator:any = ((): EgIdlObject => { + + var x:any = function(seed) { + this.a = seed || []; + this.classname = cls; + this._isfieldmapper = true; + }; + + fields.forEach(function(field, idx) { + x.prototype[field.name] = function(n) { + if (arguments.length==1) this.a[idx] = n; + return this.a[idx]; + } + }); + + return x; + }); + + this.constructors[cls] = generator(); + + // global class constructors required for JSON_v1.js + // TODO: polluting the window namespace w/ every IDL class + // is less than ideal. + window[cls] = this.constructors[cls]; + } + + for (var cls in this.classes) + mkclass(cls, this.classes[cls].fields); + }; +} + diff --git a/Open-ILS/eg2-src/src/app/core/net.ts b/Open-ILS/eg2-src/src/app/core/net.ts new file mode 100644 index 0000000000..b037de1840 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/net.ts @@ -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; + 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; + authExpired$: EventEmitter; + + // 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(); + this.authExpired$ = new EventEmitter(); + } + + // Standard request call -- Variadic params version + request(service: String, method: String, ...params: any[]): Observable { + return this.requestWithParamList(service, method, params); + } + + // Array params version + requestWithParamList(service: String, + method: String, params: any[]): Observable { + return this.requestCompiled( + new EgNetRequest(service, method, params)); + } + + requestCompiled(request: EgNetRequest): Observable { + 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/eg2-src/src/app/core/org.ts b/Open-ILS/eg2-src/src/app/core/org.ts new file mode 100644 index 0000000000..44eddd6f62 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/org.ts @@ -0,0 +1,165 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs/Rx'; +import {EgIdlObject, EgIdlService} from './idl'; +import {EgPcrudService} from './pcrud'; + +type EgOrgNodeOrId = number | EgIdlObject; + +interface OrgFilter { + canHaveUsers?: boolean; + canHaveVolumes?: boolean; + opacVisible?: boolean; +} + +@Injectable() +export class EgOrgService { + + private orgMap = {}; + private orgList: EgIdlObject[] = []; + private orgTree: EgIdlObject; // root node + children + + constructor( + private pcrud: EgPcrudService + ) {} + + get(nodeOrId: EgOrgNodeOrId): EgIdlObject { + if (typeof nodeOrId == 'object') + return nodeOrId; + return this.orgMap[nodeOrId]; + }; + + list(): EgIdlObject[] { + return this.orgList; + }; + + /** + * Returns a list of org units that match the selected criteria. + * Unset filter options are ignored. + */ + filterList(filter: OrgFilter, asId: boolean): any[] { + let list = []; + this.list().forEach(org => { + + let chu = filter.canHaveUsers; + if (chu && !this.canHaveUsers(org)) return; + if (chu === false && this.canHaveUsers(org)) return; + + let chv = filter.canHaveVolumes; + if (chv && !this.canHaveVolumes(org)) return; + if (chv === false && this.canHaveVolumes(org)) return; + + let ov = filter.opacVisible; + if (ov && !this.opacVisible(org)) return; + if (ov === false && this.opacVisible(org)) return; + + // All filter tests passed. Add it to the list + list.push(asId ? org.id() : org); + }); + + return list; + } + + tree(): EgIdlObject { + return this.orgTree; + } + + // get the root OU + root(): EgIdlObject { + return this.orgList[0]; + } + + // list of org_unit objects or IDs for ancestors + me + ancestors(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { + let node = this.get(nodeOrId); + if (!node) return []; + let nodes = [node]; + while( (node = this.get(node.parent_ou()))) + nodes.push(node); + if (asId) return nodes.map(n => n.id()); + return nodes; + }; + + // tests that a node can have users + canHaveUsers(nodeOrId): boolean { + return this + .get(nodeOrId) + .ou_type() + .can_have_users() == 't'; + } + + // tests that a node can have volumes + canHaveVolumes(nodeOrId): boolean { + return this + .get(nodeOrId) + .ou_type() + .can_have_vols() == 't'; + } + + opacVisible(nodeOrId): boolean { + return this.get(nodeOrId).opac_visible() == 't'; + } + + // list of org_unit objects or IDs for me + descendants + descendants(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { + let node = this.get(nodeOrId); + if (!node) return []; + let nodes = []; + function descend(n) { + nodes.push(n); + n.children().forEach(descend); + } + descend(node); + if (asId) + return nodes.map(function(n){return n.id()}); + return nodes; + } + + // list of org_unit objects or IDs for ancestors + me + descendants + fullPath(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { + let list = this.ancestors(nodeOrId, false).concat( + this.descendants(nodeOrId, false).slice(1)); + if (asId) + return list.map(function(n){return n.id()}); + return list; + } + + sortTree(sortField?: string, node?: EgIdlObject): void { + if (!sortField) sortField = 'shortname'; + if (!node) node = this.orgTree; + node.children( + node.children.sort((a, b) => { + return a[sortField]() < b[sortField]() ? -1 : 1 + }) + ); + node.children.forEach(n => this.sortTree(n)); + } + + absorbTree(node?: EgIdlObject): void { + if (!node) { + node = this.orgTree; + this.orgMap = {}; + this.orgList = []; + } + this.orgMap[node.id()] = node; + this.orgList.push(node); + node.children().forEach(c => this.absorbTree(c)); + } + + /** + * Grabs all of the org units from the server, chops them up into + * various shapes, then returns an "all done" promise. + */ + fetchOrgs(): Promise { + return this.pcrud.search('aou', {parent_ou : null}, + {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}, + {anonymous : true} + ).toPromise().then(tree => { + // ingest tree, etc. + this.orgTree = tree; + this.absorbTree(); + }); + } + + // NOTE: see ./org-settings.service for settings + // TODO: ^-- +} diff --git a/Open-ILS/eg2-src/src/app/core/pcrud.ts b/Open-ILS/eg2-src/src/app/core/pcrud.ts new file mode 100644 index 0000000000..0cee7d3073 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/pcrud.ts @@ -0,0 +1,311 @@ +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'; +import {EgAuthService} from './auth'; + +// Used for debugging. +declare var js2JSON: (jsThing:any) => string; +declare var OpenSRF: any; // creating sessions + +export interface EgPcrudReqOps { + authoritative?: boolean; + anonymous?: boolean; + idlist?: boolean; + atomic?: boolean; +} + +// For for documentation purposes. +type EgPcrudResponse = any; + +export class EgPcrudContext { + + static verboseLogging: boolean = true; // + static identGenerator: number = 0; // for debug logging + + private ident: number; + private authoritative: boolean; + private xactCloseMode: string; + private cudIdx: number; + private cudAction: string; + private cudLast: EgPcrudResponse; + private cudList: EgIdlObject[]; + + private idl: EgIdlService; + private net: EgNetService; + private auth: EgAuthService; + + // Tracks nested CUD actions + cudObserver: Observer; + + session: any; // OpenSRF.ClientSession + + constructor( // passed in by parent service -- not injected + egIdl: EgIdlService, + egNet: EgNetService, + egAuth: EgAuthService + ) { + this.idl = egIdl; + this.net = egNet; + this.auth = egAuth; + this.xactCloseMode = 'rollback'; + this.ident = EgPcrudContext.identGenerator++; + this.session = new OpenSRF.ClientSession('open-ils.pcrud'); + } + + toString(): string { + return '[PCRUDContext ' + this.ident + ']'; + } + + log(msg: string): void { + if (EgPcrudContext.verboseLogging) + console.debug(this + ': ' + msg); + } + + err(msg: string): void { + console.error(this + ': ' + msg); + } + + token(reqOps?: EgPcrudReqOps): string { + return (reqOps && reqOps.anonymous) ? + 'ANONYMOUS' : this.auth.token(); + } + + connect(): Promise { + this.log('connect'); + return new Promise( (resolve, reject) => { + this.session.connect({ + onconnect : () => { resolve(this); } + }); + }) + } + + disconnect(): void { + this.log('disconnect'); + this.session.disconnect(); + } + + retrieve(fmClass: string, pkey: Number | string, + pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { + if (!reqOps) reqOps = {}; + this.authoritative = reqOps.authoritative || false; + return this.dispatch( + `open-ils.pcrud.retrieve.${fmClass}`, + [this.token(reqOps), pkey, pcrudOps]); + } + + retrieveAll(fmClass: string, pcrudOps?: any, + reqOps?: EgPcrudReqOps): Observable { + let search = {}; + search[this.idl.classes[fmClass].pkey] = {'!=' : null}; + return this.search(fmClass, search, pcrudOps, reqOps); + } + + search(fmClass: string, search: any, + pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { + reqOps = reqOps || {}; + this.authoritative = reqOps.authoritative || false; + + let returnType = reqOps.idlist ? 'id_list' : 'search'; + let method = `open-ils.pcrud.${returnType}.${fmClass}`; + + if (reqOps.atomic) method += '.atomic'; + + return this.dispatch(method, [this.token(reqOps), search, pcrudOps]); + } + + create(list: EgIdlObject[]): Observable { + return this.cud('create', list) + } + update(list: EgIdlObject[]): Observable { + return this.cud('update', list) + } + remove(list: EgIdlObject[]): Observable { + return this.cud('delete', list) + } + autoApply(list: EgIdlObject[]): Observable { // RENAMED + return this.cud('auto', list) + } + + xactClose(): Observable { + return this.sendRequest( + 'open-ils.pcrud.transaction.' + this.xactCloseMode, + [this.token()] + ); + }; + + xactBegin(): Observable { + return this.sendRequest( + 'open-ils.pcrud.transaction.begin', [this.token()] + ); + }; + + private dispatch(method: string, params: any[]): Observable { + if (this.authoritative) { + return this.wrapXact(() => { + return this.sendRequest(method, params); + }); + } else { + return this.sendRequest(method, params) + } + }; + + + // => connect + // => xact_begin + // => action + // => xact_close(commit/rollback) + // => disconnect + wrapXact(mainFunc: () => Observable): Observable { + let this_ = this; + + return Observable.create(observer => { + + // 1. connect + this.connect() + + // 2. start the transaction + .then(() => {return this_.xactBegin().toPromise()}) + + // 3. execute the main body + .then(() => { + + mainFunc().subscribe( + res => observer.next(res), + err => observer.error(err), + () => { + this_.xactClose().toPromise().then(() => { + // 5. disconnect + this_.disconnect(); + // 6. all done + observer.complete(); + }); + } + ); + }) + }); + }; + + private sendRequest(method: string, + params: any[]): Observable { + + this.log(`sendRequest(${method})`); + + return this.net.requestCompiled( + new EgNetRequest( + 'open-ils.pcrud', method, params, this.session) + ); + } + + private cud(action: string, + list: EgIdlObject | EgIdlObject[]): Observable { + + this.log(`CUD(): ${action}`); + + this.cudIdx = 0; + this.cudAction = action; + this.xactCloseMode = 'commit'; + + if (!Array.isArray(list)) this.cudList = [list]; + + let this_ = this; + + return this.wrapXact(() => { + return Observable.create(observer => { + this_.cudObserver = observer; + this_.nextCudRequest(); + }); + }); + } + + /** + * Loops through the list of objects to update and sends + * them one at a time to the server for processing. Once + * all are done, the cudObserver is resolved. + */ + nextCudRequest(): void { + let this_ = this; + + if (this.cudIdx >= this.cudList.length) { + this.cudObserver.complete(); + return; + } + + let action = this.cudAction; + let fmObj = this.cudList[this.cudIdx++]; + + if (action == 'auto') { + if (fmObj.ischanged()) action = 'update'; + if (fmObj.isnew()) action = 'create'; + if (fmObj.isdeleted()) action = 'delete'; + + if (action == 'auto') { + // object does not need updating; move along + this.nextCudRequest(); + } + } + + this.sendRequest( + `open-ils.pcrud.${action}.${fmObj.classname}`, + [this.token(), fmObj] + ).subscribe( + res => this_.cudObserver.next(res), + err => this_.cudObserver.error(err), + () => this_.nextCudRequest() + ); + }; +} + +@Injectable() +export class EgPcrudService { + + constructor( + private idl: EgIdlService, + private net: EgNetService, + private auth: EgAuthService + ) {} + + // Pass-thru functions for one-off PCRUD calls + + connect(): Promise { + return this.newContext().connect(); + } + + newContext(): EgPcrudContext { + return new EgPcrudContext(this.idl, this.net, this.auth); + } + + retrieve(fmClass: string, pkey: Number | string, + pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { + return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps); + } + + retrieveAll(fmClass: string, pcrudOps?: any, + reqOps?: EgPcrudReqOps): Observable { + return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps); + } + + search(fmClass: string, search: any, + pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { + return this.newContext().search(fmClass, search, pcrudOps, reqOps); + } + + create(list: EgIdlObject[]): Observable { + return this.newContext().create(list); + } + + update(list: EgIdlObject[]): Observable { + return this.newContext().update(list); + } + + remove(list: EgIdlObject[]): Observable { + return this.newContext().remove(list); + } + + autoApply(list: EgIdlObject[]): Observable { + return this.newContext().autoApply(list); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/core/store.ts b/Open-ILS/eg2-src/src/app/core/store.ts new file mode 100644 index 0000000000..e1a879b390 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/core/store.ts @@ -0,0 +1,115 @@ +/** + * Store and retrieve data from various sources. + */ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Rx'; +import { CookieService } from 'ngx-cookie'; + +@Injectable() +export class EgStoreService { + + // Base path for cookie-based storage. + // Useful for limiting cookies to subsections of the application. + loginSessionBasePath: string; + + // Set of keys whose values should disappear at logout. + loginSessionKeys: string[] = [ + 'eg.auth.token', + 'eg.auth.time', + 'eg.auth.token.oc', + 'eg.auth.time.oc' + ]; + + constructor(private cookieService: CookieService) {} + + private parseJson(valJson: string): any { + if (valJson == null || valJson == '') return null; + try { + return JSON.parse(valJson); + } catch(E) { + console.error(`Failure to parse JSON: ${E} => ${valJson}`); + return null; + } + } + + /** + * Add a an app-local login session key + */ + addLoginSessionKey(key: string): void { + this.loginSessionKeys.push(key); + } + + setItem(key: string, val: any, isJson?: Boolean): Promise { + // TODO: route keys appropriately + this.setLocalItem(key, val, false); + return Promise.resolve(); + } + + setLocalItem(key: string, val: any, isJson?: Boolean): void { + if (!isJson) val = JSON.stringify(val); + window.localStorage.setItem(key, val); + } + + setServerItem(key: string, val: any): Promise { + return Promise.resolve(); + } + + setSessionItem(key: string, val: any, isJson?: Boolean): void { + if (!isJson) val = JSON.stringify(val); + window.sessionStorage.setItem(key, val); + } + + setLoginSessionItem(key: string, val: any, isJson?:Boolean): void { + if (!isJson) val = JSON.stringify(val); + this.cookieService.put(key, val, {path : this.loginSessionBasePath}); + } + + getItem(key: string): Promise { + // TODO: route keys appropriately + return Promise.resolve(this.getLocalItem(key)); + } + + getLocalItem(key: string): any { + return this.parseJson(window.localStorage.getItem(key)); + } + + getServerItem(key: string): Promise { + return Promise.resolve(); + } + + getSessionItem(key: string): any { + return this.parseJson(window.sessionStorage.getItem(key)); + } + + getLoginSessionItem(key: string): any { + return this.parseJson(this.cookieService.get(key)); + } + + removeItem(key: string): Promise { + // TODO: route keys appropriately + return Promise.resolve(this.removeLocalItem(key)); + } + + removeLocalItem(key: string): void { + window.localStorage.removeItem(key); + } + + removeServerItem(key: string): Promise { + return Promise.resolve(); + } + + removeSessionItem(key: string): void { + window.sessionStorage.removeItem(key); + } + + removeLoginSessionItem(key: string): void { + this.cookieService.remove(key, {path : this.loginSessionBasePath}); + } + + clearLoginSessionItems(): void { + this.loginSessionKeys.forEach( + key => this.removeLoginSessionItem(key) + ); + } +} + diff --git a/Open-ILS/eg2-src/src/app/resolver.service.ts b/Open-ILS/eg2-src/src/app/resolver.service.ts new file mode 100644 index 0000000000..7ffa74bccb --- /dev/null +++ b/Open-ILS/eg2-src/src/app/resolver.service.ts @@ -0,0 +1,31 @@ +import {Injectable} from '@angular/core'; +import {Router, Resolve, RouterStateSnapshot, + ActivatedRouteSnapshot} from '@angular/router'; +import {EgIdlService} from '@eg/core/idl'; +import {EgOrgService} from '@eg/core/org'; + +@Injectable() +export class EgBaseResolver implements Resolve> { + + constructor( + private router: Router, + private idl: EgIdlService, + private org: EgOrgService, + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + + console.debug('EgBaseResolver:resolve()'); + + // Load data common to all applications. + + this.idl.parseIdl(); + + return this.org.fetchOrgs(); + // Note that authentication happens at a deeper level, since + // some applications (e.g. a public catalog) do not require + // up-front authentication to access. + } +} diff --git a/Open-ILS/eg2-src/src/app/routing.module.ts b/Open-ILS/eg2-src/src/app/routing.module.ts new file mode 100644 index 0000000000..7d7e70e71f --- /dev/null +++ b/Open-ILS/eg2-src/src/app/routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { EgBaseResolver } from './resolver.service'; +import { WelcomeComponent } from './welcome.component'; + +/** + * Avoid requiring all apps to load all JS by lazy-loading sub-modules. + * When lazy loading, no module references should be directly imported. + * The refs are encoded in the loadChildren attribute of each route. + */ +const routes: Routes = [ + { path: '', + component: WelcomeComponent + }, { + path: 'staff', + resolve : {startup : EgBaseResolver}, + loadChildren: './staff/app.module#EgStaffModule' + } +]; + +@NgModule({ + imports: [ RouterModule.forRoot(routes) ], + exports: [ RouterModule ], + providers: [ EgBaseResolver ] +}) + +export class EgBaseRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/share/README b/Open-ILS/eg2-src/src/app/share/README new file mode 100644 index 0000000000..1a8b6e1646 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/README @@ -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/eg2-src/src/app/share/catalog/catalog-url.service.ts b/Open-ILS/eg2-src/src/app/share/catalog/catalog-url.service.ts new file mode 100644 index 0000000000..00f3203956 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/catalog/catalog-url.service.ts @@ -0,0 +1,128 @@ +import {Injectable} from '@angular/core'; +import {ParamMap} from '@angular/router'; +import {EgOrgService} from '@eg/core/org'; +import {CatalogSearchContext, FacetFilter} from './search-context'; +import {CATALOG_CCVM_FILTERS} from './catalog.service'; + +@Injectable() +export class EgCatalogUrlService { + + // consider supporting a param name prefix/namespace + + constructor(private org: EgOrgService) { } + + /** + * Returns a URL query structure suitable for using with + * router.navigate(..., {queryParams:...}). + * No navigation is performed within. + */ + toUrlParams(context: CatalogSearchContext): + {[key: string]: string | string[]} { + + let params = { + query: [], + fieldClass: [], + joinOp: [], + matchOp: [], + facets: [], + org: null, + limit: null, + offset: null + }; + + params.limit = context.pager.limit; + if (context.pager.offset) + params.offset = context.pager.offset; + + // These fields can be copied directly into place + ['format','sort','available','global'] + .forEach(field => { + if (context[field]) { + // Only propagate applied values to the URL. + params[field] = context[field]; + } + }); + + context.query.forEach((q, idx) => { + ['query', 'fieldClass','joinOp','matchOp'].forEach(field => { + // Propagate all array-based fields regardless of + // whether a value is applied to ensure correct + // correlation between values. + params[field][idx] = context[field][idx]; + }); + }); + + // CCVM filters are encoded as comma-separated lists + Object.keys(context.ccvmFilters).forEach(code => { + if (context.ccvmFilters[code] && + context.ccvmFilters[code][0] != '') { + params[code] = context.ccvmFilters[code].join(','); + } + }); + + // Each facet is a JSON encoded blob of class, name, and value + context.facetFilters.forEach(facet => { + params.facets.push(JSON.stringify({ + c : facet.facetClass, + n : facet.facetName, + v : facet.facetValue + })); + }); + + params.org = context.searchOrg.id(); + + return params; + } + + /** + * Creates a new search context from the active route params. + */ + fromUrlParams(params: ParamMap): CatalogSearchContext { + let context = new CatalogSearchContext(); + + this.applyUrlParams(context, params); + + return context; + } + + applyUrlParams(context: CatalogSearchContext, params: ParamMap): void { + + // Reset query/filter args. The will be reconstructed below. + context.reset(); + + // These fields can be copied directly into place + ['format','sort','available','global'] + .forEach(field => { + let val = params.get(field); + if (val !== null) context[field] = val; + }); + + if (params.get('limit')) + context.pager.limit = +params.get('limit'); + + if (params.get('offset')) + context.pager.offset = +params.get('offset'); + + ['query','fieldClass','joinOp','matchOp'].forEach(field => { + let arr = params.getAll(field); + if (arr && arr.length) context[field] = arr; + }); + + CATALOG_CCVM_FILTERS.forEach(code => { + let val = params.get(code); + if (val) { + context.ccvmFilters[code] = val.split(/,/); + } else { + context.ccvmFilters[code] = ['']; + } + }); + + params.getAll('facets').forEach(blob => { + let facet = JSON.parse(blob); + context.addFacet(new FacetFilter(facet.c, facet.n, facet.v)); + }); + + context.searchOrg = + this.org.get(+params.get('org')) || this.org.root(); + } +} diff --git a/Open-ILS/eg2-src/src/app/share/catalog/catalog.service.ts b/Open-ILS/eg2-src/src/app/share/catalog/catalog.service.ts new file mode 100644 index 0000000000..96f8d24ab4 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/catalog/catalog.service.ts @@ -0,0 +1,296 @@ +import {Injectable} 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'; +import {CatalogSearchContext, CatalogSearchState} from './search-context'; + +export const CATALOG_CCVM_FILTERS = [ + 'item_type', + 'item_form', + 'item_lang', + 'audience', + 'audience_group', + 'vr_format', + 'bib_level', + 'lit_form', + '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 { + + ccvmMap: {[ccvm:string] : EgIdlObject[]} = {}; + cmfMap: {[cmf:string] : EgIdlObject} = {}; + + // Keep a reference to the most recently retrieved facet data, + // since facet data is consistent across a given search. + // No need to re-fetch with every page of search data. + lastFacetData: any; + lastFacetKey: string; + + constructor( + private net: EgNetService, + private org: EgOrgService, + private unapi: EgUnapiService, + private pcrud: EgPcrudService + ) {} + + search(ctx: CatalogSearchContext): Promise { + ctx.searchState = CatalogSearchState.SEARCHING; + + var fullQuery = ctx.compileSearch(); + + console.debug(`search query: ${fullQuery}`); + + let method = 'open-ils.search.biblio.multiclass.query'; + if (ctx.isStaff) 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; + resolve(); + }); + }) + } + + applyResultData(ctx: CatalogSearchContext, result: any): void { + ctx.result = result; + ctx.pager.resultCount = result.count; + + // records[] tracks the current page of bib summaries. + result.records = []; + + // If this is a new search, reset the result IDs collection. + if (this.lastFacetKey != result.facet_key) ctx.resultIds = []; + + result.ids.forEach((blob, idx) => {ctx.addResultId(blob[0], idx)}); + } + + fetchBibSummaries(ctx: CatalogSearchContext): Promise { + let promises = []; + let depth = ctx.global ? + ctx.org.root().ou_type().depth() : + ctx.searchOrg.ou_type().depth(); + + ctx.currentResultIds().forEach((recId, idx) => { + promises.push( + this.getBibSummary(recId, ctx.searchOrg.id(), depth) + .then( + // idx maintains result sort order + summary => ctx.result.records[idx] = summary + ) + ); + }); + + return Promise.all(promises); + } + + fetchFacets(ctx: CatalogSearchContext): Promise { + + if (!ctx.result) + return Promise.reject('Cannot fetch facets without results'); + + if (this.lastFacetKey == ctx.result.facet_key) { + ctx.result.facetData = this.lastFacetData; + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + this.net.request('open-ils.search', + 'open-ils.search.facet_cache.retrieve', + ctx.result.facet_key + ).subscribe(facets => { + let facetData = {}; + Object.keys(facets).forEach(cmfId => { + let facetHash = facets[cmfId]; + let cmf = this.cmfMap[cmfId]; + + let cmfData = []; + Object.keys(facetHash).forEach(value => { + let count = facetHash[value]; + cmfData.push({value : value, count : count}); + }); + + if (!facetData[cmf.field_class()]) + facetData[cmf.field_class()] = {}; + + facetData[cmf.field_class()][cmf.name()] = { + cmfLabel : cmf.label(), + valueList : cmfData.sort((a, b) => { + if (a.count > b.count) return -1; + if (a.count < b.count) return 1; + // secondary alpha sort on display value + return a.value < b.value ? -1 : 1; + }) + }; + }); + + this.lastFacetKey = ctx.result.facet_key; + this.lastFacetData = ctx.result.facetData = facetData; + resolve(); + }); + }) + } + + fetchCcvms(): Promise { + + if (Object.keys(this.ccvmMap).length) + return Promise.resolve(); + + return new Promise((resolve, reject) => { + this.pcrud.search('ccvm', + {ctype : CATALOG_CCVM_FILTERS}, {}, {atomic: true} + ).subscribe(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 { + // 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(); + } + ) + }); + } + + + /** + * Bib record via UNAPI as mods (for now) with holdings summary + * and record attributes. + */ + getBibSummary(bibId: number, orgId?: number, depth?: number): Promise { + return new Promise((resolve, reject) => { + this.unapi.getAsXmlDocument({ + target: 'bre', + id: bibId, + extras: '{bre.extern,holdings_xml,mra}', + format: 'mods32', + orgId: orgId, + depth: depth + }).then(xmlDoc => { + let summary = this.translateBibSummary(xmlDoc); + summary.id = bibId; + resolve(summary); + }); + }); + } + + /** + * 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(xmlDoc: XMLDocument): any { // TODO: bib summary interface + + let response = { + copyCounts : [], + ccvms : {} + }; + + let resolver:any = (prefix: string): string => { + return NAMESPACE_MAPS[prefix] || null; + }; + + Object.keys(MODS_XPATH_AUTO).forEach(key => { + let result = xmlDoc.evaluate(MODS_XPATH_AUTO[key], xmlDoc, + resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + + let node = result.singleNodeValue; + if (node) response[key] = node.textContent; + }); + + let result = xmlDoc.evaluate(MODS_XPATH.extern, xmlDoc, + resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + + 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.textContent, + 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; + } +} diff --git a/Open-ILS/eg2-src/src/app/share/catalog/search-context.ts b/Open-ILS/eg2-src/src/app/share/catalog/search-context.ts new file mode 100644 index 0000000000..b3c21e53a8 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/catalog/search-context.ts @@ -0,0 +1,245 @@ +import {EgOrgService} from '@eg/core/org'; +import {EgIdlObject} from '@eg/core/idl'; +import {Pager} from '@eg/share/util/pager'; +import {Params} from '@angular/router'; + +export enum CatalogSearchState { + PENDING, + SEARCHING, + COMPLETE +} + +export class FacetFilter { + facetClass: string; + facetName: string; + facetValue: string; + + constructor(cls: string, name: string, value: string) { + this.facetClass = cls; + this.facetName = name; + this.facetValue = value; + } + + equals(filter: FacetFilter): boolean { + return ( + this.facetClass == filter.facetClass && + this.facetName == filter.facetName && + this.facetValue == filter.facetValue + ); + } +} + +// Not an angular service. +// It's conceviable there could be multiple contexts. +export class CatalogSearchContext { + + // Search options and filters + available: boolean = false; + global: boolean = false; + sort: string; + fieldClass: string[]; + query: string[]; + joinOp: string[]; + matchOp: string[]; + format: string; + searchOrg: EgIdlObject; + ccvmFilters: {[ccvmCode:string] : string[]}; + facetFilters: FacetFilter[]; + isStaff: boolean; + + // Result from most recent search. + result: any = {}; + searchState: CatalogSearchState = CatalogSearchState.PENDING; + + // List of IDs in page/offset context. + resultIds: number[] = []; + + // Utility stuff + pager: Pager; + org: EgOrgService; + + constructor() { + this.pager = new Pager(); + this.reset(); + } + + // List of result IDs for the current page of data. + currentResultIds(): number[] { + let ids = []; + for ( + let idx = this.pager.offset; + idx < Math.min( + this.pager.offset + this.pager.limit, + this.pager.resultCount + ); + idx++ + ) {ids.push(this.resultIds[idx])} + return ids; + } + + addResultId(id: number, resultIdx: number ): void { + this.resultIds[resultIdx + this.pager.offset] = id; + } + + // Return the record at the requested index. + resultIdAt(index: number): number { + return this.resultIds[index] || null; + } + + // Return the index of the requested record + indexForResult(id: number): number { + for (let i = 0; i < this.resultIds.length; i++) { + if (this.resultIds[i] == id) + return i; + } + return null; + } + + /** + * Return search context to its default state, resetting search + * parameters and clearing any cached result data. + * This does not reset global filters like limit-to-available + * or search-global. + */ + reset(): void { + this.pager.offset = 0; + this.format = ''; + this.sort = ''; + this.query = ['']; + this.fieldClass = ['keyword']; + this.matchOp = ['contains']; + this.joinOp = ['']; + this.ccvmFilters = {}; + this.facetFilters = []; + this.result= {}; + this.resultIds = []; + this.searchState = CatalogSearchState.PENDING; + } + + isSearchable(): boolean { + return this.query.length && this.query[0] != ''; + } + + 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 => { + if (this.ccvmFilters[field][0] != '') + 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 joinOp = this.joinOp[idx]; + let matchOp = this.matchOp[idx]; + let fieldClass = this.fieldClass[idx]; + + let str = ''; + if (!query) return str; + + if (idx > 0) str += ' ' + joinOp + ' '; + + str += '('; + if (fieldClass) str += fieldClass + ':'; + + switch(matchOp) { + 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 + ')'; + } + + hasFacet(facet: FacetFilter): boolean { + return Boolean( + this.facetFilters.filter( + f => {return f.equals(facet)})[0] + ); + } + + removeFacet(facet: FacetFilter): void { + this.facetFilters = this.facetFilters.filter( + f => { return !f.equals(facet); }); + } + + addFacet(facet: FacetFilter): void { + if (!this.hasFacet(facet)) + this.facetFilters.push(facet); + } + + toggleFacet(facet: FacetFilter): void { + if (this.hasFacet(facet)) { + this.removeFacet(facet); + } else { + this.facetFilters.push(facet); + } + } +} + + diff --git a/Open-ILS/eg2-src/src/app/share/org-select.component.html b/Open-ILS/eg2-src/src/app/share/org-select.component.html new file mode 100644 index 0000000000..d7b9101c89 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/org-select.component.html @@ -0,0 +1,15 @@ + + + +{{r.label}} + + + diff --git a/Open-ILS/eg2-src/src/app/share/org-select.component.ts b/Open-ILS/eg2-src/src/app/share/org-select.component.ts new file mode 100644 index 0000000000..7738215b4b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/org-select.component.ts @@ -0,0 +1,102 @@ +import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {map, debounceTime} from 'rxjs/operators'; +import {EgAuthService} from '@eg/core/auth'; +import {EgStoreService} from '@eg/core/store'; +import {EgOrgService} from '@eg/core/org'; +import {EgIdlObject} from '@eg/core/idl'; +import {NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap'; + +// Use a unicode char for spacing instead of ASCII=32 so the browser +// won't collapse the nested display entries down to a single space. +const PAD_SPACE: string = ' '; // U+2007 + +interface OrgDisplay { + id: number; + label: string; + disabled: boolean; +} + +@Component({ + selector: 'eg-org-select', + templateUrl: './org-select.component.html' +}) +export class EgOrgSelectComponent implements OnInit { + + selected: OrgDisplay; + startOrg: EgIdlObject; + hidden: number[] = []; + disabled: number[] = []; + + // Read-only properties optionally provided by the calling component. + @Input() placeholder: string; + @Input() stickySetting: string; + @Input() displayField: string = 'shortname'; + + @Input() set initialOrg(org: EgIdlObject) { + if (org) this.startOrg = org; + } + + @Input() set hideOrgs(ids: number[]) { + if (ids) this.hidden = ids; + } + + @Input() set disableOrgs(ids: number[]) { + if (ids) this.disabled = ids; + } + + /** Emitted when the org unit value is changed via the selector. + * Does not fire on initialOrg. + */ + @Output() onChange = new EventEmitter(); + + constructor( + private auth: EgAuthService, + private store: EgStoreService, + private org: EgOrgService + ) {} + + ngOnInit() { + if (this.startOrg) { + this.selected = this.formatForDisplay(this.startOrg); + } + } + + formatForDisplay(org: EgIdlObject): OrgDisplay { + return { + id : org.id(), + label : PAD_SPACE.repeat(org.ou_type().depth()) + + org[this.displayField](), + disabled : false + }; + } + + orgChanged(selEvent: NgbTypeaheadSelectItemEvent) { + this.onChange.emit(this.org.get(selEvent.item.id)); + } + + // Formats the selected value + formatter = (result: OrgDisplay) => result.label.trim(); + + filter = (text$: Observable): Observable => { + return text$ + .debounceTime(100) + .distinctUntilChanged() + .map(term => { + + return this.org.list().filter(org => { + + // Find orgs matching the search term + return org[this.displayField]() + .toLowerCase().indexOf(term.toLowerCase()) > -1 + + }).filter(org => { // Exclude hidden orgs + return this.hidden.filter( + id => {return org.id() == id}).length == 0; + + }).map(org => {return this.formatForDisplay(org)}) + }); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/share/unapi.ts b/Open-ILS/eg2-src/src/app/share/unapi.ts new file mode 100644 index 0000000000..28c25896c0 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/unapi.ts @@ -0,0 +1,54 @@ +import {Injectable, EventEmitter} from '@angular/core'; +import {EgOrgService} from '@eg/core/org'; + +/* +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) {} + + createUrl(params: EgUnapiParams): string { + let depth = params.depth || 0; + let org = params.orgId ? this.org.get(params.orgId) : this.org.root(); + + return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` + + `${org.shortname()}/${depth}&format=${params.format}`; + } + + getAsXmlDocument(params: EgUnapiParams): Promise { + // 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) => { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4) { + if (this.status == 200) { + resolve(xhttp.responseXML); + } else { + reject(`UNAPI request failed for ${url}`); + } + } + } + xhttp.open("GET", url, true); + xhttp.send(); + }); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/share/util/pager.ts b/Open-ILS/eg2-src/src/app/share/util/pager.ts new file mode 100644 index 0000000000..1c21a8dcaf --- /dev/null +++ b/Open-ILS/eg2-src/src/app/share/util/pager.ts @@ -0,0 +1,47 @@ + +/** + * Utility class for manage paged information. + */ +export class Pager { + offset: number = 0; + limit: number = null; + 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 + } + + increment(): void { + this.setPage(this.currentPage() + 1); + } + + decrement(): void { + this.setPage(this.currentPage() - 1); + } + + setPage(page: number): void { + this.offset = (this.limit * (page - 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; + } +} diff --git a/Open-ILS/eg2-src/src/app/staff/admin/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/admin/routing.module.ts new file mode 100644 index 0000000000..4e4ef09152 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/routing.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +const routes: Routes = [{ + path: '', + children : [{ + path: 'workstation', + loadChildren: '@eg/staff/admin/workstation/routing.module#EgAdminWsRoutingModule' + }] +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class EgAdminRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/staff/admin/workstation/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/admin/workstation/routing.module.ts new file mode 100644 index 0000000000..114c312f3d --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/workstation/routing.module.ts @@ -0,0 +1,14 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +const routes: Routes = [{ + path: 'workstations', + loadChildren: '@eg/staff/admin/workstation/workstations/app.module#ManageWorkstationsModule' +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class EgAdminWsRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.html b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.html new file mode 100644 index 0000000000..5b95268e34 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.html @@ -0,0 +1,75 @@ +
+
+
+ Workstation {{removingWs}} is no longer valid. Removing registration. +
+
+ Please register a workstation. +
+ +
+
Register a New Workstation For This Browser
+
+
+
+ + +
+
+
+ +
+ +
+
+
+
+
+
+ Workstations Registered With This Browser +
+
+
+
+ +
+
+
+
+ + + +
+
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.ts b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.ts new file mode 100644 index 0000000000..b724dc0f2e --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.ts @@ -0,0 +1,83 @@ +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'; +import {EgAuthService} from '@eg/core/auth'; +import {EgOrgService} from '@eg/core/org'; + +// Slim version of the WS that's stored in the cache. +interface Workstation { + id: number; + name: string; + owning_lib: number; +} + +@Component({ + templateUrl: 'app.component.html' +}) +export class WorkstationsComponent implements OnInit { + + selectedId: Number; + workstations: Workstation[] = []; + removeWorkstation: string; + newOwner: EgIdlObject; + newName: String; + + // Org selector options. + hideOrgs: number[]; + disableOrgs: number[]; + orgOnChange = (org: EgIdlObject): void => { + this.newOwner = org; + } + + constructor( + private route: ActivatedRoute, + private net: EgNetService, + private store: EgStoreService, + private auth: EgAuthService, + private org: EgOrgService + ) {} + + ngOnInit() { + this.store.getItem('eg.workstation.all') + .then(res => this.workstations = res); + + // TODO: perm limits required here too + this.disableOrgs = this.org.filterList({canHaveUsers : true}, true); + + this.removeWorkstation = this.route.snapshot.paramMap.get('remove'); + if (this.removeWorkstation) { + console.log('Removing workstation ' + this.removeWorkstation); + // TODO remove + } + } + + selected(): Workstation { + return this.workstations.filter( + ws => {return ws.id == this.selectedId})[0]; + } + + useNow(): void { + console.debug('using ' + this.selected().name); + } + + setDefault(): void { + console.debug('defaulting ' + this.selected().name); + } + + removeSelected(): void { + console.debug('removing ' + this.selected().name); + } + + canDeleteSelected(): boolean { + return true; + } + + registerWorkstation(): void { + console.log(`Registering new workstation ` + + `"${this.newName}" at ${this.newOwner.shortname()}`); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.module.ts b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.module.ts new file mode 100644 index 0000000000..c7051fb759 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.module.ts @@ -0,0 +1,21 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {EgStaffModule} from '@eg/staff/app.module'; +import {WorkstationsRoutingModule} from './routing.module'; +import {WorkstationsComponent} from './app.component'; + +@NgModule({ + declarations: [ + WorkstationsComponent + ], + imports: [ + CommonModule, + EgStaffModule, + WorkstationsRoutingModule + ] +}) + +export class ManageWorkstationsModule { + constructor() {console.log('Loading ManageWorkstationsModule')} +} + diff --git a/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/routing.module.ts new file mode 100644 index 0000000000..f1ac37ed0c --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/routing.module.ts @@ -0,0 +1,25 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {WorkstationsComponent} from './app.component'; + +// Note that we need a path value (e.g. 'manage') because without it +// there is nothing for the router to match, unless we rely on the parent +// module to handle all of our routing for us. +const routes: Routes = [ + { + path: 'manage', + component: WorkstationsComponent + }, { + path: 'remove/:remove', + component: WorkstationsComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class WorkstationsRoutingModule { +} + diff --git a/Open-ILS/eg2-src/src/app/staff/app.component.css b/Open-ILS/eg2-src/src/app/staff/app.component.css new file mode 100644 index 0000000000..508d879b9b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/app.component.css @@ -0,0 +1,8 @@ +#staff-content-container { + width: 95%; + margin-top:56px; + padding-right: 10px; + padding-left: 10px; + margin-right: auto; + margin-left: auto; +} diff --git a/Open-ILS/eg2-src/src/app/staff/app.component.html b/Open-ILS/eg2-src/src/app/staff/app.component.html new file mode 100644 index 0000000000..7bd463a623 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/app.component.html @@ -0,0 +1,8 @@ + + + +
+ + +
+ diff --git a/Open-ILS/eg2-src/src/app/staff/app.component.ts b/Open-ILS/eg2-src/src/app/staff/app.component.ts new file mode 100644 index 0000000000..3c90ab0a91 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/app.component.ts @@ -0,0 +1,73 @@ +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'; + +@Component({ + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'] +}) + +export class EgStaffComponent implements OnInit { + + readonly loginPath = '/staff/login'; + readonly wsAdminPath = '/staff/admin/workstation/workstations/manage'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: EgNetService, + private auth: EgAuthService + ) {} + + ngOnInit() { + + console.debug('EgStaffComponent:ngOnInit()'); + + // Fires on all in-app router navigation, but not initial page load. + this.router.events.subscribe(routeEvent => { + if (routeEvent instanceof NavigationEnd) { + //console.debug(`EgStaffComponent routing to ${routeEvent.url}`); + this.basicAuthChecks(routeEvent); + } + }); + + // Redirect to the login page on any auth timeout events. + this.net.authExpired$.subscribe(uhOh => { + console.debug('Auth session has expired. Redirecting to login'); + this.auth.redirectUrl = this.router.url; + this.router.navigate([this.loginPath]); + }); + + this.route.data.subscribe((data: {staffResolver : any}) => { + console.debug('EgStaff ngOnInit complete'); + + }); + } + + /** + * Verifying auth token on every route is overkill, since an expired + * token will make itself known with the first API call, but we do + * want to prevent navigation from the login or workstation admin + * page, since these can be accessed without a valid authtoken or + * workstation, respectively, once the initial route resolvers + * have done their jobs. + */ + basicAuthChecks(routeEvent: NavigationEnd): void { + + // Access to login page is always granted + if (routeEvent.url == this.loginPath) return; + + if (!this.auth.token()) + this.router.navigate([this.loginPath]); + + // Access to workstation admin page is granted regardless + // of workstation validity. + if (routeEvent.url == this.wsAdminPath) return; + + if (this.auth.workstationState != EgAuthWsState.VALID) + this.router.navigate([this.wsAdminPath]); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/app.module.ts b/Open-ILS/eg2-src/src/app/staff/app.module.ts new file mode 100644 index 0000000000..7b53d7fc41 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/app.module.ts @@ -0,0 +1,37 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {EgBaseModule} from '@eg/app.module'; + +import {EgStaffComponent} from './app.component'; +import {EgStaffRoutingModule} from './routing.module'; +import {EgStaffNavComponent} from './nav.component'; +import {EgStaffLoginComponent} from './login.component'; +import {EgStaffSplashComponent} from './splash.component'; +import {EgOrgSelectComponent} from '@eg/share/org-select.component'; + +@NgModule({ + declarations: [ + EgStaffComponent, + EgStaffNavComponent, + EgStaffSplashComponent, + EgStaffLoginComponent, + EgOrgSelectComponent + ], + imports: [ + EgStaffRoutingModule, + FormsModule, + NgbModule + ], + exports: [ + // Components available to all staff/sub modules + EgOrgSelectComponent, + FormsModule, + NgbModule + ] +}) + +export class EgStaffModule { + +} diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/app.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/app.component.html new file mode 100644 index 0000000000..1596454ac1 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/app.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/app.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/app.component.ts new file mode 100644 index 0000000000..a5ca68f3db --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/app.component.ts @@ -0,0 +1,17 @@ +import {Component, OnInit} from '@angular/core'; +import {StaffCatalogService} from './app.service'; + +@Component({ + templateUrl: 'app.component.html' +}) +export class EgCatalogComponent implements OnInit { + + constructor(private staffCat: StaffCatalogService) {} + + ngOnInit() { + // Create the search context that will be used by all + // of my child components. + this.staffCat.createContext(); + } +} + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/app.module.ts b/Open-ILS/eg2-src/src/app/staff/catalog/app.module.ts new file mode 100644 index 0000000000..b76cc0bcef --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/app.module.ts @@ -0,0 +1,48 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {EgStaffModule} from '../app.module'; +import {EgUnapiService} from '@eg/share/unapi'; +import {EgCatalogRoutingModule} from './routing.module'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; +import {EgCatalogComponent} from './app.component'; +import {SearchFormComponent} from './search-form.component'; +import {ResultsComponent} from './result/results.component'; +import {RecordComponent} from './record/record.component'; +import {CopiesComponent} from './record/copies.component'; +import {EgBibSummaryComponent} from '../share/bib-summary.component'; +import {ResultPaginationComponent} from './result/pagination.component'; +import {ResultFacetsComponent} from './result/facets.component'; +import {ResultRecordComponent} from './result/record.component'; +import {StaffCatalogService} from './app.service'; +import {RecordPaginationComponent} from './record/pagination.component'; + +@NgModule({ + declarations: [ + EgCatalogComponent, + ResultsComponent, + RecordComponent, + CopiesComponent, + EgBibSummaryComponent, + SearchFormComponent, + ResultRecordComponent, + ResultFacetsComponent, + ResultPaginationComponent, + RecordPaginationComponent + ], + imports: [ + EgStaffModule, + CommonModule, + EgCatalogRoutingModule + ], + providers: [ + EgUnapiService, + EgCatalogService, + EgCatalogUrlService, + StaffCatalogService + ] +}) + +export class EgCatalogModule { + +} diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/app.service.ts b/Open-ILS/eg2-src/src/app/staff/catalog/app.service.ts new file mode 100644 index 0000000000..625206e7f4 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/app.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; +import {Router, ActivatedRoute} from '@angular/router'; +import {EgOrgService} from '@eg/core/org'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; +import {CatalogSearchContext} from '@eg/share/catalog/search-context'; + +/** + * Shared bits needed by the staff version of the catalog. + */ + +@Injectable() +export class StaffCatalogService { + + searchContext: CatalogSearchContext; + routeIndex: number = 0; + + constructor( + private router: Router, + private route: ActivatedRoute, + private org: EgOrgService, + private cat: EgCatalogService, + private catUrl: EgCatalogUrlService + ) { } + + createContext(): void { + // Initialize the search context from the load-time URL params. + // Do this here so the search form and other context data are + // applied on every page, not just the search results page. The + // search results pages will handle running the actual search. + this.searchContext = + this.catUrl.fromUrlParams(this.route.snapshot.queryParamMap); + + this.searchContext.org = this.org; + this.searchContext.isStaff = true; + + // TODO: UI / settings + if (!this.searchContext.pager.limit) + this.searchContext.pager.limit = 20; + } + + /** + * Redirect to the search results page while propagating the current + * search paramters into the URL. Let the search results component + * execute the actual search. + */ + search(): void { + let params = this.catUrl.toUrlParams(this.searchContext); + + // Avoid redirect on empty-query searches + if (params.query[0] == '') return; + + // Force a new search every time this method is called, even if + // it's the same as the active search. Since router navigation + // exits early when the route + params is identical, add a + // random token to the route params to force a full navigation. + // This also resolves a problem where only removing secondary+ + // versions of a query param fail to cause a route navigation. + // (E.g. going from two query= params to one). Investigation + // pending. + params.ridx=''+this.routeIndex++; + + this.router.navigate( + ['/staff/catalog/search'], {queryParams: params}); + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.html new file mode 100644 index 0000000000..84e9d8e8a5 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.html @@ -0,0 +1,71 @@ +
+ +
+
+
Location
+
Call Number / Copy Notes
+
Barcode
+
Shelving Location
+
Circulation Modifier
+
Age Hold Protection
+
Active/Create Date
+
Holdable?
+
Status
+
Due Date
+
+
+
    +
  • +
    +
    {{orgName(copy.circ_lib)}}
    +
    + {{copy.call_number_prefix_label}} + {{copy.call_number_label}} + {{copy.call_number_suffix_label}} +
    +
    + {{copy.barcode}} + View + | + Edit +
    +
    {{copy.copy_location}}
    +
    {{copy.circ_modifier || ''}}
    +
    {{copy.age_protect}}
    +
    + {{copy.active_date || copy.create_date | date:'shortDate'}} +
    +
    + Yes + No +
    +
    {{copy.copy_status}}
    +
    {{copy.due_date | date:'shortDate'}}
    +
    +
  • +
+
+
+
diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.ts new file mode 100644 index 0000000000..f234eba8df --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.ts @@ -0,0 +1,84 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {EgNetService} from '@eg/core/net'; +import {StaffCatalogService} from '../app.service'; +import {Pager} from '@eg/share/util/pager'; +import {EgOrgService} from '@eg/core/org'; + +@Component({ + selector: 'eg-catalog-copies', + templateUrl: 'copies.component.html' +}) +export class CopiesComponent implements OnInit { + + pager: Pager; + copies: any[] + recId: number; + initDone: boolean = false; + + @Input() set recordId(id: number) { + this.recId = id; + // Only force new data collection when recordId() + // is invoked after ngInit() has already run. + if (this.initDone) this.collectData(); + } + + constructor( + private net: EgNetService, + private org: EgOrgService, + private staffCat: StaffCatalogService, + ) {} + + ngOnInit() { + this.initDone = true; + this.collectData(); + } + + collectData() { + if (!this.recId) return; + this.pager = new Pager(); + this.pager.limit = 10; // TODO UI + this.fetchCopies(); + } + + orgName(orgId: number): string { + return this.org.get(orgId).shortname(); + } + + fetchCopies(): void { + this.copies = []; + this.net.request( + 'open-ils.search', + 'open-ils.search.bib.copies.staff', + this.recId, + this.staffCat.searchContext.searchOrg.id(), + this.staffCat.searchContext.searchOrg.ou_type().depth(), // TODO + this.pager.limit, + this.pager.offset, + this.staffCat.searchContext.searchOrg.id() // TODO pref_ou + ).subscribe(copy => { + this.copies.push(copy); + }); + } + + holdable(copy: any): boolean { + return copy.holdable == 't' + && copy.location_holdable == 't' + && copy.status_holdable == 't'; + } + + firstPage(): void { + this.pager.offset = 0; + this.fetchCopies(); + } + prevPage(): void { + this.pager.decrement(); + this.fetchCopies(); + } + nextPage(): void { + this.pager.increment(); + this.fetchCopies(); + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.html new file mode 100644 index 0000000000..0edcded4cc --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.html @@ -0,0 +1,36 @@ + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.ts new file mode 100644 index 0000000000..a7535f6514 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.ts @@ -0,0 +1,157 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {Router} from '@angular/router'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {CatalogSearchContext} from '@eg/share/catalog/search-context'; +import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; +import {StaffCatalogService} from '../app.service'; +import {Pager} from '@eg/share/util/pager'; + + +@Component({ + selector: 'eg-catalog-record-pagination', + templateUrl: 'pagination.component.html' +}) +export class RecordPaginationComponent implements OnInit { + + id: number; + index: number; + initDone: boolean = false; + searchContext: CatalogSearchContext; + + @Input() set recordId(id: number) { + this.id = id; + // Only apply new record data after the initial load + if (this.initDone) this.setIndex(); + } + + constructor( + private router: Router, + private cat: EgCatalogService, + private catUrl: EgCatalogUrlService, + private staffCat: StaffCatalogService, + ) {} + + ngOnInit() { + this.initDone = true; + this.setIndex(); + } + + firstRecord(): void { + this.findRecordAtIndex(0).then(id => { + let params = this.catUrl.toUrlParams(this.searchContext); + this.router.navigate( + ['/staff/catalog/record/' + id], {queryParams: params}); + }); + } + + lastRecord(): void { + this.findRecordAtIndex( + this.searchContext.result.count - 1 + ).then(id => { + let params = this.catUrl.toUrlParams(this.searchContext); + this.router.navigate( + ['/staff/catalog/record/' + id], {queryParams: params}); + }); + } + + nextRecord(): void { + this.findRecordAtIndex(this.index + 1).then(id => { + let params = this.catUrl.toUrlParams(this.searchContext); + this.router.navigate( + ['/staff/catalog/record/' + id], {queryParams: params}); + }); + } + + prevRecord(): void { + this.findRecordAtIndex(this.index - 1).then(id => { + let params = this.catUrl.toUrlParams(this.searchContext); + this.router.navigate( + ['/staff/catalog/record/' + id], {queryParams: params}); + }); + } + + + // Returns the offset of the record within the search results as a whole. + searchIndex(idx: number): number { + return idx + this.searchContext.pager.offset; + } + + // Find the position of the current record in the search results + // If no results are present or the record is not found, expand + // the search scope to find the record. + setIndex(): Promise { + this.searchContext = this.staffCat.searchContext; + this.index = null; + + return new Promise((resolve, reject) => { + + this.index = this.searchContext.indexForResult(this.id); + if (this.index !== null) return resolve(); + + return this.refreshSearch().then(ok => { + this.index = this.searchContext.indexForResult(this.id); + if (this.index === null) console.warn( + 'No search results found containing the focused record.'); + resolve(); + }); + }); + } + + // Find the record ID at the specified search index. + // If no data exists for the requested index, expand the search + // to include data for that index. + findRecordAtIndex(index: number): Promise { + + // First see if the selected record sits in the current page + // of search results. + return new Promise((resolve, reject) => { + let id = this.searchContext.resultIdAt(index); + if (id) return resolve(id); + + console.debug( + 'Record paginator unable to find record at index ' + index); + + // If we have to re-run the search to find the record, + // expand the search limit out just enough to find the + // requested record plus one more. + return this.refreshSearch(index + 2).then( + ok => { + let id = this.searchContext.resultIdAt(index); + if (id) { + resolve(id); + } else { + reject('no record found'); + } + } + ); + }); + } + + refreshSearch(limit?: number): Promise { + + console.debug('paginator refreshing search'); + + if (!this.searchContext.isSearchable()) + return Promise.resolve(); + + let origPager = this.searchContext.pager; + let tmpPager = new Pager(); + tmpPager.limit = limit || 1000; + + this.searchContext.pager = tmpPager; + + return this.cat.search(this.searchContext) + .then( + ok => { this.searchContext.pager = origPager; }, + notOk => { this.searchContext.pager = origPager } + ); + } + + returnToSearch(): void { + // Fire the main search. This will direct us back to /results/ + this.staffCat.search(); + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.html new file mode 100644 index 0000000000..127254aa5d --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.html @@ -0,0 +1,18 @@ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.ts new file mode 100644 index 0000000000..78552eb443 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.ts @@ -0,0 +1,61 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {ActivatedRoute, ParamMap} from '@angular/router'; +import {EgPcrudService} from '@eg/core/pcrud'; +import {EgIdlObject} from '@eg/core/idl'; +import {CatalogSearchContext, CatalogSearchState} + from '@eg/share/catalog/search-context'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {StaffCatalogService} from '../app.service'; +import {EgBibSummaryComponent} from '../../share/bib-summary.component'; + +@Component({ + selector: 'eg-catalog-record', + templateUrl: 'record.component.html' +}) +export class RecordComponent implements OnInit { + + recordId: number; + bibSummary: any; + searchContext: CatalogSearchContext; + + constructor( + private route: ActivatedRoute, + private pcrud: EgPcrudService, + private cat: EgCatalogService, + private staffCat: StaffCatalogService + ) {} + + ngOnInit() { + this.searchContext = this.staffCat.searchContext; + + // Watch for URL record ID changes + this.route.paramMap.subscribe((params: ParamMap) => { + this.recordId = +params.get('id'); + this.loadRecord(); + }) + } + + loadRecord(): void { + this.searchContext = this.staffCat.searchContext; + + // If a search is encoded in the URL, be sure we have the + // relevant search + + this.cat.getBibSummary( + this.recordId, + this.searchContext.searchOrg.id(), + this.searchContext.searchOrg.ou_type().depth() + ).then(summary => { + this.bibSummary = summary; + this.pcrud.search('au', {id: [summary.creator, summary.editor]}) + .subscribe(user => { + if (user.id() == summary.creator) + summary.creator = user; + if (user.id() == summary.editor) + summary.editor = user; + }) + }); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/resolver.service.ts b/Open-ILS/eg2-src/src/app/staff/catalog/resolver.service.ts new file mode 100644 index 0000000000..8929d551ae --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/resolver.service.ts @@ -0,0 +1,36 @@ +import {Injectable} from '@angular/core'; +import {Location} from '@angular/common'; +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'; +import {EgAuthService} from '@eg/core/auth'; +import {EgPcrudService} from '@eg/core/pcrud'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; + +@Injectable() +export class EgCatalogResolver implements Resolve> { + + constructor( + private router: Router, + private ngLocation: Location, + private store: EgStoreService, + private net: EgNetService, + private auth: EgAuthService, + private cat: EgCatalogService + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + + console.debug('EgCatalogResolver:resolve()'); + + return Promise.all([ + this.cat.fetchCcvms(), + this.cat.fetchCmfs() + ]); + } +} + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.html new file mode 100644 index 0000000000..188ae3091e --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.html @@ -0,0 +1,43 @@ + +
+
+
+
+
+
+

+ {{searchContext.result.facetData[facetConf.facetClass][name].cmfLabel}} +

+ +
+
+
+
+
+
diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.ts new file mode 100644 index 0000000000..8101ceda01 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.ts @@ -0,0 +1,48 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {CatalogSearchContext, FacetFilter} from '@eg/share/catalog/search-context'; +import {StaffCatalogService} from '../app.service'; + +export const FACET_CONFIG = { + display: [ + {facetClass : 'author', facetOrder : ['personal', 'corporate']}, + {facetClass : 'subject', facetOrder : ['topic']}, + {facetClass : 'identifier', facetOrder : ['genre']}, + {facetClass : 'series', facetOrder : ['seriestitle']}, + {facetClass : 'subject', facetOrder : ['name', 'geographic']} + ], + displayCount : 5 +}; + +@Component({ + selector: 'eg-catalog-result-facets', + templateUrl: 'facets.component.html' +}) +export class ResultFacetsComponent implements OnInit { + + searchContext: CatalogSearchContext; + facetConfig: any; + + constructor( + private cat: EgCatalogService, + private staffCat: StaffCatalogService + ) { + this.facetConfig = FACET_CONFIG; + } + + ngOnInit() { + this.searchContext = this.staffCat.searchContext; + } + + facetIsApplied(cls: string, name: string, value: string): boolean { + return this.searchContext.hasFacet(new FacetFilter(cls, name, value)); + } + + applyFacet(cls: string, name: string, value: string): void { + this.searchContext.toggleFacet(new FacetFilter(cls, name, value)); + this.searchContext.pager.offset = 0; + this.staffCat.search(); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.css b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.css new file mode 100644 index 0000000000..c283ff45d5 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.css @@ -0,0 +1,8 @@ + +/* Bootstrap default is 20px */ +.pagination {margin: 0px 0px 0px 0px} + +.pagination li:not(.active) a { + cursor: pointer; +} + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.html new file mode 100644 index 0000000000..55b63dd0d9 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.html @@ -0,0 +1,28 @@ + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.ts new file mode 100644 index 0000000000..8dbb4d84c9 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.ts @@ -0,0 +1,41 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {CatalogSearchContext} from '@eg/share/catalog/search-context'; +import {StaffCatalogService} from '../app.service'; + +@Component({ + selector: 'eg-catalog-result-pagination', + styleUrls: ['pagination.component.css'], + templateUrl: 'pagination.component.html' +}) +export class ResultPaginationComponent implements OnInit { + + searchContext: CatalogSearchContext; + + constructor( + private cat: EgCatalogService, + private staffCat: StaffCatalogService + ) {} + + ngOnInit() { + this.searchContext = this.staffCat.searchContext; + } + + nextPage(): void { + this.searchContext.pager.increment(); + this.staffCat.search(); + } + + prevPage(): void { + this.searchContext.pager.decrement(); + this.staffCat.search(); + } + + setPage(page: number): void { + if (this.searchContext.pager.currentPage() == page) return; + this.searchContext.pager.setPage(page); + this.staffCat.search(); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.html new file mode 100644 index 0000000000..c9a0cd9b45 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.html @@ -0,0 +1,129 @@ + + +
+
+
+
+ + + + +
+
+
+
+ + + #{{index + 1 + searchContext.pager.offset}} + + + {{bibSummary.title || ' '}} + +
+
+ +
+
+ + + {{bibSummary.ccvms.icon_format.label}} + + {{bibSummary.edition}} + {{bibSummary.pubdate}} +
+
+
+
+
+
+
+ + {{copyCount.available}} / {{copyCount.count}} items + +
+
+ @ {{orgName(copyCount.org_unit)}} +
+
+
+
+
+
+
+ TCN: {{bibSummary.tcn_value}} +
+
+
+
+ Holds: {{bibSummary.holdCount}} +
+
+
+
+
+
+
+ Created {{bibSummary.create_date | date:'shortDate'}} by + + + {{bibSummary.creator.usrname()}} + + + ... +
+
+
+
+
+
+ Edited {{bibSummary.edit_date | date:'shortDate'}} by + + {{bibSummary.editor.usrname()}} + + ... +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.ts new file mode 100644 index 0000000000..beee4cfbb0 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.ts @@ -0,0 +1,72 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {Router} from '@angular/router'; +import {EgOrgService} from '@eg/core/org'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; +import {CatalogSearchContext} from '@eg/share/catalog/search-context'; +import {EgNetService} from '@eg/core/net'; +import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; +import {StaffCatalogService} from '../app.service'; + +@Component({ + selector: 'eg-catalog-result-record', + templateUrl: 'record.component.html' +}) +export class ResultRecordComponent implements OnInit { + + @Input() index: number; // 0-index display row + @Input() bibSummary: any; + searchContext: CatalogSearchContext; + + constructor( + private router: Router, + private org: EgOrgService, + private net: EgNetService, + private cat: EgCatalogService, + private catUrl: EgCatalogUrlService, + private staffCat: StaffCatalogService + ) {} + + ngOnInit() { + this.searchContext = this.staffCat.searchContext; + this.fleshHoldCount(); + } + + fleshHoldCount(): void { + this.net.request( + 'open-ils.circ', + 'open-ils.circ.bre.holds.count', this.bibSummary.id + ).subscribe(count => this.bibSummary.holdCount = count); + } + + orgName(orgId: number): string { + return this.org.get(orgId).shortname(); + } + + placeHold(): void { + alert('Placing hold on bib ' + this.bibSummary.id); + } + + addToList(): void { + alert('Adding to list for bib ' + this.bibSummary.id); + } + + searchAuthor(bibSummary: any) { + this.searchContext.reset(); + this.searchContext.fieldClass = ['author']; + this.searchContext.query = [bibSummary.author]; + this.staffCat.search(); + } + + /** + * Propagate the search params along when navigating to each record. + */ + navigatToRecord(id: number) { + let params = this.catUrl.toUrlParams(this.searchContext); + + this.router.navigate( + ['/staff/catalog/record/' + id], {queryParams: params}); + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.html new file mode 100644 index 0000000000..be7c36aec8 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.html @@ -0,0 +1,30 @@ + +
+
+
+

Search Results ({{searchContext.result.count}})

+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.ts new file mode 100644 index 0000000000..b87a2cdd7d --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.ts @@ -0,0 +1,107 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {Observable} from 'rxjs/Rx'; +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} + from '@eg/share/catalog/search-context'; +import {EgPcrudService} from '@eg/core/pcrud'; +import {StaffCatalogService} from '../app.service'; +import {EgIdlObject} from '@eg/core/idl'; + +@Component({ + selector: 'eg-catalog-results', + templateUrl: 'results.component.html' +}) +export class ResultsComponent implements OnInit { + + searchContext: CatalogSearchContext; + + // Cache record creator/editor since this will likely be a + // reasonably small set of data w/ lots of repitition. + userCache: {[id:number] : EgIdlObject} = {}; + + constructor( + private route: ActivatedRoute, + private pcrud: EgPcrudService, + private cat: EgCatalogService, + private catUrl: EgCatalogUrlService, + private staffCat: StaffCatalogService + ) {} + + ngOnInit() { + this.searchContext = this.staffCat.searchContext; + + // Our search context is initialized on page load. Once + // ResultsComponent is active, it will not be reinitialized, + // even if the route parameters changes (unless we change the + // route reuse policy). Watch for changes here to pick up new + // searches. This will also fire on page load. + this.route.queryParamMap.subscribe((params: ParamMap) => { + + // TODO: Angular docs suggest using switchMap(), but + // it's not firing for some reason. Also, could avoid + // firing unnecessary searches when a param unrelated to + // searching is changed by .map()'ing out only the desired + // params and running through .distinctUntilChanged(), but + // .map() is not firing either. I'm missing something. + this.searchByUrl(params); + }) + } + + searchByUrl(params: ParamMap): void { + this.catUrl.applyUrlParams(this.searchContext, params); + + // A query string is required at minimum. + if (!this.searchContext.isSearchable()) return; + + this.cat.search(this.searchContext) + .then(ok => { + this.cat.fetchFacets(this.searchContext); + this.cat.fetchBibSummaries(this.searchContext) + .then(ok2 => this.fleshSearchResults()); + }); + } + + fleshSearchResults(): void { + let records = this.searchContext.result.records; + if (records.length == 0) return; + + // Flesh the creator / editor fields with the user object. + // Handle the user fleshing here (instead of record.component so + // we only need to grab one copy of each user. + let userIds: {[id:number]: boolean} = {}; + records.forEach(recSum => { + if (this.userCache[recSum.creator]) { + recSum.creator = this.userCache[recSum.creator]; + } else { + userIds[Number(recSum.creator)] = true; + } + + if (this.userCache[recSum.editor]) { + recSum.editor = this.userCache[recSum.editor]; + } else { + userIds[Number(recSum.editor)] = true; + } + }); + + if (!Object.keys(userIds).length) return; + + this.pcrud.search('au', {id : Object.keys(userIds)}) + .subscribe(usr => { + this.userCache[usr.id()] = usr; + records.forEach(recSum => { + if (recSum.creator == usr.id()) recSum.creator = usr; + if (recSum.editor == usr.id()) recSum.editor = usr; + }); + }); + } + + searchIsDone(): boolean { + return this.searchContext.searchState == CatalogSearchState.COMPLETE; + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/catalog/routing.module.ts new file mode 100644 index 0000000000..467db52e2c --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/routing.module.ts @@ -0,0 +1,27 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {EgCatalogComponent} from './app.component'; +import {ResultsComponent} from './result/results.component'; +import {RecordComponent} from './record/record.component'; +import {EgCatalogResolver} from './resolver.service'; + +const routes: Routes = [{ + path: '', + component: EgCatalogComponent, + resolve: {catResolver : EgCatalogResolver}, + children : [{ + path: 'search', + component: ResultsComponent, + }, { + path: 'record/:id', + component: RecordComponent, + }] +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [EgCatalogResolver ] +}) + +export class EgCatalogRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.css b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.css new file mode 100644 index 0000000000..f67d8fa0c7 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.css @@ -0,0 +1,9 @@ + +/* filter checkbox labels move to bottom */ +.checkbox label { + margin-bottom: .1rem; +} + +#staffcat-search-form { + border-bottom: 2px dashed rgba(0,0,0,.225); +} diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.html b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.html new file mode 100644 index 0000000000..3ee4d2110b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.html @@ -0,0 +1,219 @@ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+
+ + + + +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ Searching.. +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ Copy location filter goes here... +
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.ts b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.ts new file mode 100644 index 0000000000..94ef0bf03f --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.ts @@ -0,0 +1,97 @@ +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, CatalogSearchState} + from '@eg/share/catalog/search-context'; +import {StaffCatalogService} from './app.service'; + +@Component({ + selector: 'eg-catalog-search-form', + styleUrls: ['search-form.component.css'], + templateUrl: 'search-form.component.html' +}) +export class SearchFormComponent implements OnInit { + + searchContext: CatalogSearchContext; + ccvmMap: {[ccvm:string] : EgIdlObject[]} = {}; + cmfMap: {[cmf:string] : EgIdlObject} = {}; + showAdvancedSearch: boolean = false; + + constructor( + private org: EgOrgService, + private cat: EgCatalogService, + private staffCat: StaffCatalogService + ) {} + + ngOnInit() { + this.ccvmMap = this.cat.ccvmMap; + this.cmfMap = this.cat.cmfMap; + this.searchContext = this.staffCat.searchContext; + + // Start with advanced search options open + // if any filters are active. + this.showAdvancedSearch = this.hasAdvancedOptions(); + } + + /** + * Display the advanced/extended search options when asked to + * or if any advanced options are selected. + */ + showAdvanced(): boolean { + return this.showAdvancedSearch; + } + + hasAdvancedOptions(): boolean { + // ccvm filters may be present without any filters applied. + // e.g. if filters were applied then removed. + let show = false; + Object.keys(this.searchContext.ccvmFilters).forEach(ccvm => { + if (this.searchContext.ccvmFilters[ccvm][0] != '') + show = true; + }); + + return show; + } + + orgOnChange = (org: EgIdlObject): void => { + this.searchContext.searchOrg = org; + } + + addSearchRow(index: number): void { + this.searchContext.query.splice(index, 0, ''); + this.searchContext.fieldClass.splice(index, 0, 'keyword'); + this.searchContext.joinOp.splice(index, 0, '&&'); + this.searchContext.matchOp.splice(index, 0, 'contains'); + } + + delSearchRow(index: number): void { + this.searchContext.query.splice(index, 1); + this.searchContext.fieldClass.splice(index, 1); + this.searchContext.joinOp.splice(index, 1); + this.searchContext.matchOp.splice(index, 1); + } + + checkEnter($event: any): void { + if ($event.keyCode == 13) { + this.searchContext.pager.offset = 0; + this.searchByForm(); + } + } + + // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes + trackByIdx(index: any, item: any) { + return index; + } + + searchByForm(): void { + this.staffCat.search(); + } + + searchIsActive(): boolean { + return this.searchContext.searchState == CatalogSearchState.SEARCHING; + } + +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.html b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.html new file mode 100644 index 0000000000..1f55cb1d8b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.html @@ -0,0 +1,8 @@ +

Search for Patron by Barcode

+ +Barcode: + +
+
    +
  • {{str}}
  • +
diff --git a/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.ts b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.ts new file mode 100644 index 0000000000..43d36daf97 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EgNetService } from '@eg/core/net'; +import { EgAuthService } from '@eg/core/auth'; + +@Component({ + templateUrl: 'app.component.html' +}) + +export class EgBcSearchComponent implements OnInit { + + barcode: String = ''; + strList: String[] = []; + + constructor( + private route: ActivatedRoute, + private net: EgNetService, + private auth: EgAuthService + ) {} + + ngOnInit() { + + this.barcode = this.route.snapshot.paramMap.get('barcode'); + + if (this.barcode) { + // Find the user and redirect to the + } + + this.route.data.subscribe((data: { startup : any }) => { + console.debug('EgBcSearch ngOnInit complete'); + }); + + this.net.request( + 'open-ils.actor', + 'opensrf.system.echo', + 'hello', 'goodbye', 'in the middle' + ).subscribe(res => this.strList.push(res)); + } + + findUser(): void { + // find user by this.barcode; + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.module.ts b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.module.ts new file mode 100644 index 0000000000..f119697292 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { EgBcSearchComponent } from './app.component'; +import { EgBcSearchRoutingModule } from './routing.module'; + +@NgModule({ + declarations: [ + EgBcSearchComponent + ], + imports: [ + EgBcSearchRoutingModule, + CommonModule, + FormsModule + ], +}) + +export class EgBcSearchModule {} + diff --git a/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/routing.module.ts new file mode 100644 index 0000000000..2a685f321e --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/routing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { EgBcSearchComponent } from './app.component'; + +const routes: Routes = [ + { path: '', + component: EgBcSearchComponent + }, + { path: ':barcode', + component: EgBcSearchComponent + }, +]; + +@NgModule({ + imports: [ RouterModule.forChild(routes) ], + exports: [ RouterModule ] +}) + +export class EgBcSearchRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/staff/circ/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/circ/routing.module.ts new file mode 100644 index 0000000000..1b0a0f0cbe --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/circ/routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [{ + path: '', + children : [{ + path: 'patron', + children: [{ + path: 'bcsearch', + loadChildren: '@eg/staff/circ/patron/bcsearch/app.module#EgBcSearchModule' + }] + }] +}]; + +@NgModule({ + imports: [ RouterModule.forChild(routes) ], + exports: [ RouterModule ] +}) + +export class EgCircRoutingModule {} diff --git a/Open-ILS/eg2-src/src/app/staff/login.component.html b/Open-ILS/eg2-src/src/app/staff/login.component.html new file mode 100644 index 0000000000..869fe879cf --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/login.component.html @@ -0,0 +1,36 @@ +
+
+ Sign In +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+
diff --git a/Open-ILS/eg2-src/src/app/staff/login.component.ts b/Open-ILS/eg2-src/src/app/staff/login.component.ts new file mode 100644 index 0000000000..64ae6c5361 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/login.component.ts @@ -0,0 +1,79 @@ +import { Component, OnInit, Renderer } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; +import { EgAuthService, EgAuthWsState } from '@eg/core/auth'; +import { EgStoreService } from '@eg/core/store'; // TODO: testing + +@Component({ + templateUrl : './login.component.html' +}) + +export class EgStaffLoginComponent implements OnInit { + + args = { + username : '', + password : '', + type : 'staff', + //workstation : '' + workstation : 'BR1-skiddoo' // testing + }; + + workstations = []; + + constructor( + private router: Router, + private ngLocation: Location, + private renderer: Renderer, + private auth: EgAuthService, + private store: EgStoreService + ) {} + + ngOnInit() { + + // clear out any stale auth data + this.auth.logout(); + + // Focus username + this.renderer.selectRootElement('#username').focus(); + + // load browser-local workstation data + + // TODO: insert for testing. + this.store.setItem( + 'eg.workstation.all', + [{name:'BR1-skiddoo',id:1,owning_lib:4}] + ); + } + + handleSubmit() { + + // post-login URL + let url: string = this.auth.redirectUrl || '/staff/splash'; + let workstation: string = this.args.workstation; + + this.auth.login(this.args).then( + ok => { + this.auth.redirectUrl = null; + + if (this.auth.workstationState == EgAuthWsState.NOT_FOUND_SERVER) { + // User attempted to login with a workstation that is + // unknown to the server. Redirect to the WS admin page. + this.router.navigate( + ['/staff/admin/workstation/workstations/remove/${workstation}']); + } else { + // Force reload of the app after a successful login. + // This allows the route resolver to re-run with a + // valid auth token and workstation. + window.location.href = + this.ngLocation.prepareExternalUrl(url); + } + }, + notOk => { + // indicate failure in the UI. + } + ); + } +} + + + diff --git a/Open-ILS/eg2-src/src/app/staff/nav.component.css b/Open-ILS/eg2-src/src/app/staff/nav.component.css new file mode 100644 index 0000000000..ee4f93e89b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/nav.component.css @@ -0,0 +1,72 @@ +/* remove dropdown carret for icon-based entries */ +#staff-navbar .no-caret::after { + display:none; +} + +/* move the caret closer to the dropdown text */ +#staff-navbar .dropdown-toggle::after { + margin-left:0px; +} + +#staff-navbar { + background: -webkit-linear-gradient(#00593d, #007a54); + background-color: #007a54; + color: #fff; + font-size: 14px; +} + +#staff-navbar .navbar-nav { + padding: 3px; +} + +/* align top of dropdown w/ bottom of nav */ +#staff-navbar .dropdown-menu { + margin-top: 7px; +} +#staff-navbar .material-icons { + padding-right:3px; +} +#staff-navbar .dropdown-item { + font-size: 14px; + font-weight: 400; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + padding-left: 0.7rem; + padding-right: 0.7rem; + margin: -4px; +} + +#staff-navbar .dropdown-item .material-icons { + font-size: 18px; +} + +#staff-navbar .nav-link { + color: #fff; + padding-top:1px; + padding-bottom:1px; +} +#staff-navbar .nav-link:hover { + color: #ddd; + cursor: pointer; +} + +#staff-navbar .navbar-nav > .open > a, +#staff-navbar .navbar-nav > .open > a:focus, +#staff-navbar .navbar-nav > .open > a:hover { + background-color: #7a7a7a; +} +#staff-navbar .navbar-nav>.dropdown>a .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} +#staff-navbar .navbar-nav>.dropdown>a:hover .caret { + border-top-color: #ddd; + border-bottom-color: #ddd; +} + +/* Align material-icons with sibling text; otherwise they float up */ +#staff-navbar .with-material-icon, #staff-navbar .dropdown-item { + display: inline-flex; + vertical-align: middle; + align-items: center; +} + diff --git a/Open-ILS/eg2-src/src/app/staff/nav.component.html b/Open-ILS/eg2-src/src/app/staff/nav.component.html new file mode 100644 index 0000000000..859ec7f865 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/nav.component.html @@ -0,0 +1,203 @@ + + diff --git a/Open-ILS/eg2-src/src/app/staff/nav.component.ts b/Open-ILS/eg2-src/src/app/staff/nav.component.ts new file mode 100644 index 0000000000..62fb605a63 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/nav.component.ts @@ -0,0 +1,24 @@ +import {Component, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {EgAuthService} from '@eg/core/auth'; + +@Component({ + selector: 'eg-staff-nav-bar', + styleUrls: ['nav.component.css'], + templateUrl: 'nav.component.html' +}) + +export class EgStaffNavComponent implements OnInit { + + user: string; + workstation: string; + + constructor(private auth: EgAuthService) {} + + ngOnInit() { + this.user = this.auth.user().usrname(); + this.workstation = this.auth.workstation(); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/resolver.service.ts b/Open-ILS/eg2-src/src/app/staff/resolver.service.ts new file mode 100644 index 0000000000..8c23030377 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/resolver.service.ts @@ -0,0 +1,78 @@ +import {Injectable} from '@angular/core'; +import {Location} from '@angular/common'; +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'; +import {EgAuthService} from '@eg/core/auth'; + +/** + * Apply configuration, etc. required by all staff components. + * This resolver is called before authentication is confirmed. + * See EgStaffCommonDataResolver for staff-wide, post-auth activities. + */ +@Injectable() +export class EgStaffResolver implements Resolve> { + + readonly loginPath = '/staff/login'; + readonly wsAdminPath = '/staff/admin/workstation/workstations/manage'; + + constructor( + private router: Router, + private ngLocation: Location, + private store: EgStoreService, + private net: EgNetService, + private auth: EgAuthService + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable { + + console.debug('EgStaffResolver:resolve()'); + + // Staff cookies stay in /$base/staff/ + // NOTE: storing session data at '/' so it can be shared by + // Angularjs apps. + this.store.loginSessionBasePath = '/'; + //this.ngLocation.prepareExternalUrl('/staff'); + + // Login resets everything. No need to load data. + if (state.url == '/staff/login') return Observable.of(true); + + return Observable.create(observer => { + this.auth.testAuthToken().then( + tokenOk => { + console.debug('EgStaffResolver: authtoken verified'); + this.auth.verifyWorkstation().then( + wsOk => { + this.loadStartupData(observer).then( + ok => observer.complete() + ); + }, + wsNotOk => { + if (state.url != this.wsAdminPath) { + this.router.navigate([this.wsAdminPath]); + } + observer.complete(); + } + ); + }, + tokenNotOk => { + // Authtoken is not OK. + console.debug('EgStaffResolver: authtoken is not valid'); + this.auth.redirectUrl = state.url; + this.router.navigate([this.loginPath]); + observer.error('invalid auth'); + } + ); + }); + } + + loadStartupData(observer: Observer): Promise { + console.debug('EgStaffResolver:loadStartupData()'); + return Promise.resolve(); + } +} + diff --git a/Open-ILS/eg2-src/src/app/staff/routing.module.ts b/Open-ILS/eg2-src/src/app/staff/routing.module.ts new file mode 100644 index 0000000000..81c0609565 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/routing.module.ts @@ -0,0 +1,46 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {EgStaffResolver} from './resolver.service'; +import {EgStaffComponent} from './app.component'; +import {EgStaffLoginComponent} from './login.component'; +import {EgStaffSplashComponent} from './splash.component'; + +// Not using 'canActivate' because it's called before all resolvers, +// but the resolvers parse the IDL, etc. + +const routes: Routes = [{ + path: '', + component: EgStaffComponent, + resolve: {staffResolver : EgStaffResolver}, + children: [{ + path: '', + redirectTo: 'splash', + pathMatch: 'full', + }, { + path: 'login', + component: EgStaffLoginComponent + }, { + path: 'splash', + component: EgStaffSplashComponent + }, { + path: 'circ', + loadChildren : '@eg/staff/circ/routing.module#EgCircRoutingModule' + }, { + path: 'catalog', + loadChildren : '@eg/staff/catalog/app.module#EgCatalogModule' + }, { + path: 'admin', + loadChildren : '@eg/staff/admin/routing.module#EgAdminRoutingModule' + }] +}]; + +@NgModule({ + imports: [ RouterModule.forChild(routes) ], + exports: [ RouterModule ], + providers: [ + EgStaffResolver + ] +}) + +export class EgStaffRoutingModule {} + diff --git a/Open-ILS/eg2-src/src/app/staff/share/README b/Open-ILS/eg2-src/src/app/staff/share/README new file mode 100644 index 0000000000..1d6d167d9c --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/share/README @@ -0,0 +1 @@ +Classes, services, and components shared in the staff app. diff --git a/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.html b/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.html new file mode 100644 index 0000000000..66266086d8 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.html @@ -0,0 +1,66 @@ + +
+
+
+ Record Summary +
+
+ +
+
+
    +
  • +
    +
    Title:
    +
    {{summary.title}}
    +
    Edition:
    +
    {{summary.edition}}
    +
    TCN:
    +
    {{summary.tcn_value}}
    +
    Created By:
    +
    + {{summary.creator.usrname()}} +
    +
    +
  • +
  • +
    +
    Author:
    +
    {{summary.author}}
    +
    Pubdate:
    +
    {{summary.pubdate}}
    +
    Database ID:
    +
    {{summary.id}}
    +
    Last Edited By:
    +
    + {{summary.editor.usrname()}} +
    +
    +
  • +
  • +
    +
    Bib Call #:
    +
    {{summary.callNumber}}
    +
    Record Owner:
    +
    TODO
    +
    Created On:
    +
    {{summary.create_date | date:'shortDate'}}
    +
    Last Edited On:
    +
    {{summary.edit_date | date:'shortDate'}}
    +
    +
  • +
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.ts b/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.ts new file mode 100644 index 0000000000..877b18a4f5 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.ts @@ -0,0 +1,76 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {EgNetService} from '@eg/core/net'; +import {EgPcrudService} from '@eg/core/pcrud'; +import {EgCatalogService} from '@eg/share/catalog/catalog.service'; + +@Component({ + selector: 'eg-bib-summary', + templateUrl: 'bib-summary.component.html' +}) +export class EgBibSummaryComponent implements OnInit { + + initDone: boolean = false; + + // If provided, the record will be fetched by the component. + @Input() recordId: number; + + // Otherwise, we'll use the provided bib summary object. + summary: any; + @Input() set bibSummary(s: any) { + this.summary = s; + if (this.initDone) this.fetchBibCallNumber(); + } + + expandDisplay: boolean = true; + + constructor( + private cat: EgCatalogService, + private net: EgNetService, + private pcrud: EgPcrudService + ) {} + + ngOnInit() { + this.initDone = true; + if (this.summary) { + this.fetchBibCallNumber(); + } else { + if (this.recordId) this.loadSummary(); + } + } + + loadSummary(): void { + this.cat.getBibSummary(this.recordId).then(summary => { + this.summary = summary; + this.fetchBibCallNumber(); + + // Flesh the user data + this.pcrud.search('au', {id: [summary.creator, summary.editor]}) + .subscribe(user => { + if (user.id() == summary.creator) + summary.creator = user; + if (user.id() == summary.editor) + summary.editor = user; + }) + }); + } + + fetchBibCallNumber(): void { + if (!this.summary || this.summary.callNumber) return; + + // TODO labelClass = cat.default_classification_scheme YAOUS + let labelClass = 1; + + this.net.request( + 'open-ils.cat', + 'open-ils.cat.biblio.record.marc_cn.retrieve', + this.summary.id, labelClass + ).subscribe(cnArray => { + if (cnArray && cnArray.length > 0) { + let key1 = Object.keys(cnArray[0])[0]; + this.summary.callNumber = cnArray[0][key1]; + } + }); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/staff/splash.component.html b/Open-ILS/eg2-src/src/app/staff/splash.component.html new file mode 100644 index 0000000000..4846cc5e27 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/splash.component.html @@ -0,0 +1,121 @@ + + + + +
+ + +
+
+ +
+
+ +
+
+
+
+
Circulation and Patrons
+
+ +
+
+ +
+
+
+
Item Search and Cataloging
+
+
+
+
+
+ + + + + +
+
+ +
+ + Copy Buckets +
+
+
+
+
+ +
+
+
+
Administration
+
+ +
+
+
+
+ diff --git a/Open-ILS/eg2-src/src/app/staff/splash.component.ts b/Open-ILS/eg2-src/src/app/staff/splash.component.ts new file mode 100644 index 0000000000..e113437229 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/staff/splash.component.ts @@ -0,0 +1,38 @@ +import {Component, OnInit, Renderer} from '@angular/core'; +import {Router} from '@angular/router'; + +@Component({ + templateUrl: 'splash.component.html' +}) + +export class EgStaffSplashComponent implements OnInit { + + catSearchQuery: string; + + constructor( + private renderer: Renderer, + private router: Router + ) {} + + ngOnInit() { + + // Focus catalog search form + this.renderer.selectRootElement('#catalog-search-input').focus(); + } + + checkEnter($event: any): void { + if ($event.keyCode == 13) + this.searchCatalog(); + } + + searchCatalog(): void { + if (!this.catSearchQuery) return; + + this.router.navigate( + ['/staff/catalog/search'], + {queryParams: {query : this.catSearchQuery}} + ); + } +} + + diff --git a/Open-ILS/eg2-src/src/app/welcome.component.html b/Open-ILS/eg2-src/src/app/welcome.component.html new file mode 100644 index 0000000000..3ce97cc247 --- /dev/null +++ b/Open-ILS/eg2-src/src/app/welcome.component.html @@ -0,0 +1,11 @@ +
+

Welcome to Webby

+

+ If you see this page, you're probably in good shape... +

+
+

+ But maybe you meant to go to the staff page + or the catalog. +

+
diff --git a/Open-ILS/eg2-src/src/app/welcome.component.ts b/Open-ILS/eg2-src/src/app/welcome.component.ts new file mode 100644 index 0000000000..398d12776b --- /dev/null +++ b/Open-ILS/eg2-src/src/app/welcome.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl : './welcome.component.html' +}) + +export class WelcomeComponent implements OnInit { + + ngOnInit() { + } +} + + + diff --git a/Open-ILS/eg2-src/src/assets/.gitkeep b/Open-ILS/eg2-src/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Open-ILS/eg2-src/src/environments/environment.prod.ts b/Open-ILS/eg2-src/src/environments/environment.prod.ts new file mode 100644 index 0000000000..3612073bc3 --- /dev/null +++ b/Open-ILS/eg2-src/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/Open-ILS/eg2-src/src/environments/environment.ts b/Open-ILS/eg2-src/src/environments/environment.ts new file mode 100644 index 0000000000..b7f639aeca --- /dev/null +++ b/Open-ILS/eg2-src/src/environments/environment.ts @@ -0,0 +1,8 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +export const environment = { + production: false +}; diff --git a/Open-ILS/eg2-src/src/favicon.ico b/Open-ILS/eg2-src/src/favicon.ico new file mode 100644 index 0000000000..8081c7ceaf Binary files /dev/null and b/Open-ILS/eg2-src/src/favicon.ico differ diff --git a/Open-ILS/eg2-src/src/index.html b/Open-ILS/eg2-src/src/index.html new file mode 100644 index 0000000000..a876726798 --- /dev/null +++ b/Open-ILS/eg2-src/src/index.html @@ -0,0 +1,27 @@ + + + + + AngEG + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/eg2-src/src/main.ts b/Open-ILS/eg2-src/src/main.ts new file mode 100644 index 0000000000..08b359c3b7 --- /dev/null +++ b/Open-ILS/eg2-src/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { EgBaseModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(EgBaseModule) + .catch(err => console.log(err)); diff --git a/Open-ILS/eg2-src/src/polyfills.ts b/Open-ILS/eg2-src/src/polyfills.ts new file mode 100644 index 0000000000..20d40751a6 --- /dev/null +++ b/Open-ILS/eg2-src/src/polyfills.ts @@ -0,0 +1,76 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Required to support Web Animations `@angular/platform-browser/animations`. + * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + + +/*************************************************************************************************** + * Zone JS is required by Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +/** + * Date, currency, decimal and percent pipes. + * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 + */ +// import 'intl'; // Run `npm install --save intl`. +/** + * Need to import at least one locale-data with intl. + */ +// import 'intl/locale-data/jsonp/en'; diff --git a/Open-ILS/eg2-src/src/styles.css b/Open-ILS/eg2-src/src/styles.css new file mode 100644 index 0000000000..c580fb05cb --- /dev/null +++ b/Open-ILS/eg2-src/src/styles.css @@ -0,0 +1,67 @@ +/* You can add global styles to this file, and also import other style files */ + +/** material design experiments +@import "~@angular/material/prebuilt-themes/indigo-pink.css"; +*/ + + +/** BS default fonts are huge */ +body, .form-control, .btn { + /* This more or less matches the font size of the angularjs client. + * The default BS4 font of 1rem is comically large. + */ + font-size: .88rem; +} +h2 {font-size: 1.25rem} +h3 {font-size: 1.15rem} +h4 {font-size: 1.05rem} +h5 {font-size: .95rem} + +.small-text-1 {font-size: 85%} + + +/** Ang5 routes on clicks to href's with no values, so we can't have + * bare href's to force anchor styling. Use this for anchors w/ no href. + * TODO: should we style all of them? a:not([href]) .... + * */ +.no-href { + cursor: pointer; + color: #007bff; +} + + +/** BS has flex utility classes, but none for specifying flex widths */ +.flex-1 {flex: 1} +.flex-2 {flex: 2} +.flex-3 {flex: 3} +.flex-4 {flex: 4} +.flex-5 {flex: 5} + + +/* usefulf for mat-icon buttons without any background or borders */ +.material-icon-button { + /* Transparent background */ + border: none; + background-color: rgba(0, 0, 0, 0.0); + padding-left: .25rem; + padding-right: .25rem; /* default .5rem */ +} + +.material-icons { + /** default is 24px which is pretty chunky */ + font-size: 22px; +} + +/* allow spans/labels to vertically orient with material icons */ +.label-with-material-icon { + display: inline-flex; + vertical-align: middle; + align-items: center; +} + +/* Default .card padding is extreme */ +.tight-card .card-body, +.tight-card .list-group-item { + padding: .25rem; +} + diff --git a/Open-ILS/eg2-src/src/test.ts b/Open-ILS/eg2-src/src/test.ts new file mode 100644 index 0000000000..cd612eeb0e --- /dev/null +++ b/Open-ILS/eg2-src/src/test.ts @@ -0,0 +1,32 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. +declare const __karma__: any; +declare const require: any; + +// Prevent Karma from running prematurely. +__karma__.loaded = function () {}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); +// Finally, start Karma to run the tests. +__karma__.start(); diff --git a/Open-ILS/eg2-src/src/tsconfig.app.json b/Open-ILS/eg2-src/src/tsconfig.app.json new file mode 100644 index 0000000000..39ba8dbacb --- /dev/null +++ b/Open-ILS/eg2-src/src/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "baseUrl": "./", + "module": "es2015", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/Open-ILS/eg2-src/src/tsconfig.spec.json b/Open-ILS/eg2-src/src/tsconfig.spec.json new file mode 100644 index 0000000000..63d89ff283 --- /dev/null +++ b/Open-ILS/eg2-src/src/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/Open-ILS/eg2-src/src/typings.d.ts b/Open-ILS/eg2-src/src/typings.d.ts new file mode 100644 index 0000000000..ef5c7bd620 --- /dev/null +++ b/Open-ILS/eg2-src/src/typings.d.ts @@ -0,0 +1,5 @@ +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} diff --git a/Open-ILS/eg2-src/tsconfig.json b/Open-ILS/eg2-src/tsconfig.json new file mode 100644 index 0000000000..14a504dc91 --- /dev/null +++ b/Open-ILS/eg2-src/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "baseUrl": "src", + "paths": { + "@eg/*": ["app/*"], + "@env/*": ["environments/*"] + }, + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/Open-ILS/eg2-src/tslint.json b/Open-ILS/eg2-src/tslint.json new file mode 100644 index 0000000000..c24dc293d7 --- /dev/null +++ b/Open-ILS/eg2-src/tslint.json @@ -0,0 +1,141 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs", + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "typeof-compare": true, + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "invoke-injectable": true + } +} diff --git a/Open-ILS/webby-src/.angular-cli.json b/Open-ILS/webby-src/.angular-cli.json deleted file mode 100644 index a90b80061d..0000000000 --- a/Open-ILS/webby-src/.angular-cli.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "project": { - "name": "eg" - }, - "apps": [ - { - "root": "src", - "outDir": "dist", - "assets": [ - "assets", - "favicon.ico" - ], - "index": "index.html", - "main": "main.ts", - "polyfills": "polyfills.ts", - "test": "test.ts", - "tsconfig": "tsconfig.app.json", - "testTsconfig": "tsconfig.spec.json", - "prefix": "app", - "styles": [ - "styles.css" - ], - "scripts": [], - "environmentSource": "environments/environment.ts", - "environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" - } - } - ], - "e2e": { - "protractor": { - "config": "./protractor.conf.js" - } - }, - "lint": [ - { - "project": "src/tsconfig.app.json", - "exclude": "**/node_modules/**" - }, - { - "project": "src/tsconfig.spec.json", - "exclude": "**/node_modules/**" - }, - { - "project": "e2e/tsconfig.e2e.json", - "exclude": "**/node_modules/**" - } - ], - "test": { - "karma": { - "config": "./karma.conf.js" - } - }, - "defaults": { - "styleExt": "css", - "component": {} - } -} diff --git a/Open-ILS/webby-src/.editorconfig b/Open-ILS/webby-src/.editorconfig deleted file mode 100644 index 6e87a003da..0000000000 --- a/Open-ILS/webby-src/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# Editor configuration, see http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/Open-ILS/webby-src/.gitignore b/Open-ILS/webby-src/.gitignore deleted file mode 100644 index 54bfd2001e..0000000000 --- a/Open-ILS/webby-src/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist -/tmp -/out-tsc - -# dependencies -/node_modules - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -# misc -/.sass-cache -/connect.lock -/coverage -/libpeerconnection.log -npm-debug.log -testem.log -/typings - -# e2e -/e2e/*.js -/e2e/*.map - -# System Files -.DS_Store -Thumbs.db diff --git a/Open-ILS/webby-src/README.adoc b/Open-ILS/webby-src/README.adoc deleted file mode 100644 index fd58af9902..0000000000 --- a/Open-ILS/webby-src/README.adoc +++ /dev/null @@ -1,17 +0,0 @@ -= EG Angular2 App = - -=== Apache Configuration === - -[source,conf] ---------------------------------------------------------------------- - - FallbackResource /webby/index.html - ---------------------------------------------------------------------- - -=== Transpile + Deploy in --watch mode for Dev === - -[source,sh] ---------------------------------------------------------------------- -ng build --deploy-url /webby/ --base-href /webby/ --output-path ../web/webby/ --watch ---------------------------------------------------------------------- diff --git a/Open-ILS/webby-src/e2e/app.e2e-spec.ts b/Open-ILS/webby-src/e2e/app.e2e-spec.ts deleted file mode 100644 index c2a69a8a6c..0000000000 --- a/Open-ILS/webby-src/e2e/app.e2e-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AppPage } from './app.po'; - -describe('eg App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display welcome message', () => { - page.navigateTo(); - expect(page.getParagraphText()).toEqual('Welcome to app!'); - }); -}); diff --git a/Open-ILS/webby-src/e2e/app.po.ts b/Open-ILS/webby-src/e2e/app.po.ts deleted file mode 100644 index 82ea75ba50..0000000000 --- a/Open-ILS/webby-src/e2e/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get('/'); - } - - getParagraphText() { - return element(by.css('app-root h1')).getText(); - } -} diff --git a/Open-ILS/webby-src/e2e/tsconfig.e2e.json b/Open-ILS/webby-src/e2e/tsconfig.e2e.json deleted file mode 100644 index 1d9e5edf09..0000000000 --- a/Open-ILS/webby-src/e2e/tsconfig.e2e.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/e2e", - "baseUrl": "./", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} diff --git a/Open-ILS/webby-src/karma.conf.js b/Open-ILS/webby-src/karma.conf.js deleted file mode 100644 index af139fada3..0000000000 --- a/Open-ILS/webby-src/karma.conf.js +++ /dev/null @@ -1,33 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular/cli'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular/cli/plugins/karma') - ], - client:{ - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - reports: [ 'html', 'lcovonly' ], - fixWebpackSourcePaths: true - }, - angularCli: { - environment: 'dev' - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false - }); -}; diff --git a/Open-ILS/webby-src/package.json b/Open-ILS/webby-src/package.json deleted file mode 100644 index 41b5925989..0000000000 --- a/Open-ILS/webby-src/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "eg", - "version": "0.0.0", - "license": "MIT", - "scripts": { - "ng": "ng", - "start": "ng serve", - "build": "ng build", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e" - }, - "private": true, - "dependencies": { - "@angular/animations": "^5.0.0", - "@angular/common": "^5.0.0", - "@angular/compiler": "^5.0.0", - "@angular/core": "^5.0.0", - "@angular/forms": "^5.0.0", - "@angular/http": "^5.0.0", - "@angular/platform-browser": "^5.0.0", - "@angular/platform-browser-dynamic": "^5.0.0", - "@angular/router": "^5.0.0", - "@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.5", - "core-js": "^2.4.1", - "jquery": "^3.2.1", - "ngx-cookie": "^2.0.1", - "rxjs": "^5.5.2", - "zone.js": "^0.8.14" - }, - "devDependencies": { - "@angular/cli": "1.5.1", - "@angular/compiler-cli": "^5.0.0", - "@angular/language-service": "^5.0.0", - "@types/jasmine": "~2.5.53", - "@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", - "karma": "~1.7.0", - "karma-chrome-launcher": "~2.1.1", - "karma-cli": "~1.0.1", - "karma-coverage-istanbul-reporter": "^1.2.1", - "karma-jasmine": "~1.1.0", - "karma-jasmine-html-reporter": "^0.2.2", - "protractor": "~5.1.2", - "ts-node": "~3.2.0", - "tslint": "~5.7.0", - "typescript": "~2.4.2" - } -} diff --git a/Open-ILS/webby-src/protractor.conf.js b/Open-ILS/webby-src/protractor.conf.js deleted file mode 100644 index 7ee3b5ee86..0000000000 --- a/Open-ILS/webby-src/protractor.conf.js +++ /dev/null @@ -1,28 +0,0 @@ -// Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -exports.config = { - allScriptsTimeout: 11000, - specs: [ - './e2e/**/*.e2e-spec.ts' - ], - capabilities: { - 'browserName': 'chrome' - }, - directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} - }, - onPrepare() { - require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' - }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -}; diff --git a/Open-ILS/webby-src/src/app/app.component.ts b/Open-ILS/webby-src/src/app/app.component.ts deleted file mode 100644 index d049f7a828..0000000000 --- a/Open-ILS/webby-src/src/app/app.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {Component} from '@angular/core'; - -@Component({ - selector: 'eg-root', - template: '' -}) - -export class EgBaseComponent { -} - - diff --git a/Open-ILS/webby-src/src/app/app.module.ts b/Open-ILS/webby-src/src/app/app.module.ts deleted file mode 100644 index d9d06e3b27..0000000000 --- a/Open-ILS/webby-src/src/app/app.module.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * EgBaseModule is the shared starting point for all apps. - * It provides the root router and a simple welcome page for - * users that end up here accidentally. - */ -import {BrowserModule} from '@angular/platform-browser'; -import {NgModule} from '@angular/core'; -import {Router} from '@angular/router'; // Debugging -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; // ng-bootstrap -import {CookieModule} from 'ngx-cookie'; // import CookieMonster - -import {EgBaseComponent} from './app.component'; -import {EgBaseRoutingModule} from './routing.module'; -import {WelcomeComponent} from './welcome.component'; - -// Import and 'provide' globally required services. -import {EgEventService} from '@eg/core/event'; -import {EgStoreService} from '@eg/core/store'; -import {EgIdlService} from '@eg/core/idl'; -import {EgNetService} from '@eg/core/net'; -import {EgAuthService} from '@eg/core/auth'; -import {EgPcrudService} from '@eg/core/pcrud'; -import {EgOrgService} from '@eg/core/org'; - -@NgModule({ - declarations: [ - EgBaseComponent, - WelcomeComponent - ], - imports: [ - EgBaseRoutingModule, - BrowserModule, - NgbModule.forRoot(), - CookieModule.forRoot() - ], - providers: [ - EgEventService, - EgStoreService, - EgIdlService, - EgNetService, - EgAuthService, - EgPcrudService, - EgOrgService - ], - exports: [], - bootstrap: [EgBaseComponent] -}) - -export class EgBaseModule { - constructor(router: Router) { - /* - console.debug('Routes: ', - JSON.stringify(router.config, undefined, 2)); - */ - } -} diff --git a/Open-ILS/webby-src/src/app/core/README b/Open-ILS/webby-src/src/app/core/README deleted file mode 100644 index 58828bed85..0000000000 --- a/Open-ILS/webby-src/src/app/core/README +++ /dev/null @@ -1,8 +0,0 @@ -Core Angular services and assocated types/classes. - -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. - diff --git a/Open-ILS/webby-src/src/app/core/auth.ts b/Open-ILS/webby-src/src/app/core/auth.ts deleted file mode 100644 index 611797abeb..0000000000 --- a/Open-ILS/webby-src/src/app/core/auth.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * - */ -import { Injectable, EventEmitter } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; -import { EgNetService } from './net'; -import { EgEventService, EgEvent } from './event'; -import { EgIdlService, EgIdlObject } from './idl'; -import { EgStoreService } from './store'; - -// Models a login instance. -class EgAuthUser { - user: EgIdlObject; - workstation: string; // workstation name - token: string; - authtime: number; - - constructor(token: string, authtime: number, workstation?: string) { - this.token = token; - this.workstation = workstation; - this.authtime = authtime; - } -} - -// Params required for calling the login() method. -interface EgAuthLoginArgs { - username: string, - password: string, - type: string, - workstation?: string -} - -export enum EgAuthWsState { - PENDING, - NOT_USED, - NOT_FOUND_SERVER, - NOT_FOUND_LOCAL, - VALID -}; - -@Injectable() -export class EgAuthService { - - private activeUser: EgAuthUser; - - // opChangeUser refers to the user that has been superseded during - // an op-change event. This use will become the activeUser once - // again, when the op-change cycle has completed. - private opChangeUser: EgAuthUser; - - workstationState: EgAuthWsState = EgAuthWsState.PENDING; - - redirectUrl: string; - - constructor( - private egEvt: EgEventService, - private net: EgNetService, - private store: EgStoreService - ) {} - - // - Accessor functions alway refer to the active user. - - user(): EgIdlObject { - return this.activeUser.user - }; - - // Workstation name. - workstation(): string { - return this.activeUser.workstation; - }; - - token(): string { - return this.activeUser ? this.activeUser.token : null; - }; - - authtime(): Number { - return this.activeUser.authtime - }; - - // NOTE: EgNetService emits an event if the auth session has expired. - testAuthToken(): Promise { - - this.activeUser = new EgAuthUser( - this.store.getLoginSessionItem('eg.auth.token'), - this.store.getLoginSessionItem('eg.auth.time') - ); - - if (!this.token()) return Promise.reject('no authtoken'); - - return new Promise( (resolve, reject) => { - this.net.request( - 'open-ils.auth', - 'open-ils.auth.session.retrieve', this.token() - ).subscribe( - user => { - // EgNetService interceps NO_SESSION events. - // We can only get here if the session is valid. - this.activeUser.user = user; - this.sessionPoll(); - resolve(); - }, - err => { reject(); } - ); - }); - } - - checkWorkstation(): void { - // TODO: - // Emits event on invalid workstation. - } - - login(args: EgAuthLoginArgs, isOpChange?: boolean): Promise { - - return new Promise((resolve, reject) => { - this.net.request('open-ils.auth', 'open-ils.auth.login', args) - .subscribe(res => { - this.handleLoginResponse(args, this.egEvt.parse(res), isOpChange) - .then( - ok => resolve(ok), - notOk => reject(notOk) - ); - }); - }); - } - - handleLoginResponse( - args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise { - - switch (evt.textcode) { - case 'SUCCESS': - this.handleLoginOk(args, evt, isOpChange); - return Promise.resolve(); - - case 'WORKSTATION_NOT_FOUND': - console.error(`No such workstation "${args.workstation}"`); - this.workstationState = EgAuthWsState.NOT_FOUND_SERVER; - delete args.workstation; - return this.login(args, isOpChange); - - default: - console.error(`Login returned unexpected event: ${evt}`); - return Promise.reject('login failed'); - } - } - - // Stash the login data - handleLoginOk(args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): void { - - if (isOpChange) { - this.store.setLoginSessionItem('eg.auth.token.oc', this.token()); - this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime()); - this.opChangeUser = this.activeUser; - } - - this.activeUser = new EgAuthUser( - evt.payload.authtoken, - evt.payload.authtime, - args.workstation - ); - - this.store.setLoginSessionItem('eg.auth.token', this.token()); - this.store.setLoginSessionItem('eg.auth.time', this.authtime()); - } - - undoOpChange(): Promise { - if (this.opChangeUser) { - this.deleteSession(); - this.activeUser = this.opChangeUser; - this.opChangeUser = null; - this.store.removeLoginSessionItem('eg.auth.token.oc'); - this.store.removeLoginSessionItem('eg.auth.time.oc'); - this.store.setLoginSessionItem('eg.auth.token', this.token()); - this.store.setLoginSessionItem('eg.auth.time', this.authtime()); - } - return this.testAuthToken(); - } - - sessionPoll(): void { - // TODO - } - - // Resolves if login workstation matches a workstation known to this - // browser instance. - verifyWorkstation(): Promise { - return new Promise((resolve, reject) => { - - if (!this.user()) { - this.workstationState = EgAuthWsState.PENDING; - reject(); - return; - } - - if (!this.user().wsid()) { - this.workstationState = EgAuthWsState.NOT_USED; - reject(); - return; - } - - this.store.getItem('eg.workstation.all') - .then(workstations => { - if (!workstations) workstations = []; - - let ws = workstations.filter( - w => {return w.id == this.user().wsid()})[0]; - - if (ws) { - this.activeUser.workstation = ws.name; - this.workstationState = EgAuthWsState.VALID; - resolve(); - } else { - this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL; - reject(); - } - }); - }); - } - - deleteSession(): void { - if (this.token()) { - this.net.request( - 'open-ils.auth', - 'open-ils.auth.session.delete', this.token()) - .subscribe(x => console.debug('logged out')) - } - } - - logout(broadcast?: boolean) { - console.debug('logging out'); - - if (broadcast) { - // TODO - //this.authChannel.postMessage({action : 'logout'}); - } - - this.deleteSession(); - this.store.clearLoginSessionItems(); - this.activeUser = null; - this.opChangeUser = null; - } -} diff --git a/Open-ILS/webby-src/src/app/core/event.ts b/Open-ILS/webby-src/src/app/core/event.ts deleted file mode 100644 index 3f6afc7d30..0000000000 --- a/Open-ILS/webby-src/src/app/core/event.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Injectable } from '@angular/core'; - -export class EgEvent { - code : Number; - textcode : String; - payload : any; - desc : String; - debug : String; - note : String; - servertime : String; - ilsperm : String; - ilspermloc : Number; - success : Boolean = false; - - toString(): String { - let s = `Event: ${this.code}:${this.textcode} -> ${this.desc}`; - if (this.ilsperm) - s += ` ${this.ilsperm}@${this.ilspermloc}`; - if (this.note) - s += `\n${this.note}`; - return s; - } -} - -@Injectable() -export class EgEventService { - - /** - * Returns an EgEvent if 'thing' is an event, null otherwise. - */ - parse(thing: any): EgEvent { - - // All events have a textcode - if (thing && typeof thing == 'object' && 'textcode' in thing) { - - let evt = new EgEvent(); - - ['textcode','payload','desc','note','servertime','ilsperm'] - .forEach(field => { evt[field] = thing[field]; }); - - evt.debug = thing.stacktrace; - evt.code = new Number(thing.code); - evt.ilspermloc = new Number(thing.ilspermloc); - evt.success = thing.textcode == 'SUCCESS'; - - return evt; - } - - return null; - } -} - - diff --git a/Open-ILS/webby-src/src/app/core/idl.ts b/Open-ILS/webby-src/src/app/core/idl.ts deleted file mode 100644 index 8f46933954..0000000000 --- a/Open-ILS/webby-src/src/app/core/idl.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable } from '@angular/core'; - -// Added globally by /IDL2js -declare var _preload_fieldmapper_IDL: Object; - -/** - * NOTE: To achieve full type strictness and avoid compile warnings, - * we would likely have to pre-compile the IDL down to a .ts file with all - * of the IDL class and field definitions. - */ - -/** - * Every IDL object class implements this interface. - */ -export interface EgIdlObject { - a: any[]; - classname: String; - _isfieldmapper: Boolean; - // Dynamically appended functions from the IDL. - [fields: string]: any; -} - -@Injectable() -export class EgIdlService { - - classes = {}; // IDL class metadata - constructors = {}; // IDL instance generators - - /** - * Create a new IDL object instance. - */ - create(cls: string, seed?:any[]): EgIdlObject { - if (this.constructors[cls]) - return new this.constructors[cls](seed); - throw new Error(`No such IDL class ${cls}`); - } - - parseIdl(): void { - - try { - this.classes = _preload_fieldmapper_IDL; - } catch (E) { - console.error('IDL (IDL2js) not found. Is the system running?'); - return; - } - - /** - * Creates the class constructor and getter/setter - * methods for each IDL class. - */ - let mkclass = (cls, fields) => { - this.classes[cls].classname = cls; - - // This dance lets us encode each IDL object with the - // EgIdlObject interface. Useful for adding type restrictions - // where desired for functions, etc. - let generator:any = ((): EgIdlObject => { - - var x:any = function(seed) { - this.a = seed || []; - this.classname = cls; - this._isfieldmapper = true; - }; - - fields.forEach(function(field, idx) { - x.prototype[field.name] = function(n) { - if (arguments.length==1) this.a[idx] = n; - return this.a[idx]; - } - }); - - return x; - }); - - this.constructors[cls] = generator(); - - // global class constructors required for JSON_v1.js - // TODO: polluting the window namespace w/ every IDL class - // is less than ideal. - window[cls] = this.constructors[cls]; - } - - for (var cls in this.classes) - mkclass(cls, this.classes[cls].fields); - }; -} - diff --git a/Open-ILS/webby-src/src/app/core/net.ts b/Open-ILS/webby-src/src/app/core/net.ts deleted file mode 100644 index b037de1840..0000000000 --- a/Open-ILS/webby-src/src/app/core/net.ts +++ /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; - 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; - authExpired$: EventEmitter; - - // 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(); - this.authExpired$ = new EventEmitter(); - } - - // Standard request call -- Variadic params version - request(service: String, method: String, ...params: any[]): Observable { - return this.requestWithParamList(service, method, params); - } - - // Array params version - requestWithParamList(service: String, - method: String, params: any[]): Observable { - return this.requestCompiled( - new EgNetRequest(service, method, params)); - } - - requestCompiled(request: EgNetRequest): Observable { - 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/org.ts b/Open-ILS/webby-src/src/app/core/org.ts deleted file mode 100644 index 44eddd6f62..0000000000 --- a/Open-ILS/webby-src/src/app/core/org.ts +++ /dev/null @@ -1,165 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs/Rx'; -import {EgIdlObject, EgIdlService} from './idl'; -import {EgPcrudService} from './pcrud'; - -type EgOrgNodeOrId = number | EgIdlObject; - -interface OrgFilter { - canHaveUsers?: boolean; - canHaveVolumes?: boolean; - opacVisible?: boolean; -} - -@Injectable() -export class EgOrgService { - - private orgMap = {}; - private orgList: EgIdlObject[] = []; - private orgTree: EgIdlObject; // root node + children - - constructor( - private pcrud: EgPcrudService - ) {} - - get(nodeOrId: EgOrgNodeOrId): EgIdlObject { - if (typeof nodeOrId == 'object') - return nodeOrId; - return this.orgMap[nodeOrId]; - }; - - list(): EgIdlObject[] { - return this.orgList; - }; - - /** - * Returns a list of org units that match the selected criteria. - * Unset filter options are ignored. - */ - filterList(filter: OrgFilter, asId: boolean): any[] { - let list = []; - this.list().forEach(org => { - - let chu = filter.canHaveUsers; - if (chu && !this.canHaveUsers(org)) return; - if (chu === false && this.canHaveUsers(org)) return; - - let chv = filter.canHaveVolumes; - if (chv && !this.canHaveVolumes(org)) return; - if (chv === false && this.canHaveVolumes(org)) return; - - let ov = filter.opacVisible; - if (ov && !this.opacVisible(org)) return; - if (ov === false && this.opacVisible(org)) return; - - // All filter tests passed. Add it to the list - list.push(asId ? org.id() : org); - }); - - return list; - } - - tree(): EgIdlObject { - return this.orgTree; - } - - // get the root OU - root(): EgIdlObject { - return this.orgList[0]; - } - - // list of org_unit objects or IDs for ancestors + me - ancestors(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { - let node = this.get(nodeOrId); - if (!node) return []; - let nodes = [node]; - while( (node = this.get(node.parent_ou()))) - nodes.push(node); - if (asId) return nodes.map(n => n.id()); - return nodes; - }; - - // tests that a node can have users - canHaveUsers(nodeOrId): boolean { - return this - .get(nodeOrId) - .ou_type() - .can_have_users() == 't'; - } - - // tests that a node can have volumes - canHaveVolumes(nodeOrId): boolean { - return this - .get(nodeOrId) - .ou_type() - .can_have_vols() == 't'; - } - - opacVisible(nodeOrId): boolean { - return this.get(nodeOrId).opac_visible() == 't'; - } - - // list of org_unit objects or IDs for me + descendants - descendants(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { - let node = this.get(nodeOrId); - if (!node) return []; - let nodes = []; - function descend(n) { - nodes.push(n); - n.children().forEach(descend); - } - descend(node); - if (asId) - return nodes.map(function(n){return n.id()}); - return nodes; - } - - // list of org_unit objects or IDs for ancestors + me + descendants - fullPath(nodeOrId: EgOrgNodeOrId, asId?: boolean): any[] { - let list = this.ancestors(nodeOrId, false).concat( - this.descendants(nodeOrId, false).slice(1)); - if (asId) - return list.map(function(n){return n.id()}); - return list; - } - - sortTree(sortField?: string, node?: EgIdlObject): void { - if (!sortField) sortField = 'shortname'; - if (!node) node = this.orgTree; - node.children( - node.children.sort((a, b) => { - return a[sortField]() < b[sortField]() ? -1 : 1 - }) - ); - node.children.forEach(n => this.sortTree(n)); - } - - absorbTree(node?: EgIdlObject): void { - if (!node) { - node = this.orgTree; - this.orgMap = {}; - this.orgList = []; - } - this.orgMap[node.id()] = node; - this.orgList.push(node); - node.children().forEach(c => this.absorbTree(c)); - } - - /** - * Grabs all of the org units from the server, chops them up into - * various shapes, then returns an "all done" promise. - */ - fetchOrgs(): Promise { - return this.pcrud.search('aou', {parent_ou : null}, - {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}, - {anonymous : true} - ).toPromise().then(tree => { - // ingest tree, etc. - this.orgTree = tree; - this.absorbTree(); - }); - } - - // NOTE: see ./org-settings.service for settings - // TODO: ^-- -} diff --git a/Open-ILS/webby-src/src/app/core/pcrud.ts b/Open-ILS/webby-src/src/app/core/pcrud.ts deleted file mode 100644 index 0cee7d3073..0000000000 --- a/Open-ILS/webby-src/src/app/core/pcrud.ts +++ /dev/null @@ -1,311 +0,0 @@ -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'; -import {EgAuthService} from './auth'; - -// Used for debugging. -declare var js2JSON: (jsThing:any) => string; -declare var OpenSRF: any; // creating sessions - -export interface EgPcrudReqOps { - authoritative?: boolean; - anonymous?: boolean; - idlist?: boolean; - atomic?: boolean; -} - -// For for documentation purposes. -type EgPcrudResponse = any; - -export class EgPcrudContext { - - static verboseLogging: boolean = true; // - static identGenerator: number = 0; // for debug logging - - private ident: number; - private authoritative: boolean; - private xactCloseMode: string; - private cudIdx: number; - private cudAction: string; - private cudLast: EgPcrudResponse; - private cudList: EgIdlObject[]; - - private idl: EgIdlService; - private net: EgNetService; - private auth: EgAuthService; - - // Tracks nested CUD actions - cudObserver: Observer; - - session: any; // OpenSRF.ClientSession - - constructor( // passed in by parent service -- not injected - egIdl: EgIdlService, - egNet: EgNetService, - egAuth: EgAuthService - ) { - this.idl = egIdl; - this.net = egNet; - this.auth = egAuth; - this.xactCloseMode = 'rollback'; - this.ident = EgPcrudContext.identGenerator++; - this.session = new OpenSRF.ClientSession('open-ils.pcrud'); - } - - toString(): string { - return '[PCRUDContext ' + this.ident + ']'; - } - - log(msg: string): void { - if (EgPcrudContext.verboseLogging) - console.debug(this + ': ' + msg); - } - - err(msg: string): void { - console.error(this + ': ' + msg); - } - - token(reqOps?: EgPcrudReqOps): string { - return (reqOps && reqOps.anonymous) ? - 'ANONYMOUS' : this.auth.token(); - } - - connect(): Promise { - this.log('connect'); - return new Promise( (resolve, reject) => { - this.session.connect({ - onconnect : () => { resolve(this); } - }); - }) - } - - disconnect(): void { - this.log('disconnect'); - this.session.disconnect(); - } - - retrieve(fmClass: string, pkey: Number | string, - pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { - if (!reqOps) reqOps = {}; - this.authoritative = reqOps.authoritative || false; - return this.dispatch( - `open-ils.pcrud.retrieve.${fmClass}`, - [this.token(reqOps), pkey, pcrudOps]); - } - - retrieveAll(fmClass: string, pcrudOps?: any, - reqOps?: EgPcrudReqOps): Observable { - let search = {}; - search[this.idl.classes[fmClass].pkey] = {'!=' : null}; - return this.search(fmClass, search, pcrudOps, reqOps); - } - - search(fmClass: string, search: any, - pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { - reqOps = reqOps || {}; - this.authoritative = reqOps.authoritative || false; - - let returnType = reqOps.idlist ? 'id_list' : 'search'; - let method = `open-ils.pcrud.${returnType}.${fmClass}`; - - if (reqOps.atomic) method += '.atomic'; - - return this.dispatch(method, [this.token(reqOps), search, pcrudOps]); - } - - create(list: EgIdlObject[]): Observable { - return this.cud('create', list) - } - update(list: EgIdlObject[]): Observable { - return this.cud('update', list) - } - remove(list: EgIdlObject[]): Observable { - return this.cud('delete', list) - } - autoApply(list: EgIdlObject[]): Observable { // RENAMED - return this.cud('auto', list) - } - - xactClose(): Observable { - return this.sendRequest( - 'open-ils.pcrud.transaction.' + this.xactCloseMode, - [this.token()] - ); - }; - - xactBegin(): Observable { - return this.sendRequest( - 'open-ils.pcrud.transaction.begin', [this.token()] - ); - }; - - private dispatch(method: string, params: any[]): Observable { - if (this.authoritative) { - return this.wrapXact(() => { - return this.sendRequest(method, params); - }); - } else { - return this.sendRequest(method, params) - } - }; - - - // => connect - // => xact_begin - // => action - // => xact_close(commit/rollback) - // => disconnect - wrapXact(mainFunc: () => Observable): Observable { - let this_ = this; - - return Observable.create(observer => { - - // 1. connect - this.connect() - - // 2. start the transaction - .then(() => {return this_.xactBegin().toPromise()}) - - // 3. execute the main body - .then(() => { - - mainFunc().subscribe( - res => observer.next(res), - err => observer.error(err), - () => { - this_.xactClose().toPromise().then(() => { - // 5. disconnect - this_.disconnect(); - // 6. all done - observer.complete(); - }); - } - ); - }) - }); - }; - - private sendRequest(method: string, - params: any[]): Observable { - - this.log(`sendRequest(${method})`); - - return this.net.requestCompiled( - new EgNetRequest( - 'open-ils.pcrud', method, params, this.session) - ); - } - - private cud(action: string, - list: EgIdlObject | EgIdlObject[]): Observable { - - this.log(`CUD(): ${action}`); - - this.cudIdx = 0; - this.cudAction = action; - this.xactCloseMode = 'commit'; - - if (!Array.isArray(list)) this.cudList = [list]; - - let this_ = this; - - return this.wrapXact(() => { - return Observable.create(observer => { - this_.cudObserver = observer; - this_.nextCudRequest(); - }); - }); - } - - /** - * Loops through the list of objects to update and sends - * them one at a time to the server for processing. Once - * all are done, the cudObserver is resolved. - */ - nextCudRequest(): void { - let this_ = this; - - if (this.cudIdx >= this.cudList.length) { - this.cudObserver.complete(); - return; - } - - let action = this.cudAction; - let fmObj = this.cudList[this.cudIdx++]; - - if (action == 'auto') { - if (fmObj.ischanged()) action = 'update'; - if (fmObj.isnew()) action = 'create'; - if (fmObj.isdeleted()) action = 'delete'; - - if (action == 'auto') { - // object does not need updating; move along - this.nextCudRequest(); - } - } - - this.sendRequest( - `open-ils.pcrud.${action}.${fmObj.classname}`, - [this.token(), fmObj] - ).subscribe( - res => this_.cudObserver.next(res), - err => this_.cudObserver.error(err), - () => this_.nextCudRequest() - ); - }; -} - -@Injectable() -export class EgPcrudService { - - constructor( - private idl: EgIdlService, - private net: EgNetService, - private auth: EgAuthService - ) {} - - // Pass-thru functions for one-off PCRUD calls - - connect(): Promise { - return this.newContext().connect(); - } - - newContext(): EgPcrudContext { - return new EgPcrudContext(this.idl, this.net, this.auth); - } - - retrieve(fmClass: string, pkey: Number | string, - pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { - return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps); - } - - retrieveAll(fmClass: string, pcrudOps?: any, - reqOps?: EgPcrudReqOps): Observable { - return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps); - } - - search(fmClass: string, search: any, - pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable { - return this.newContext().search(fmClass, search, pcrudOps, reqOps); - } - - create(list: EgIdlObject[]): Observable { - return this.newContext().create(list); - } - - update(list: EgIdlObject[]): Observable { - return this.newContext().update(list); - } - - remove(list: EgIdlObject[]): Observable { - return this.newContext().remove(list); - } - - autoApply(list: EgIdlObject[]): Observable { - return this.newContext().autoApply(list); - } -} - - diff --git a/Open-ILS/webby-src/src/app/core/store.ts b/Open-ILS/webby-src/src/app/core/store.ts deleted file mode 100644 index e1a879b390..0000000000 --- a/Open-ILS/webby-src/src/app/core/store.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Store and retrieve data from various sources. - */ -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; -import { CookieService } from 'ngx-cookie'; - -@Injectable() -export class EgStoreService { - - // Base path for cookie-based storage. - // Useful for limiting cookies to subsections of the application. - loginSessionBasePath: string; - - // Set of keys whose values should disappear at logout. - loginSessionKeys: string[] = [ - 'eg.auth.token', - 'eg.auth.time', - 'eg.auth.token.oc', - 'eg.auth.time.oc' - ]; - - constructor(private cookieService: CookieService) {} - - private parseJson(valJson: string): any { - if (valJson == null || valJson == '') return null; - try { - return JSON.parse(valJson); - } catch(E) { - console.error(`Failure to parse JSON: ${E} => ${valJson}`); - return null; - } - } - - /** - * Add a an app-local login session key - */ - addLoginSessionKey(key: string): void { - this.loginSessionKeys.push(key); - } - - setItem(key: string, val: any, isJson?: Boolean): Promise { - // TODO: route keys appropriately - this.setLocalItem(key, val, false); - return Promise.resolve(); - } - - setLocalItem(key: string, val: any, isJson?: Boolean): void { - if (!isJson) val = JSON.stringify(val); - window.localStorage.setItem(key, val); - } - - setServerItem(key: string, val: any): Promise { - return Promise.resolve(); - } - - setSessionItem(key: string, val: any, isJson?: Boolean): void { - if (!isJson) val = JSON.stringify(val); - window.sessionStorage.setItem(key, val); - } - - setLoginSessionItem(key: string, val: any, isJson?:Boolean): void { - if (!isJson) val = JSON.stringify(val); - this.cookieService.put(key, val, {path : this.loginSessionBasePath}); - } - - getItem(key: string): Promise { - // TODO: route keys appropriately - return Promise.resolve(this.getLocalItem(key)); - } - - getLocalItem(key: string): any { - return this.parseJson(window.localStorage.getItem(key)); - } - - getServerItem(key: string): Promise { - return Promise.resolve(); - } - - getSessionItem(key: string): any { - return this.parseJson(window.sessionStorage.getItem(key)); - } - - getLoginSessionItem(key: string): any { - return this.parseJson(this.cookieService.get(key)); - } - - removeItem(key: string): Promise { - // TODO: route keys appropriately - return Promise.resolve(this.removeLocalItem(key)); - } - - removeLocalItem(key: string): void { - window.localStorage.removeItem(key); - } - - removeServerItem(key: string): Promise { - return Promise.resolve(); - } - - removeSessionItem(key: string): void { - window.sessionStorage.removeItem(key); - } - - removeLoginSessionItem(key: string): void { - this.cookieService.remove(key, {path : this.loginSessionBasePath}); - } - - clearLoginSessionItems(): void { - this.loginSessionKeys.forEach( - key => this.removeLoginSessionItem(key) - ); - } -} - diff --git a/Open-ILS/webby-src/src/app/resolver.service.ts b/Open-ILS/webby-src/src/app/resolver.service.ts deleted file mode 100644 index 7ffa74bccb..0000000000 --- a/Open-ILS/webby-src/src/app/resolver.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Router, Resolve, RouterStateSnapshot, - ActivatedRouteSnapshot} from '@angular/router'; -import {EgIdlService} from '@eg/core/idl'; -import {EgOrgService} from '@eg/core/org'; - -@Injectable() -export class EgBaseResolver implements Resolve> { - - constructor( - private router: Router, - private idl: EgIdlService, - private org: EgOrgService, - ) {} - - resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Promise { - - console.debug('EgBaseResolver:resolve()'); - - // Load data common to all applications. - - this.idl.parseIdl(); - - return this.org.fetchOrgs(); - // Note that authentication happens at a deeper level, since - // some applications (e.g. a public catalog) do not require - // up-front authentication to access. - } -} diff --git a/Open-ILS/webby-src/src/app/routing.module.ts b/Open-ILS/webby-src/src/app/routing.module.ts deleted file mode 100644 index 7d7e70e71f..0000000000 --- a/Open-ILS/webby-src/src/app/routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { EgBaseResolver } from './resolver.service'; -import { WelcomeComponent } from './welcome.component'; - -/** - * Avoid requiring all apps to load all JS by lazy-loading sub-modules. - * When lazy loading, no module references should be directly imported. - * The refs are encoded in the loadChildren attribute of each route. - */ -const routes: Routes = [ - { path: '', - component: WelcomeComponent - }, { - path: 'staff', - resolve : {startup : EgBaseResolver}, - loadChildren: './staff/app.module#EgStaffModule' - } -]; - -@NgModule({ - imports: [ RouterModule.forRoot(routes) ], - exports: [ RouterModule ], - providers: [ EgBaseResolver ] -}) - -export class EgBaseRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/share/README b/Open-ILS/webby-src/src/app/share/README deleted file mode 100644 index 1a8b6e1646..0000000000 --- a/Open-ILS/webby-src/src/app/share/README +++ /dev/null @@ -1,7 +0,0 @@ -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/catalog/catalog-url.service.ts b/Open-ILS/webby-src/src/app/share/catalog/catalog-url.service.ts deleted file mode 100644 index 00f3203956..0000000000 --- a/Open-ILS/webby-src/src/app/share/catalog/catalog-url.service.ts +++ /dev/null @@ -1,128 +0,0 @@ -import {Injectable} from '@angular/core'; -import {ParamMap} from '@angular/router'; -import {EgOrgService} from '@eg/core/org'; -import {CatalogSearchContext, FacetFilter} from './search-context'; -import {CATALOG_CCVM_FILTERS} from './catalog.service'; - -@Injectable() -export class EgCatalogUrlService { - - // consider supporting a param name prefix/namespace - - constructor(private org: EgOrgService) { } - - /** - * Returns a URL query structure suitable for using with - * router.navigate(..., {queryParams:...}). - * No navigation is performed within. - */ - toUrlParams(context: CatalogSearchContext): - {[key: string]: string | string[]} { - - let params = { - query: [], - fieldClass: [], - joinOp: [], - matchOp: [], - facets: [], - org: null, - limit: null, - offset: null - }; - - params.limit = context.pager.limit; - if (context.pager.offset) - params.offset = context.pager.offset; - - // These fields can be copied directly into place - ['format','sort','available','global'] - .forEach(field => { - if (context[field]) { - // Only propagate applied values to the URL. - params[field] = context[field]; - } - }); - - context.query.forEach((q, idx) => { - ['query', 'fieldClass','joinOp','matchOp'].forEach(field => { - // Propagate all array-based fields regardless of - // whether a value is applied to ensure correct - // correlation between values. - params[field][idx] = context[field][idx]; - }); - }); - - // CCVM filters are encoded as comma-separated lists - Object.keys(context.ccvmFilters).forEach(code => { - if (context.ccvmFilters[code] && - context.ccvmFilters[code][0] != '') { - params[code] = context.ccvmFilters[code].join(','); - } - }); - - // Each facet is a JSON encoded blob of class, name, and value - context.facetFilters.forEach(facet => { - params.facets.push(JSON.stringify({ - c : facet.facetClass, - n : facet.facetName, - v : facet.facetValue - })); - }); - - params.org = context.searchOrg.id(); - - return params; - } - - /** - * Creates a new search context from the active route params. - */ - fromUrlParams(params: ParamMap): CatalogSearchContext { - let context = new CatalogSearchContext(); - - this.applyUrlParams(context, params); - - return context; - } - - applyUrlParams(context: CatalogSearchContext, params: ParamMap): void { - - // Reset query/filter args. The will be reconstructed below. - context.reset(); - - // These fields can be copied directly into place - ['format','sort','available','global'] - .forEach(field => { - let val = params.get(field); - if (val !== null) context[field] = val; - }); - - if (params.get('limit')) - context.pager.limit = +params.get('limit'); - - if (params.get('offset')) - context.pager.offset = +params.get('offset'); - - ['query','fieldClass','joinOp','matchOp'].forEach(field => { - let arr = params.getAll(field); - if (arr && arr.length) context[field] = arr; - }); - - CATALOG_CCVM_FILTERS.forEach(code => { - let val = params.get(code); - if (val) { - context.ccvmFilters[code] = val.split(/,/); - } else { - context.ccvmFilters[code] = ['']; - } - }); - - params.getAll('facets').forEach(blob => { - let facet = JSON.parse(blob); - context.addFacet(new FacetFilter(facet.c, facet.n, facet.v)); - }); - - context.searchOrg = - this.org.get(+params.get('org')) || this.org.root(); - } -} diff --git a/Open-ILS/webby-src/src/app/share/catalog/catalog.service.ts b/Open-ILS/webby-src/src/app/share/catalog/catalog.service.ts deleted file mode 100644 index 96f8d24ab4..0000000000 --- a/Open-ILS/webby-src/src/app/share/catalog/catalog.service.ts +++ /dev/null @@ -1,296 +0,0 @@ -import {Injectable} 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'; -import {CatalogSearchContext, CatalogSearchState} from './search-context'; - -export const CATALOG_CCVM_FILTERS = [ - 'item_type', - 'item_form', - 'item_lang', - 'audience', - 'audience_group', - 'vr_format', - 'bib_level', - 'lit_form', - '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 { - - ccvmMap: {[ccvm:string] : EgIdlObject[]} = {}; - cmfMap: {[cmf:string] : EgIdlObject} = {}; - - // Keep a reference to the most recently retrieved facet data, - // since facet data is consistent across a given search. - // No need to re-fetch with every page of search data. - lastFacetData: any; - lastFacetKey: string; - - constructor( - private net: EgNetService, - private org: EgOrgService, - private unapi: EgUnapiService, - private pcrud: EgPcrudService - ) {} - - search(ctx: CatalogSearchContext): Promise { - ctx.searchState = CatalogSearchState.SEARCHING; - - var fullQuery = ctx.compileSearch(); - - console.debug(`search query: ${fullQuery}`); - - let method = 'open-ils.search.biblio.multiclass.query'; - if (ctx.isStaff) 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; - resolve(); - }); - }) - } - - applyResultData(ctx: CatalogSearchContext, result: any): void { - ctx.result = result; - ctx.pager.resultCount = result.count; - - // records[] tracks the current page of bib summaries. - result.records = []; - - // If this is a new search, reset the result IDs collection. - if (this.lastFacetKey != result.facet_key) ctx.resultIds = []; - - result.ids.forEach((blob, idx) => {ctx.addResultId(blob[0], idx)}); - } - - fetchBibSummaries(ctx: CatalogSearchContext): Promise { - let promises = []; - let depth = ctx.global ? - ctx.org.root().ou_type().depth() : - ctx.searchOrg.ou_type().depth(); - - ctx.currentResultIds().forEach((recId, idx) => { - promises.push( - this.getBibSummary(recId, ctx.searchOrg.id(), depth) - .then( - // idx maintains result sort order - summary => ctx.result.records[idx] = summary - ) - ); - }); - - return Promise.all(promises); - } - - fetchFacets(ctx: CatalogSearchContext): Promise { - - if (!ctx.result) - return Promise.reject('Cannot fetch facets without results'); - - if (this.lastFacetKey == ctx.result.facet_key) { - ctx.result.facetData = this.lastFacetData; - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - this.net.request('open-ils.search', - 'open-ils.search.facet_cache.retrieve', - ctx.result.facet_key - ).subscribe(facets => { - let facetData = {}; - Object.keys(facets).forEach(cmfId => { - let facetHash = facets[cmfId]; - let cmf = this.cmfMap[cmfId]; - - let cmfData = []; - Object.keys(facetHash).forEach(value => { - let count = facetHash[value]; - cmfData.push({value : value, count : count}); - }); - - if (!facetData[cmf.field_class()]) - facetData[cmf.field_class()] = {}; - - facetData[cmf.field_class()][cmf.name()] = { - cmfLabel : cmf.label(), - valueList : cmfData.sort((a, b) => { - if (a.count > b.count) return -1; - if (a.count < b.count) return 1; - // secondary alpha sort on display value - return a.value < b.value ? -1 : 1; - }) - }; - }); - - this.lastFacetKey = ctx.result.facet_key; - this.lastFacetData = ctx.result.facetData = facetData; - resolve(); - }); - }) - } - - fetchCcvms(): Promise { - - if (Object.keys(this.ccvmMap).length) - return Promise.resolve(); - - return new Promise((resolve, reject) => { - this.pcrud.search('ccvm', - {ctype : CATALOG_CCVM_FILTERS}, {}, {atomic: true} - ).subscribe(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 { - // 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(); - } - ) - }); - } - - - /** - * Bib record via UNAPI as mods (for now) with holdings summary - * and record attributes. - */ - getBibSummary(bibId: number, orgId?: number, depth?: number): Promise { - return new Promise((resolve, reject) => { - this.unapi.getAsXmlDocument({ - target: 'bre', - id: bibId, - extras: '{bre.extern,holdings_xml,mra}', - format: 'mods32', - orgId: orgId, - depth: depth - }).then(xmlDoc => { - let summary = this.translateBibSummary(xmlDoc); - summary.id = bibId; - resolve(summary); - }); - }); - } - - /** - * 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(xmlDoc: XMLDocument): any { // TODO: bib summary interface - - let response = { - copyCounts : [], - ccvms : {} - }; - - let resolver:any = (prefix: string): string => { - return NAMESPACE_MAPS[prefix] || null; - }; - - Object.keys(MODS_XPATH_AUTO).forEach(key => { - let result = xmlDoc.evaluate(MODS_XPATH_AUTO[key], xmlDoc, - resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null); - - let node = result.singleNodeValue; - if (node) response[key] = node.textContent; - }); - - let result = xmlDoc.evaluate(MODS_XPATH.extern, xmlDoc, - resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null); - - 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.textContent, - 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; - } -} diff --git a/Open-ILS/webby-src/src/app/share/catalog/search-context.ts b/Open-ILS/webby-src/src/app/share/catalog/search-context.ts deleted file mode 100644 index b3c21e53a8..0000000000 --- a/Open-ILS/webby-src/src/app/share/catalog/search-context.ts +++ /dev/null @@ -1,245 +0,0 @@ -import {EgOrgService} from '@eg/core/org'; -import {EgIdlObject} from '@eg/core/idl'; -import {Pager} from '@eg/share/util/pager'; -import {Params} from '@angular/router'; - -export enum CatalogSearchState { - PENDING, - SEARCHING, - COMPLETE -} - -export class FacetFilter { - facetClass: string; - facetName: string; - facetValue: string; - - constructor(cls: string, name: string, value: string) { - this.facetClass = cls; - this.facetName = name; - this.facetValue = value; - } - - equals(filter: FacetFilter): boolean { - return ( - this.facetClass == filter.facetClass && - this.facetName == filter.facetName && - this.facetValue == filter.facetValue - ); - } -} - -// Not an angular service. -// It's conceviable there could be multiple contexts. -export class CatalogSearchContext { - - // Search options and filters - available: boolean = false; - global: boolean = false; - sort: string; - fieldClass: string[]; - query: string[]; - joinOp: string[]; - matchOp: string[]; - format: string; - searchOrg: EgIdlObject; - ccvmFilters: {[ccvmCode:string] : string[]}; - facetFilters: FacetFilter[]; - isStaff: boolean; - - // Result from most recent search. - result: any = {}; - searchState: CatalogSearchState = CatalogSearchState.PENDING; - - // List of IDs in page/offset context. - resultIds: number[] = []; - - // Utility stuff - pager: Pager; - org: EgOrgService; - - constructor() { - this.pager = new Pager(); - this.reset(); - } - - // List of result IDs for the current page of data. - currentResultIds(): number[] { - let ids = []; - for ( - let idx = this.pager.offset; - idx < Math.min( - this.pager.offset + this.pager.limit, - this.pager.resultCount - ); - idx++ - ) {ids.push(this.resultIds[idx])} - return ids; - } - - addResultId(id: number, resultIdx: number ): void { - this.resultIds[resultIdx + this.pager.offset] = id; - } - - // Return the record at the requested index. - resultIdAt(index: number): number { - return this.resultIds[index] || null; - } - - // Return the index of the requested record - indexForResult(id: number): number { - for (let i = 0; i < this.resultIds.length; i++) { - if (this.resultIds[i] == id) - return i; - } - return null; - } - - /** - * Return search context to its default state, resetting search - * parameters and clearing any cached result data. - * This does not reset global filters like limit-to-available - * or search-global. - */ - reset(): void { - this.pager.offset = 0; - this.format = ''; - this.sort = ''; - this.query = ['']; - this.fieldClass = ['keyword']; - this.matchOp = ['contains']; - this.joinOp = ['']; - this.ccvmFilters = {}; - this.facetFilters = []; - this.result= {}; - this.resultIds = []; - this.searchState = CatalogSearchState.PENDING; - } - - isSearchable(): boolean { - return this.query.length && this.query[0] != ''; - } - - 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 => { - if (this.ccvmFilters[field][0] != '') - 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 joinOp = this.joinOp[idx]; - let matchOp = this.matchOp[idx]; - let fieldClass = this.fieldClass[idx]; - - let str = ''; - if (!query) return str; - - if (idx > 0) str += ' ' + joinOp + ' '; - - str += '('; - if (fieldClass) str += fieldClass + ':'; - - switch(matchOp) { - 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 + ')'; - } - - hasFacet(facet: FacetFilter): boolean { - return Boolean( - this.facetFilters.filter( - f => {return f.equals(facet)})[0] - ); - } - - removeFacet(facet: FacetFilter): void { - this.facetFilters = this.facetFilters.filter( - f => { return !f.equals(facet); }); - } - - addFacet(facet: FacetFilter): void { - if (!this.hasFacet(facet)) - this.facetFilters.push(facet); - } - - toggleFacet(facet: FacetFilter): void { - if (this.hasFacet(facet)) { - this.removeFacet(facet); - } else { - this.facetFilters.push(facet); - } - } -} - - diff --git a/Open-ILS/webby-src/src/app/share/org-select.component.html b/Open-ILS/webby-src/src/app/share/org-select.component.html deleted file mode 100644 index d7b9101c89..0000000000 --- a/Open-ILS/webby-src/src/app/share/org-select.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - -{{r.label}} - - - diff --git a/Open-ILS/webby-src/src/app/share/org-select.component.ts b/Open-ILS/webby-src/src/app/share/org-select.component.ts deleted file mode 100644 index 7738215b4b..0000000000 --- a/Open-ILS/webby-src/src/app/share/org-select.component.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core'; -import {Observable} from 'rxjs/Observable'; -import {map, debounceTime} from 'rxjs/operators'; -import {EgAuthService} from '@eg/core/auth'; -import {EgStoreService} from '@eg/core/store'; -import {EgOrgService} from '@eg/core/org'; -import {EgIdlObject} from '@eg/core/idl'; -import {NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap'; - -// Use a unicode char for spacing instead of ASCII=32 so the browser -// won't collapse the nested display entries down to a single space. -const PAD_SPACE: string = ' '; // U+2007 - -interface OrgDisplay { - id: number; - label: string; - disabled: boolean; -} - -@Component({ - selector: 'eg-org-select', - templateUrl: './org-select.component.html' -}) -export class EgOrgSelectComponent implements OnInit { - - selected: OrgDisplay; - startOrg: EgIdlObject; - hidden: number[] = []; - disabled: number[] = []; - - // Read-only properties optionally provided by the calling component. - @Input() placeholder: string; - @Input() stickySetting: string; - @Input() displayField: string = 'shortname'; - - @Input() set initialOrg(org: EgIdlObject) { - if (org) this.startOrg = org; - } - - @Input() set hideOrgs(ids: number[]) { - if (ids) this.hidden = ids; - } - - @Input() set disableOrgs(ids: number[]) { - if (ids) this.disabled = ids; - } - - /** Emitted when the org unit value is changed via the selector. - * Does not fire on initialOrg. - */ - @Output() onChange = new EventEmitter(); - - constructor( - private auth: EgAuthService, - private store: EgStoreService, - private org: EgOrgService - ) {} - - ngOnInit() { - if (this.startOrg) { - this.selected = this.formatForDisplay(this.startOrg); - } - } - - formatForDisplay(org: EgIdlObject): OrgDisplay { - return { - id : org.id(), - label : PAD_SPACE.repeat(org.ou_type().depth()) - + org[this.displayField](), - disabled : false - }; - } - - orgChanged(selEvent: NgbTypeaheadSelectItemEvent) { - this.onChange.emit(this.org.get(selEvent.item.id)); - } - - // Formats the selected value - formatter = (result: OrgDisplay) => result.label.trim(); - - filter = (text$: Observable): Observable => { - return text$ - .debounceTime(100) - .distinctUntilChanged() - .map(term => { - - return this.org.list().filter(org => { - - // Find orgs matching the search term - return org[this.displayField]() - .toLowerCase().indexOf(term.toLowerCase()) > -1 - - }).filter(org => { // Exclude hidden orgs - return this.hidden.filter( - id => {return org.id() == id}).length == 0; - - }).map(org => {return this.formatForDisplay(org)}) - }); - } -} - - diff --git a/Open-ILS/webby-src/src/app/share/unapi.ts b/Open-ILS/webby-src/src/app/share/unapi.ts deleted file mode 100644 index 28c25896c0..0000000000 --- a/Open-ILS/webby-src/src/app/share/unapi.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {Injectable, EventEmitter} from '@angular/core'; -import {EgOrgService} from '@eg/core/org'; - -/* -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) {} - - createUrl(params: EgUnapiParams): string { - let depth = params.depth || 0; - let org = params.orgId ? this.org.get(params.orgId) : this.org.root(); - - return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` + - `${org.shortname()}/${depth}&format=${params.format}`; - } - - getAsXmlDocument(params: EgUnapiParams): Promise { - // 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) => { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - resolve(xhttp.responseXML); - } else { - reject(`UNAPI request failed for ${url}`); - } - } - } - xhttp.open("GET", url, true); - xhttp.send(); - }); - } -} - - diff --git a/Open-ILS/webby-src/src/app/share/util/pager.ts b/Open-ILS/webby-src/src/app/share/util/pager.ts deleted file mode 100644 index 1c21a8dcaf..0000000000 --- a/Open-ILS/webby-src/src/app/share/util/pager.ts +++ /dev/null @@ -1,47 +0,0 @@ - -/** - * Utility class for manage paged information. - */ -export class Pager { - offset: number = 0; - limit: number = null; - 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 - } - - increment(): void { - this.setPage(this.currentPage() + 1); - } - - decrement(): void { - this.setPage(this.currentPage() - 1); - } - - setPage(page: number): void { - this.offset = (this.limit * (page - 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; - } -} diff --git a/Open-ILS/webby-src/src/app/staff/admin/routing.module.ts b/Open-ILS/webby-src/src/app/staff/admin/routing.module.ts deleted file mode 100644 index 4e4ef09152..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/routing.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = [{ - path: '', - children : [{ - path: 'workstation', - loadChildren: '@eg/staff/admin/workstation/routing.module#EgAdminWsRoutingModule' - }] -}]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) - -export class EgAdminRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/staff/admin/workstation/routing.module.ts b/Open-ILS/webby-src/src/app/staff/admin/workstation/routing.module.ts deleted file mode 100644 index 114c312f3d..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/workstation/routing.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = [{ - path: 'workstations', - loadChildren: '@eg/staff/admin/workstation/workstations/app.module#ManageWorkstationsModule' -}]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) - -export class EgAdminWsRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.html b/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.html deleted file mode 100644 index 5b95268e34..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.html +++ /dev/null @@ -1,75 +0,0 @@ -
-
-
- Workstation {{removingWs}} is no longer valid. Removing registration. -
-
- Please register a workstation. -
- -
-
Register a New Workstation For This Browser
-
-
-
- - -
-
-
- -
- -
-
-
-
-
-
- Workstations Registered With This Browser -
-
-
-
- -
-
-
-
- - - -
-
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.ts b/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.ts deleted file mode 100644 index b724dc0f2e..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -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'; -import {EgAuthService} from '@eg/core/auth'; -import {EgOrgService} from '@eg/core/org'; - -// Slim version of the WS that's stored in the cache. -interface Workstation { - id: number; - name: string; - owning_lib: number; -} - -@Component({ - templateUrl: 'app.component.html' -}) -export class WorkstationsComponent implements OnInit { - - selectedId: Number; - workstations: Workstation[] = []; - removeWorkstation: string; - newOwner: EgIdlObject; - newName: String; - - // Org selector options. - hideOrgs: number[]; - disableOrgs: number[]; - orgOnChange = (org: EgIdlObject): void => { - this.newOwner = org; - } - - constructor( - private route: ActivatedRoute, - private net: EgNetService, - private store: EgStoreService, - private auth: EgAuthService, - private org: EgOrgService - ) {} - - ngOnInit() { - this.store.getItem('eg.workstation.all') - .then(res => this.workstations = res); - - // TODO: perm limits required here too - this.disableOrgs = this.org.filterList({canHaveUsers : true}, true); - - this.removeWorkstation = this.route.snapshot.paramMap.get('remove'); - if (this.removeWorkstation) { - console.log('Removing workstation ' + this.removeWorkstation); - // TODO remove - } - } - - selected(): Workstation { - return this.workstations.filter( - ws => {return ws.id == this.selectedId})[0]; - } - - useNow(): void { - console.debug('using ' + this.selected().name); - } - - setDefault(): void { - console.debug('defaulting ' + this.selected().name); - } - - removeSelected(): void { - console.debug('removing ' + this.selected().name); - } - - canDeleteSelected(): boolean { - return true; - } - - registerWorkstation(): void { - console.log(`Registering new workstation ` + - `"${this.newName}" at ${this.newOwner.shortname()}`); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.module.ts b/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.module.ts deleted file mode 100644 index c7051fb759..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {EgStaffModule} from '@eg/staff/app.module'; -import {WorkstationsRoutingModule} from './routing.module'; -import {WorkstationsComponent} from './app.component'; - -@NgModule({ - declarations: [ - WorkstationsComponent - ], - imports: [ - CommonModule, - EgStaffModule, - WorkstationsRoutingModule - ] -}) - -export class ManageWorkstationsModule { - constructor() {console.log('Loading ManageWorkstationsModule')} -} - diff --git a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/routing.module.ts b/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/routing.module.ts deleted file mode 100644 index f1ac37ed0c..0000000000 --- a/Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/routing.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; -import {WorkstationsComponent} from './app.component'; - -// Note that we need a path value (e.g. 'manage') because without it -// there is nothing for the router to match, unless we rely on the parent -// module to handle all of our routing for us. -const routes: Routes = [ - { - path: 'manage', - component: WorkstationsComponent - }, { - path: 'remove/:remove', - component: WorkstationsComponent - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) - -export class WorkstationsRoutingModule { -} - diff --git a/Open-ILS/webby-src/src/app/staff/app.component.css b/Open-ILS/webby-src/src/app/staff/app.component.css deleted file mode 100644 index 508d879b9b..0000000000 --- a/Open-ILS/webby-src/src/app/staff/app.component.css +++ /dev/null @@ -1,8 +0,0 @@ -#staff-content-container { - width: 95%; - margin-top:56px; - padding-right: 10px; - padding-left: 10px; - margin-right: auto; - margin-left: auto; -} diff --git a/Open-ILS/webby-src/src/app/staff/app.component.html b/Open-ILS/webby-src/src/app/staff/app.component.html deleted file mode 100644 index 7bd463a623..0000000000 --- a/Open-ILS/webby-src/src/app/staff/app.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - -
- - -
- diff --git a/Open-ILS/webby-src/src/app/staff/app.component.ts b/Open-ILS/webby-src/src/app/staff/app.component.ts deleted file mode 100644 index 3c90ab0a91..0000000000 --- a/Open-ILS/webby-src/src/app/staff/app.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -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'; - -@Component({ - templateUrl: 'app.component.html', - styleUrls: ['app.component.css'] -}) - -export class EgStaffComponent implements OnInit { - - readonly loginPath = '/staff/login'; - readonly wsAdminPath = '/staff/admin/workstation/workstations/manage'; - - constructor( - private router: Router, - private route: ActivatedRoute, - private net: EgNetService, - private auth: EgAuthService - ) {} - - ngOnInit() { - - console.debug('EgStaffComponent:ngOnInit()'); - - // Fires on all in-app router navigation, but not initial page load. - this.router.events.subscribe(routeEvent => { - if (routeEvent instanceof NavigationEnd) { - //console.debug(`EgStaffComponent routing to ${routeEvent.url}`); - this.basicAuthChecks(routeEvent); - } - }); - - // Redirect to the login page on any auth timeout events. - this.net.authExpired$.subscribe(uhOh => { - console.debug('Auth session has expired. Redirecting to login'); - this.auth.redirectUrl = this.router.url; - this.router.navigate([this.loginPath]); - }); - - this.route.data.subscribe((data: {staffResolver : any}) => { - console.debug('EgStaff ngOnInit complete'); - - }); - } - - /** - * Verifying auth token on every route is overkill, since an expired - * token will make itself known with the first API call, but we do - * want to prevent navigation from the login or workstation admin - * page, since these can be accessed without a valid authtoken or - * workstation, respectively, once the initial route resolvers - * have done their jobs. - */ - basicAuthChecks(routeEvent: NavigationEnd): void { - - // Access to login page is always granted - if (routeEvent.url == this.loginPath) return; - - if (!this.auth.token()) - this.router.navigate([this.loginPath]); - - // Access to workstation admin page is granted regardless - // of workstation validity. - if (routeEvent.url == this.wsAdminPath) return; - - if (this.auth.workstationState != EgAuthWsState.VALID) - this.router.navigate([this.wsAdminPath]); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/app.module.ts b/Open-ILS/webby-src/src/app/staff/app.module.ts deleted file mode 100644 index 7b53d7fc41..0000000000 --- a/Open-ILS/webby-src/src/app/staff/app.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {CommonModule} from '@angular/common'; -import {NgModule} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {EgBaseModule} from '@eg/app.module'; - -import {EgStaffComponent} from './app.component'; -import {EgStaffRoutingModule} from './routing.module'; -import {EgStaffNavComponent} from './nav.component'; -import {EgStaffLoginComponent} from './login.component'; -import {EgStaffSplashComponent} from './splash.component'; -import {EgOrgSelectComponent} from '@eg/share/org-select.component'; - -@NgModule({ - declarations: [ - EgStaffComponent, - EgStaffNavComponent, - EgStaffSplashComponent, - EgStaffLoginComponent, - EgOrgSelectComponent - ], - imports: [ - EgStaffRoutingModule, - FormsModule, - NgbModule - ], - exports: [ - // Components available to all staff/sub modules - EgOrgSelectComponent, - FormsModule, - NgbModule - ] -}) - -export class EgStaffModule { - -} diff --git a/Open-ILS/webby-src/src/app/staff/catalog/app.component.html b/Open-ILS/webby-src/src/app/staff/catalog/app.component.html deleted file mode 100644 index 1596454ac1..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/app.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/app.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/app.component.ts deleted file mode 100644 index a5ca68f3db..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/app.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Component, OnInit} from '@angular/core'; -import {StaffCatalogService} from './app.service'; - -@Component({ - templateUrl: 'app.component.html' -}) -export class EgCatalogComponent implements OnInit { - - constructor(private staffCat: StaffCatalogService) {} - - ngOnInit() { - // Create the search context that will be used by all - // of my child components. - this.staffCat.createContext(); - } -} - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/app.module.ts b/Open-ILS/webby-src/src/app/staff/catalog/app.module.ts deleted file mode 100644 index b76cc0bcef..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/app.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {CommonModule} from '@angular/common'; -import {NgModule} from '@angular/core'; -import {EgStaffModule} from '../app.module'; -import {EgUnapiService} from '@eg/share/unapi'; -import {EgCatalogRoutingModule} from './routing.module'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; -import {EgCatalogComponent} from './app.component'; -import {SearchFormComponent} from './search-form.component'; -import {ResultsComponent} from './result/results.component'; -import {RecordComponent} from './record/record.component'; -import {CopiesComponent} from './record/copies.component'; -import {EgBibSummaryComponent} from '../share/bib-summary.component'; -import {ResultPaginationComponent} from './result/pagination.component'; -import {ResultFacetsComponent} from './result/facets.component'; -import {ResultRecordComponent} from './result/record.component'; -import {StaffCatalogService} from './app.service'; -import {RecordPaginationComponent} from './record/pagination.component'; - -@NgModule({ - declarations: [ - EgCatalogComponent, - ResultsComponent, - RecordComponent, - CopiesComponent, - EgBibSummaryComponent, - SearchFormComponent, - ResultRecordComponent, - ResultFacetsComponent, - ResultPaginationComponent, - RecordPaginationComponent - ], - imports: [ - EgStaffModule, - CommonModule, - EgCatalogRoutingModule - ], - providers: [ - EgUnapiService, - EgCatalogService, - EgCatalogUrlService, - StaffCatalogService - ] -}) - -export class EgCatalogModule { - -} diff --git a/Open-ILS/webby-src/src/app/staff/catalog/app.service.ts b/Open-ILS/webby-src/src/app/staff/catalog/app.service.ts deleted file mode 100644 index 625206e7f4..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/app.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Router, ActivatedRoute} from '@angular/router'; -import {EgOrgService} from '@eg/core/org'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; -import {CatalogSearchContext} from '@eg/share/catalog/search-context'; - -/** - * Shared bits needed by the staff version of the catalog. - */ - -@Injectable() -export class StaffCatalogService { - - searchContext: CatalogSearchContext; - routeIndex: number = 0; - - constructor( - private router: Router, - private route: ActivatedRoute, - private org: EgOrgService, - private cat: EgCatalogService, - private catUrl: EgCatalogUrlService - ) { } - - createContext(): void { - // Initialize the search context from the load-time URL params. - // Do this here so the search form and other context data are - // applied on every page, not just the search results page. The - // search results pages will handle running the actual search. - this.searchContext = - this.catUrl.fromUrlParams(this.route.snapshot.queryParamMap); - - this.searchContext.org = this.org; - this.searchContext.isStaff = true; - - // TODO: UI / settings - if (!this.searchContext.pager.limit) - this.searchContext.pager.limit = 20; - } - - /** - * Redirect to the search results page while propagating the current - * search paramters into the URL. Let the search results component - * execute the actual search. - */ - search(): void { - let params = this.catUrl.toUrlParams(this.searchContext); - - // Avoid redirect on empty-query searches - if (params.query[0] == '') return; - - // Force a new search every time this method is called, even if - // it's the same as the active search. Since router navigation - // exits early when the route + params is identical, add a - // random token to the route params to force a full navigation. - // This also resolves a problem where only removing secondary+ - // versions of a query param fail to cause a route navigation. - // (E.g. going from two query= params to one). Investigation - // pending. - params.ridx=''+this.routeIndex++; - - this.router.navigate( - ['/staff/catalog/search'], {queryParams: params}); - } - -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.html b/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.html deleted file mode 100644 index 84e9d8e8a5..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.html +++ /dev/null @@ -1,71 +0,0 @@ -
- -
-
-
Location
-
Call Number / Copy Notes
-
Barcode
-
Shelving Location
-
Circulation Modifier
-
Age Hold Protection
-
Active/Create Date
-
Holdable?
-
Status
-
Due Date
-
-
-
    -
  • -
    -
    {{orgName(copy.circ_lib)}}
    -
    - {{copy.call_number_prefix_label}} - {{copy.call_number_label}} - {{copy.call_number_suffix_label}} -
    -
    - {{copy.barcode}} - View - | - Edit -
    -
    {{copy.copy_location}}
    -
    {{copy.circ_modifier || ''}}
    -
    {{copy.age_protect}}
    -
    - {{copy.active_date || copy.create_date | date:'shortDate'}} -
    -
    - Yes - No -
    -
    {{copy.copy_status}}
    -
    {{copy.due_date | date:'shortDate'}}
    -
    -
  • -
-
-
-
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.ts deleted file mode 100644 index f234eba8df..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.ts +++ /dev/null @@ -1,84 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {EgNetService} from '@eg/core/net'; -import {StaffCatalogService} from '../app.service'; -import {Pager} from '@eg/share/util/pager'; -import {EgOrgService} from '@eg/core/org'; - -@Component({ - selector: 'eg-catalog-copies', - templateUrl: 'copies.component.html' -}) -export class CopiesComponent implements OnInit { - - pager: Pager; - copies: any[] - recId: number; - initDone: boolean = false; - - @Input() set recordId(id: number) { - this.recId = id; - // Only force new data collection when recordId() - // is invoked after ngInit() has already run. - if (this.initDone) this.collectData(); - } - - constructor( - private net: EgNetService, - private org: EgOrgService, - private staffCat: StaffCatalogService, - ) {} - - ngOnInit() { - this.initDone = true; - this.collectData(); - } - - collectData() { - if (!this.recId) return; - this.pager = new Pager(); - this.pager.limit = 10; // TODO UI - this.fetchCopies(); - } - - orgName(orgId: number): string { - return this.org.get(orgId).shortname(); - } - - fetchCopies(): void { - this.copies = []; - this.net.request( - 'open-ils.search', - 'open-ils.search.bib.copies.staff', - this.recId, - this.staffCat.searchContext.searchOrg.id(), - this.staffCat.searchContext.searchOrg.ou_type().depth(), // TODO - this.pager.limit, - this.pager.offset, - this.staffCat.searchContext.searchOrg.id() // TODO pref_ou - ).subscribe(copy => { - this.copies.push(copy); - }); - } - - holdable(copy: any): boolean { - return copy.holdable == 't' - && copy.location_holdable == 't' - && copy.status_holdable == 't'; - } - - firstPage(): void { - this.pager.offset = 0; - this.fetchCopies(); - } - prevPage(): void { - this.pager.decrement(); - this.fetchCopies(); - } - nextPage(): void { - this.pager.increment(); - this.fetchCopies(); - } - -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.html b/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.html deleted file mode 100644 index 0edcded4cc..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.ts deleted file mode 100644 index a7535f6514..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.ts +++ /dev/null @@ -1,157 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {Router} from '@angular/router'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {CatalogSearchContext} from '@eg/share/catalog/search-context'; -import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; -import {StaffCatalogService} from '../app.service'; -import {Pager} from '@eg/share/util/pager'; - - -@Component({ - selector: 'eg-catalog-record-pagination', - templateUrl: 'pagination.component.html' -}) -export class RecordPaginationComponent implements OnInit { - - id: number; - index: number; - initDone: boolean = false; - searchContext: CatalogSearchContext; - - @Input() set recordId(id: number) { - this.id = id; - // Only apply new record data after the initial load - if (this.initDone) this.setIndex(); - } - - constructor( - private router: Router, - private cat: EgCatalogService, - private catUrl: EgCatalogUrlService, - private staffCat: StaffCatalogService, - ) {} - - ngOnInit() { - this.initDone = true; - this.setIndex(); - } - - firstRecord(): void { - this.findRecordAtIndex(0).then(id => { - let params = this.catUrl.toUrlParams(this.searchContext); - this.router.navigate( - ['/staff/catalog/record/' + id], {queryParams: params}); - }); - } - - lastRecord(): void { - this.findRecordAtIndex( - this.searchContext.result.count - 1 - ).then(id => { - let params = this.catUrl.toUrlParams(this.searchContext); - this.router.navigate( - ['/staff/catalog/record/' + id], {queryParams: params}); - }); - } - - nextRecord(): void { - this.findRecordAtIndex(this.index + 1).then(id => { - let params = this.catUrl.toUrlParams(this.searchContext); - this.router.navigate( - ['/staff/catalog/record/' + id], {queryParams: params}); - }); - } - - prevRecord(): void { - this.findRecordAtIndex(this.index - 1).then(id => { - let params = this.catUrl.toUrlParams(this.searchContext); - this.router.navigate( - ['/staff/catalog/record/' + id], {queryParams: params}); - }); - } - - - // Returns the offset of the record within the search results as a whole. - searchIndex(idx: number): number { - return idx + this.searchContext.pager.offset; - } - - // Find the position of the current record in the search results - // If no results are present or the record is not found, expand - // the search scope to find the record. - setIndex(): Promise { - this.searchContext = this.staffCat.searchContext; - this.index = null; - - return new Promise((resolve, reject) => { - - this.index = this.searchContext.indexForResult(this.id); - if (this.index !== null) return resolve(); - - return this.refreshSearch().then(ok => { - this.index = this.searchContext.indexForResult(this.id); - if (this.index === null) console.warn( - 'No search results found containing the focused record.'); - resolve(); - }); - }); - } - - // Find the record ID at the specified search index. - // If no data exists for the requested index, expand the search - // to include data for that index. - findRecordAtIndex(index: number): Promise { - - // First see if the selected record sits in the current page - // of search results. - return new Promise((resolve, reject) => { - let id = this.searchContext.resultIdAt(index); - if (id) return resolve(id); - - console.debug( - 'Record paginator unable to find record at index ' + index); - - // If we have to re-run the search to find the record, - // expand the search limit out just enough to find the - // requested record plus one more. - return this.refreshSearch(index + 2).then( - ok => { - let id = this.searchContext.resultIdAt(index); - if (id) { - resolve(id); - } else { - reject('no record found'); - } - } - ); - }); - } - - refreshSearch(limit?: number): Promise { - - console.debug('paginator refreshing search'); - - if (!this.searchContext.isSearchable()) - return Promise.resolve(); - - let origPager = this.searchContext.pager; - let tmpPager = new Pager(); - tmpPager.limit = limit || 1000; - - this.searchContext.pager = tmpPager; - - return this.cat.search(this.searchContext) - .then( - ok => { this.searchContext.pager = origPager; }, - notOk => { this.searchContext.pager = origPager } - ); - } - - returnToSearch(): void { - // Fire the main search. This will direct us back to /results/ - this.staffCat.search(); - } - -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.html b/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.html deleted file mode 100644 index 127254aa5d..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.html +++ /dev/null @@ -1,18 +0,0 @@ - -
-
-
- - -
-
-
- - -
-
- -
-
- - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.ts deleted file mode 100644 index 78552eb443..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/record/record.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {ActivatedRoute, ParamMap} from '@angular/router'; -import {EgPcrudService} from '@eg/core/pcrud'; -import {EgIdlObject} from '@eg/core/idl'; -import {CatalogSearchContext, CatalogSearchState} - from '@eg/share/catalog/search-context'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {StaffCatalogService} from '../app.service'; -import {EgBibSummaryComponent} from '../../share/bib-summary.component'; - -@Component({ - selector: 'eg-catalog-record', - templateUrl: 'record.component.html' -}) -export class RecordComponent implements OnInit { - - recordId: number; - bibSummary: any; - searchContext: CatalogSearchContext; - - constructor( - private route: ActivatedRoute, - private pcrud: EgPcrudService, - private cat: EgCatalogService, - private staffCat: StaffCatalogService - ) {} - - ngOnInit() { - this.searchContext = this.staffCat.searchContext; - - // Watch for URL record ID changes - this.route.paramMap.subscribe((params: ParamMap) => { - this.recordId = +params.get('id'); - this.loadRecord(); - }) - } - - loadRecord(): void { - this.searchContext = this.staffCat.searchContext; - - // If a search is encoded in the URL, be sure we have the - // relevant search - - this.cat.getBibSummary( - this.recordId, - this.searchContext.searchOrg.id(), - this.searchContext.searchOrg.ou_type().depth() - ).then(summary => { - this.bibSummary = summary; - this.pcrud.search('au', {id: [summary.creator, summary.editor]}) - .subscribe(user => { - if (user.id() == summary.creator) - summary.creator = user; - if (user.id() == summary.editor) - summary.editor = user; - }) - }); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/resolver.service.ts b/Open-ILS/webby-src/src/app/staff/catalog/resolver.service.ts deleted file mode 100644 index 8929d551ae..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/resolver.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Location} from '@angular/common'; -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'; -import {EgAuthService} from '@eg/core/auth'; -import {EgPcrudService} from '@eg/core/pcrud'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; - -@Injectable() -export class EgCatalogResolver implements Resolve> { - - constructor( - private router: Router, - private ngLocation: Location, - private store: EgStoreService, - private net: EgNetService, - private auth: EgAuthService, - private cat: EgCatalogService - ) {} - - resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Promise { - - console.debug('EgCatalogResolver:resolve()'); - - return Promise.all([ - this.cat.fetchCcvms(), - this.cat.fetchCmfs() - ]); - } -} - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.html b/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.html deleted file mode 100644 index 188ae3091e..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.html +++ /dev/null @@ -1,43 +0,0 @@ - -
-
-
-
-
-
-

- {{searchContext.result.facetData[facetConf.facetClass][name].cmfLabel}} -

- -
-
-
-
-
-
diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.ts deleted file mode 100644 index 8101ceda01..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {CatalogSearchContext, FacetFilter} from '@eg/share/catalog/search-context'; -import {StaffCatalogService} from '../app.service'; - -export const FACET_CONFIG = { - display: [ - {facetClass : 'author', facetOrder : ['personal', 'corporate']}, - {facetClass : 'subject', facetOrder : ['topic']}, - {facetClass : 'identifier', facetOrder : ['genre']}, - {facetClass : 'series', facetOrder : ['seriestitle']}, - {facetClass : 'subject', facetOrder : ['name', 'geographic']} - ], - displayCount : 5 -}; - -@Component({ - selector: 'eg-catalog-result-facets', - templateUrl: 'facets.component.html' -}) -export class ResultFacetsComponent implements OnInit { - - searchContext: CatalogSearchContext; - facetConfig: any; - - constructor( - private cat: EgCatalogService, - private staffCat: StaffCatalogService - ) { - this.facetConfig = FACET_CONFIG; - } - - ngOnInit() { - this.searchContext = this.staffCat.searchContext; - } - - facetIsApplied(cls: string, name: string, value: string): boolean { - return this.searchContext.hasFacet(new FacetFilter(cls, name, value)); - } - - applyFacet(cls: string, name: string, value: string): void { - this.searchContext.toggleFacet(new FacetFilter(cls, name, value)); - this.searchContext.pager.offset = 0; - this.staffCat.search(); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.css b/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.css deleted file mode 100644 index c283ff45d5..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.css +++ /dev/null @@ -1,8 +0,0 @@ - -/* Bootstrap default is 20px */ -.pagination {margin: 0px 0px 0px 0px} - -.pagination li:not(.active) a { - cursor: pointer; -} - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.html b/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.html deleted file mode 100644 index 55b63dd0d9..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.html +++ /dev/null @@ -1,28 +0,0 @@ - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.ts deleted file mode 100644 index 8dbb4d84c9..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {CatalogSearchContext} from '@eg/share/catalog/search-context'; -import {StaffCatalogService} from '../app.service'; - -@Component({ - selector: 'eg-catalog-result-pagination', - styleUrls: ['pagination.component.css'], - templateUrl: 'pagination.component.html' -}) -export class ResultPaginationComponent implements OnInit { - - searchContext: CatalogSearchContext; - - constructor( - private cat: EgCatalogService, - private staffCat: StaffCatalogService - ) {} - - ngOnInit() { - this.searchContext = this.staffCat.searchContext; - } - - nextPage(): void { - this.searchContext.pager.increment(); - this.staffCat.search(); - } - - prevPage(): void { - this.searchContext.pager.decrement(); - this.staffCat.search(); - } - - setPage(page: number): void { - if (this.searchContext.pager.currentPage() == page) return; - this.searchContext.pager.setPage(page); - this.staffCat.search(); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.html b/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.html deleted file mode 100644 index c9a0cd9b45..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.html +++ /dev/null @@ -1,129 +0,0 @@ - - -
-
-
-
- - - - -
-
-
-
- - - #{{index + 1 + searchContext.pager.offset}} - - - {{bibSummary.title || ' '}} - -
-
- -
-
- - - {{bibSummary.ccvms.icon_format.label}} - - {{bibSummary.edition}} - {{bibSummary.pubdate}} -
-
-
-
-
-
-
- - {{copyCount.available}} / {{copyCount.count}} items - -
-
- @ {{orgName(copyCount.org_unit)}} -
-
-
-
-
-
-
- TCN: {{bibSummary.tcn_value}} -
-
-
-
- Holds: {{bibSummary.holdCount}} -
-
-
-
-
-
-
- Created {{bibSummary.create_date | date:'shortDate'}} by - - - {{bibSummary.creator.usrname()}} - - - ... -
-
-
-
-
-
- Edited {{bibSummary.edit_date | date:'shortDate'}} by - - {{bibSummary.editor.usrname()}} - - ... -
-
-
-
-
-
- - - - - - -
-
-
-
-
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.ts deleted file mode 100644 index beee4cfbb0..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/record.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {Router} from '@angular/router'; -import {EgOrgService} from '@eg/core/org'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; -import {CatalogSearchContext} from '@eg/share/catalog/search-context'; -import {EgNetService} from '@eg/core/net'; -import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service'; -import {StaffCatalogService} from '../app.service'; - -@Component({ - selector: 'eg-catalog-result-record', - templateUrl: 'record.component.html' -}) -export class ResultRecordComponent implements OnInit { - - @Input() index: number; // 0-index display row - @Input() bibSummary: any; - searchContext: CatalogSearchContext; - - constructor( - private router: Router, - private org: EgOrgService, - private net: EgNetService, - private cat: EgCatalogService, - private catUrl: EgCatalogUrlService, - private staffCat: StaffCatalogService - ) {} - - ngOnInit() { - this.searchContext = this.staffCat.searchContext; - this.fleshHoldCount(); - } - - fleshHoldCount(): void { - this.net.request( - 'open-ils.circ', - 'open-ils.circ.bre.holds.count', this.bibSummary.id - ).subscribe(count => this.bibSummary.holdCount = count); - } - - orgName(orgId: number): string { - return this.org.get(orgId).shortname(); - } - - placeHold(): void { - alert('Placing hold on bib ' + this.bibSummary.id); - } - - addToList(): void { - alert('Adding to list for bib ' + this.bibSummary.id); - } - - searchAuthor(bibSummary: any) { - this.searchContext.reset(); - this.searchContext.fieldClass = ['author']; - this.searchContext.query = [bibSummary.author]; - this.staffCat.search(); - } - - /** - * Propagate the search params along when navigating to each record. - */ - navigatToRecord(id: number) { - let params = this.catUrl.toUrlParams(this.searchContext); - - this.router.navigate( - ['/staff/catalog/record/' + id], {queryParams: params}); - } - -} - - diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.html b/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.html deleted file mode 100644 index be7c36aec8..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.html +++ /dev/null @@ -1,30 +0,0 @@ - -
-
-
-

Search Results ({{searchContext.result.count}})

-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- - -
-
-
-
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.ts deleted file mode 100644 index b87a2cdd7d..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/result/results.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {Observable} from 'rxjs/Rx'; -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} - from '@eg/share/catalog/search-context'; -import {EgPcrudService} from '@eg/core/pcrud'; -import {StaffCatalogService} from '../app.service'; -import {EgIdlObject} from '@eg/core/idl'; - -@Component({ - selector: 'eg-catalog-results', - templateUrl: 'results.component.html' -}) -export class ResultsComponent implements OnInit { - - searchContext: CatalogSearchContext; - - // Cache record creator/editor since this will likely be a - // reasonably small set of data w/ lots of repitition. - userCache: {[id:number] : EgIdlObject} = {}; - - constructor( - private route: ActivatedRoute, - private pcrud: EgPcrudService, - private cat: EgCatalogService, - private catUrl: EgCatalogUrlService, - private staffCat: StaffCatalogService - ) {} - - ngOnInit() { - this.searchContext = this.staffCat.searchContext; - - // Our search context is initialized on page load. Once - // ResultsComponent is active, it will not be reinitialized, - // even if the route parameters changes (unless we change the - // route reuse policy). Watch for changes here to pick up new - // searches. This will also fire on page load. - this.route.queryParamMap.subscribe((params: ParamMap) => { - - // TODO: Angular docs suggest using switchMap(), but - // it's not firing for some reason. Also, could avoid - // firing unnecessary searches when a param unrelated to - // searching is changed by .map()'ing out only the desired - // params and running through .distinctUntilChanged(), but - // .map() is not firing either. I'm missing something. - this.searchByUrl(params); - }) - } - - searchByUrl(params: ParamMap): void { - this.catUrl.applyUrlParams(this.searchContext, params); - - // A query string is required at minimum. - if (!this.searchContext.isSearchable()) return; - - this.cat.search(this.searchContext) - .then(ok => { - this.cat.fetchFacets(this.searchContext); - this.cat.fetchBibSummaries(this.searchContext) - .then(ok2 => this.fleshSearchResults()); - }); - } - - fleshSearchResults(): void { - let records = this.searchContext.result.records; - if (records.length == 0) return; - - // Flesh the creator / editor fields with the user object. - // Handle the user fleshing here (instead of record.component so - // we only need to grab one copy of each user. - let userIds: {[id:number]: boolean} = {}; - records.forEach(recSum => { - if (this.userCache[recSum.creator]) { - recSum.creator = this.userCache[recSum.creator]; - } else { - userIds[Number(recSum.creator)] = true; - } - - if (this.userCache[recSum.editor]) { - recSum.editor = this.userCache[recSum.editor]; - } else { - userIds[Number(recSum.editor)] = true; - } - }); - - if (!Object.keys(userIds).length) return; - - this.pcrud.search('au', {id : Object.keys(userIds)}) - .subscribe(usr => { - this.userCache[usr.id()] = usr; - records.forEach(recSum => { - if (recSum.creator == usr.id()) recSum.creator = usr; - if (recSum.editor == usr.id()) recSum.editor = usr; - }); - }); - } - - searchIsDone(): boolean { - return this.searchContext.searchState == CatalogSearchState.COMPLETE; - } - -} - - 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 deleted file mode 100644 index 467db52e2c..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; -import {EgCatalogComponent} from './app.component'; -import {ResultsComponent} from './result/results.component'; -import {RecordComponent} from './record/record.component'; -import {EgCatalogResolver} from './resolver.service'; - -const routes: Routes = [{ - path: '', - component: EgCatalogComponent, - resolve: {catResolver : EgCatalogResolver}, - children : [{ - path: 'search', - component: ResultsComponent, - }, { - path: 'record/:id', - component: RecordComponent, - }] -}]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], - providers: [EgCatalogResolver ] -}) - -export class EgCatalogRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.css b/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.css deleted file mode 100644 index f67d8fa0c7..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.css +++ /dev/null @@ -1,9 +0,0 @@ - -/* filter checkbox labels move to bottom */ -.checkbox label { - margin-bottom: .1rem; -} - -#staffcat-search-form { - border-bottom: 2px dashed rgba(0,0,0,.225); -} diff --git a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.html b/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.html deleted file mode 100644 index 3ee4d2110b..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.html +++ /dev/null @@ -1,219 +0,0 @@ - -
-
-
-
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
-
- - -
-
-
-
- - - - -
-
-
- -
-
-
- - -
-
- -
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
-
-
- Searching.. -
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- Copy location filter goes here... -
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.ts b/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.ts deleted file mode 100644 index 94ef0bf03f..0000000000 --- a/Open-ILS/webby-src/src/app/staff/catalog/search-form.component.ts +++ /dev/null @@ -1,97 +0,0 @@ -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, CatalogSearchState} - from '@eg/share/catalog/search-context'; -import {StaffCatalogService} from './app.service'; - -@Component({ - selector: 'eg-catalog-search-form', - styleUrls: ['search-form.component.css'], - templateUrl: 'search-form.component.html' -}) -export class SearchFormComponent implements OnInit { - - searchContext: CatalogSearchContext; - ccvmMap: {[ccvm:string] : EgIdlObject[]} = {}; - cmfMap: {[cmf:string] : EgIdlObject} = {}; - showAdvancedSearch: boolean = false; - - constructor( - private org: EgOrgService, - private cat: EgCatalogService, - private staffCat: StaffCatalogService - ) {} - - ngOnInit() { - this.ccvmMap = this.cat.ccvmMap; - this.cmfMap = this.cat.cmfMap; - this.searchContext = this.staffCat.searchContext; - - // Start with advanced search options open - // if any filters are active. - this.showAdvancedSearch = this.hasAdvancedOptions(); - } - - /** - * Display the advanced/extended search options when asked to - * or if any advanced options are selected. - */ - showAdvanced(): boolean { - return this.showAdvancedSearch; - } - - hasAdvancedOptions(): boolean { - // ccvm filters may be present without any filters applied. - // e.g. if filters were applied then removed. - let show = false; - Object.keys(this.searchContext.ccvmFilters).forEach(ccvm => { - if (this.searchContext.ccvmFilters[ccvm][0] != '') - show = true; - }); - - return show; - } - - orgOnChange = (org: EgIdlObject): void => { - this.searchContext.searchOrg = org; - } - - addSearchRow(index: number): void { - this.searchContext.query.splice(index, 0, ''); - this.searchContext.fieldClass.splice(index, 0, 'keyword'); - this.searchContext.joinOp.splice(index, 0, '&&'); - this.searchContext.matchOp.splice(index, 0, 'contains'); - } - - delSearchRow(index: number): void { - this.searchContext.query.splice(index, 1); - this.searchContext.fieldClass.splice(index, 1); - this.searchContext.joinOp.splice(index, 1); - this.searchContext.matchOp.splice(index, 1); - } - - checkEnter($event: any): void { - if ($event.keyCode == 13) { - this.searchContext.pager.offset = 0; - this.searchByForm(); - } - } - - // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes - trackByIdx(index: any, item: any) { - return index; - } - - searchByForm(): void { - this.staffCat.search(); - } - - searchIsActive(): boolean { - return this.searchContext.searchState == CatalogSearchState.SEARCHING; - } - -} - - diff --git a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.html b/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.html deleted file mode 100644 index 1f55cb1d8b..0000000000 --- a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.html +++ /dev/null @@ -1,8 +0,0 @@ -

Search for Patron by Barcode

- -Barcode: - -
-
    -
  • {{str}}
  • -
diff --git a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.ts b/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.ts deleted file mode 100644 index 43d36daf97..0000000000 --- a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { EgNetService } from '@eg/core/net'; -import { EgAuthService } from '@eg/core/auth'; - -@Component({ - templateUrl: 'app.component.html' -}) - -export class EgBcSearchComponent implements OnInit { - - barcode: String = ''; - strList: String[] = []; - - constructor( - private route: ActivatedRoute, - private net: EgNetService, - private auth: EgAuthService - ) {} - - ngOnInit() { - - this.barcode = this.route.snapshot.paramMap.get('barcode'); - - if (this.barcode) { - // Find the user and redirect to the - } - - this.route.data.subscribe((data: { startup : any }) => { - console.debug('EgBcSearch ngOnInit complete'); - }); - - this.net.request( - 'open-ils.actor', - 'opensrf.system.echo', - 'hello', 'goodbye', 'in the middle' - ).subscribe(res => this.strList.push(res)); - } - - findUser(): void { - // find user by this.barcode; - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.module.ts b/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.module.ts deleted file mode 100644 index f119697292..0000000000 --- a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { EgBcSearchComponent } from './app.component'; -import { EgBcSearchRoutingModule } from './routing.module'; - -@NgModule({ - declarations: [ - EgBcSearchComponent - ], - imports: [ - EgBcSearchRoutingModule, - CommonModule, - FormsModule - ], -}) - -export class EgBcSearchModule {} - diff --git a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/routing.module.ts b/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/routing.module.ts deleted file mode 100644 index 2a685f321e..0000000000 --- a/Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/routing.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { EgBcSearchComponent } from './app.component'; - -const routes: Routes = [ - { path: '', - component: EgBcSearchComponent - }, - { path: ':barcode', - component: EgBcSearchComponent - }, -]; - -@NgModule({ - imports: [ RouterModule.forChild(routes) ], - exports: [ RouterModule ] -}) - -export class EgBcSearchRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/staff/circ/routing.module.ts b/Open-ILS/webby-src/src/app/staff/circ/routing.module.ts deleted file mode 100644 index 1b0a0f0cbe..0000000000 --- a/Open-ILS/webby-src/src/app/staff/circ/routing.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; - -const routes: Routes = [{ - path: '', - children : [{ - path: 'patron', - children: [{ - path: 'bcsearch', - loadChildren: '@eg/staff/circ/patron/bcsearch/app.module#EgBcSearchModule' - }] - }] -}]; - -@NgModule({ - imports: [ RouterModule.forChild(routes) ], - exports: [ RouterModule ] -}) - -export class EgCircRoutingModule {} diff --git a/Open-ILS/webby-src/src/app/staff/login.component.html b/Open-ILS/webby-src/src/app/staff/login.component.html deleted file mode 100644 index 869fe879cf..0000000000 --- a/Open-ILS/webby-src/src/app/staff/login.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
- Sign In -
-
- -
- - -
- -
- - -
- - -
-
-
diff --git a/Open-ILS/webby-src/src/app/staff/login.component.ts b/Open-ILS/webby-src/src/app/staff/login.component.ts deleted file mode 100644 index 64ae6c5361..0000000000 --- a/Open-ILS/webby-src/src/app/staff/login.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Component, OnInit, Renderer } from '@angular/core'; -import { Location } from '@angular/common'; -import { Router } from '@angular/router'; -import { EgAuthService, EgAuthWsState } from '@eg/core/auth'; -import { EgStoreService } from '@eg/core/store'; // TODO: testing - -@Component({ - templateUrl : './login.component.html' -}) - -export class EgStaffLoginComponent implements OnInit { - - args = { - username : '', - password : '', - type : 'staff', - //workstation : '' - workstation : 'BR1-skiddoo' // testing - }; - - workstations = []; - - constructor( - private router: Router, - private ngLocation: Location, - private renderer: Renderer, - private auth: EgAuthService, - private store: EgStoreService - ) {} - - ngOnInit() { - - // clear out any stale auth data - this.auth.logout(); - - // Focus username - this.renderer.selectRootElement('#username').focus(); - - // load browser-local workstation data - - // TODO: insert for testing. - this.store.setItem( - 'eg.workstation.all', - [{name:'BR1-skiddoo',id:1,owning_lib:4}] - ); - } - - handleSubmit() { - - // post-login URL - let url: string = this.auth.redirectUrl || '/staff/splash'; - let workstation: string = this.args.workstation; - - this.auth.login(this.args).then( - ok => { - this.auth.redirectUrl = null; - - if (this.auth.workstationState == EgAuthWsState.NOT_FOUND_SERVER) { - // User attempted to login with a workstation that is - // unknown to the server. Redirect to the WS admin page. - this.router.navigate( - ['/staff/admin/workstation/workstations/remove/${workstation}']); - } else { - // Force reload of the app after a successful login. - // This allows the route resolver to re-run with a - // valid auth token and workstation. - window.location.href = - this.ngLocation.prepareExternalUrl(url); - } - }, - notOk => { - // indicate failure in the UI. - } - ); - } -} - - - diff --git a/Open-ILS/webby-src/src/app/staff/nav.component.css b/Open-ILS/webby-src/src/app/staff/nav.component.css deleted file mode 100644 index ee4f93e89b..0000000000 --- a/Open-ILS/webby-src/src/app/staff/nav.component.css +++ /dev/null @@ -1,72 +0,0 @@ -/* remove dropdown carret for icon-based entries */ -#staff-navbar .no-caret::after { - display:none; -} - -/* move the caret closer to the dropdown text */ -#staff-navbar .dropdown-toggle::after { - margin-left:0px; -} - -#staff-navbar { - background: -webkit-linear-gradient(#00593d, #007a54); - background-color: #007a54; - color: #fff; - font-size: 14px; -} - -#staff-navbar .navbar-nav { - padding: 3px; -} - -/* align top of dropdown w/ bottom of nav */ -#staff-navbar .dropdown-menu { - margin-top: 7px; -} -#staff-navbar .material-icons { - padding-right:3px; -} -#staff-navbar .dropdown-item { - font-size: 14px; - font-weight: 400; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - padding-left: 0.7rem; - padding-right: 0.7rem; - margin: -4px; -} - -#staff-navbar .dropdown-item .material-icons { - font-size: 18px; -} - -#staff-navbar .nav-link { - color: #fff; - padding-top:1px; - padding-bottom:1px; -} -#staff-navbar .nav-link:hover { - color: #ddd; - cursor: pointer; -} - -#staff-navbar .navbar-nav > .open > a, -#staff-navbar .navbar-nav > .open > a:focus, -#staff-navbar .navbar-nav > .open > a:hover { - background-color: #7a7a7a; -} -#staff-navbar .navbar-nav>.dropdown>a .caret { - border-top-color: #fff; - border-bottom-color: #fff; -} -#staff-navbar .navbar-nav>.dropdown>a:hover .caret { - border-top-color: #ddd; - border-bottom-color: #ddd; -} - -/* Align material-icons with sibling text; otherwise they float up */ -#staff-navbar .with-material-icon, #staff-navbar .dropdown-item { - display: inline-flex; - vertical-align: middle; - align-items: center; -} - diff --git a/Open-ILS/webby-src/src/app/staff/nav.component.html b/Open-ILS/webby-src/src/app/staff/nav.component.html deleted file mode 100644 index 859ec7f865..0000000000 --- a/Open-ILS/webby-src/src/app/staff/nav.component.html +++ /dev/null @@ -1,203 +0,0 @@ - - diff --git a/Open-ILS/webby-src/src/app/staff/nav.component.ts b/Open-ILS/webby-src/src/app/staff/nav.component.ts deleted file mode 100644 index 62fb605a63..0000000000 --- a/Open-ILS/webby-src/src/app/staff/nav.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Component, OnInit} from '@angular/core'; -import {ActivatedRoute, Router} from '@angular/router'; -import {EgAuthService} from '@eg/core/auth'; - -@Component({ - selector: 'eg-staff-nav-bar', - styleUrls: ['nav.component.css'], - templateUrl: 'nav.component.html' -}) - -export class EgStaffNavComponent implements OnInit { - - user: string; - workstation: string; - - constructor(private auth: EgAuthService) {} - - ngOnInit() { - this.user = this.auth.user().usrname(); - this.workstation = this.auth.workstation(); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/resolver.service.ts b/Open-ILS/webby-src/src/app/staff/resolver.service.ts deleted file mode 100644 index 8c23030377..0000000000 --- a/Open-ILS/webby-src/src/app/staff/resolver.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Location} from '@angular/common'; -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'; -import {EgAuthService} from '@eg/core/auth'; - -/** - * Apply configuration, etc. required by all staff components. - * This resolver is called before authentication is confirmed. - * See EgStaffCommonDataResolver for staff-wide, post-auth activities. - */ -@Injectable() -export class EgStaffResolver implements Resolve> { - - readonly loginPath = '/staff/login'; - readonly wsAdminPath = '/staff/admin/workstation/workstations/manage'; - - constructor( - private router: Router, - private ngLocation: Location, - private store: EgStoreService, - private net: EgNetService, - private auth: EgAuthService - ) {} - - resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable { - - console.debug('EgStaffResolver:resolve()'); - - // Staff cookies stay in /$base/staff/ - // NOTE: storing session data at '/' so it can be shared by - // Angularjs apps. - this.store.loginSessionBasePath = '/'; - //this.ngLocation.prepareExternalUrl('/staff'); - - // Login resets everything. No need to load data. - if (state.url == '/staff/login') return Observable.of(true); - - return Observable.create(observer => { - this.auth.testAuthToken().then( - tokenOk => { - console.debug('EgStaffResolver: authtoken verified'); - this.auth.verifyWorkstation().then( - wsOk => { - this.loadStartupData(observer).then( - ok => observer.complete() - ); - }, - wsNotOk => { - if (state.url != this.wsAdminPath) { - this.router.navigate([this.wsAdminPath]); - } - observer.complete(); - } - ); - }, - tokenNotOk => { - // Authtoken is not OK. - console.debug('EgStaffResolver: authtoken is not valid'); - this.auth.redirectUrl = state.url; - this.router.navigate([this.loginPath]); - observer.error('invalid auth'); - } - ); - }); - } - - loadStartupData(observer: Observer): Promise { - console.debug('EgStaffResolver:loadStartupData()'); - return Promise.resolve(); - } -} - diff --git a/Open-ILS/webby-src/src/app/staff/routing.module.ts b/Open-ILS/webby-src/src/app/staff/routing.module.ts deleted file mode 100644 index 81c0609565..0000000000 --- a/Open-ILS/webby-src/src/app/staff/routing.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; -import {EgStaffResolver} from './resolver.service'; -import {EgStaffComponent} from './app.component'; -import {EgStaffLoginComponent} from './login.component'; -import {EgStaffSplashComponent} from './splash.component'; - -// Not using 'canActivate' because it's called before all resolvers, -// but the resolvers parse the IDL, etc. - -const routes: Routes = [{ - path: '', - component: EgStaffComponent, - resolve: {staffResolver : EgStaffResolver}, - children: [{ - path: '', - redirectTo: 'splash', - pathMatch: 'full', - }, { - path: 'login', - component: EgStaffLoginComponent - }, { - path: 'splash', - component: EgStaffSplashComponent - }, { - path: 'circ', - loadChildren : '@eg/staff/circ/routing.module#EgCircRoutingModule' - }, { - path: 'catalog', - loadChildren : '@eg/staff/catalog/app.module#EgCatalogModule' - }, { - path: 'admin', - loadChildren : '@eg/staff/admin/routing.module#EgAdminRoutingModule' - }] -}]; - -@NgModule({ - imports: [ RouterModule.forChild(routes) ], - exports: [ RouterModule ], - providers: [ - EgStaffResolver - ] -}) - -export class EgStaffRoutingModule {} - diff --git a/Open-ILS/webby-src/src/app/staff/share/README b/Open-ILS/webby-src/src/app/staff/share/README deleted file mode 100644 index 1d6d167d9c..0000000000 --- a/Open-ILS/webby-src/src/app/staff/share/README +++ /dev/null @@ -1 +0,0 @@ -Classes, services, and components shared in the staff app. diff --git a/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.html b/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.html deleted file mode 100644 index 66266086d8..0000000000 --- a/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.html +++ /dev/null @@ -1,66 +0,0 @@ - -
-
-
- Record Summary -
-
- -
-
-
    -
  • -
    -
    Title:
    -
    {{summary.title}}
    -
    Edition:
    -
    {{summary.edition}}
    -
    TCN:
    -
    {{summary.tcn_value}}
    -
    Created By:
    -
    - {{summary.creator.usrname()}} -
    -
    -
  • -
  • -
    -
    Author:
    -
    {{summary.author}}
    -
    Pubdate:
    -
    {{summary.pubdate}}
    -
    Database ID:
    -
    {{summary.id}}
    -
    Last Edited By:
    -
    - {{summary.editor.usrname()}} -
    -
    -
  • -
  • -
    -
    Bib Call #:
    -
    {{summary.callNumber}}
    -
    Record Owner:
    -
    TODO
    -
    Created On:
    -
    {{summary.create_date | date:'shortDate'}}
    -
    Last Edited On:
    -
    {{summary.edit_date | date:'shortDate'}}
    -
    -
  • -
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.ts b/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.ts deleted file mode 100644 index 877b18a4f5..0000000000 --- a/Open-ILS/webby-src/src/app/staff/share/bib-summary.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {EgNetService} from '@eg/core/net'; -import {EgPcrudService} from '@eg/core/pcrud'; -import {EgCatalogService} from '@eg/share/catalog/catalog.service'; - -@Component({ - selector: 'eg-bib-summary', - templateUrl: 'bib-summary.component.html' -}) -export class EgBibSummaryComponent implements OnInit { - - initDone: boolean = false; - - // If provided, the record will be fetched by the component. - @Input() recordId: number; - - // Otherwise, we'll use the provided bib summary object. - summary: any; - @Input() set bibSummary(s: any) { - this.summary = s; - if (this.initDone) this.fetchBibCallNumber(); - } - - expandDisplay: boolean = true; - - constructor( - private cat: EgCatalogService, - private net: EgNetService, - private pcrud: EgPcrudService - ) {} - - ngOnInit() { - this.initDone = true; - if (this.summary) { - this.fetchBibCallNumber(); - } else { - if (this.recordId) this.loadSummary(); - } - } - - loadSummary(): void { - this.cat.getBibSummary(this.recordId).then(summary => { - this.summary = summary; - this.fetchBibCallNumber(); - - // Flesh the user data - this.pcrud.search('au', {id: [summary.creator, summary.editor]}) - .subscribe(user => { - if (user.id() == summary.creator) - summary.creator = user; - if (user.id() == summary.editor) - summary.editor = user; - }) - }); - } - - fetchBibCallNumber(): void { - if (!this.summary || this.summary.callNumber) return; - - // TODO labelClass = cat.default_classification_scheme YAOUS - let labelClass = 1; - - this.net.request( - 'open-ils.cat', - 'open-ils.cat.biblio.record.marc_cn.retrieve', - this.summary.id, labelClass - ).subscribe(cnArray => { - if (cnArray && cnArray.length > 0) { - let key1 = Object.keys(cnArray[0])[0]; - this.summary.callNumber = cnArray[0][key1]; - } - }); - } -} - - diff --git a/Open-ILS/webby-src/src/app/staff/splash.component.html b/Open-ILS/webby-src/src/app/staff/splash.component.html deleted file mode 100644 index 4846cc5e27..0000000000 --- a/Open-ILS/webby-src/src/app/staff/splash.component.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - -
- - -
-
- -
-
- -
-
-
-
-
Circulation and Patrons
-
- -
-
- -
-
-
-
Item Search and Cataloging
-
-
-
-
-
- - - - - -
-
- -
- - Copy Buckets -
-
-
-
-
- -
-
-
-
Administration
-
- -
-
-
-
- diff --git a/Open-ILS/webby-src/src/app/staff/splash.component.ts b/Open-ILS/webby-src/src/app/staff/splash.component.ts deleted file mode 100644 index e113437229..0000000000 --- a/Open-ILS/webby-src/src/app/staff/splash.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Component, OnInit, Renderer} from '@angular/core'; -import {Router} from '@angular/router'; - -@Component({ - templateUrl: 'splash.component.html' -}) - -export class EgStaffSplashComponent implements OnInit { - - catSearchQuery: string; - - constructor( - private renderer: Renderer, - private router: Router - ) {} - - ngOnInit() { - - // Focus catalog search form - this.renderer.selectRootElement('#catalog-search-input').focus(); - } - - checkEnter($event: any): void { - if ($event.keyCode == 13) - this.searchCatalog(); - } - - searchCatalog(): void { - if (!this.catSearchQuery) return; - - this.router.navigate( - ['/staff/catalog/search'], - {queryParams: {query : this.catSearchQuery}} - ); - } -} - - diff --git a/Open-ILS/webby-src/src/app/welcome.component.html b/Open-ILS/webby-src/src/app/welcome.component.html deleted file mode 100644 index 3ce97cc247..0000000000 --- a/Open-ILS/webby-src/src/app/welcome.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-

Welcome to Webby

-

- If you see this page, you're probably in good shape... -

-
-

- But maybe you meant to go to the staff page - or the catalog. -

-
diff --git a/Open-ILS/webby-src/src/app/welcome.component.ts b/Open-ILS/webby-src/src/app/welcome.component.ts deleted file mode 100644 index 398d12776b..0000000000 --- a/Open-ILS/webby-src/src/app/welcome.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - templateUrl : './welcome.component.html' -}) - -export class WelcomeComponent implements OnInit { - - ngOnInit() { - } -} - - - diff --git a/Open-ILS/webby-src/src/assets/.gitkeep b/Open-ILS/webby-src/src/assets/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Open-ILS/webby-src/src/environments/environment.prod.ts b/Open-ILS/webby-src/src/environments/environment.prod.ts deleted file mode 100644 index 3612073bc3..0000000000 --- a/Open-ILS/webby-src/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -}; diff --git a/Open-ILS/webby-src/src/environments/environment.ts b/Open-ILS/webby-src/src/environments/environment.ts deleted file mode 100644 index b7f639aeca..0000000000 --- a/Open-ILS/webby-src/src/environments/environment.ts +++ /dev/null @@ -1,8 +0,0 @@ -// The file contents for the current environment will overwrite these during build. -// The build system defaults to the dev environment which uses `environment.ts`, but if you do -// `ng build --env=prod` then `environment.prod.ts` will be used instead. -// The list of which env maps to which file can be found in `.angular-cli.json`. - -export const environment = { - production: false -}; diff --git a/Open-ILS/webby-src/src/favicon.ico b/Open-ILS/webby-src/src/favicon.ico deleted file mode 100644 index 8081c7ceaf..0000000000 Binary files a/Open-ILS/webby-src/src/favicon.ico and /dev/null differ diff --git a/Open-ILS/webby-src/src/index.html b/Open-ILS/webby-src/src/index.html deleted file mode 100644 index a876726798..0000000000 --- a/Open-ILS/webby-src/src/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - AngEG - - - - - - - - - - - - - - - - - - diff --git a/Open-ILS/webby-src/src/main.ts b/Open-ILS/webby-src/src/main.ts deleted file mode 100644 index 08b359c3b7..0000000000 --- a/Open-ILS/webby-src/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { EgBaseModule } from './app/app.module'; -import { environment } from './environments/environment'; - -if (environment.production) { - enableProdMode(); -} - -platformBrowserDynamic().bootstrapModule(EgBaseModule) - .catch(err => console.log(err)); diff --git a/Open-ILS/webby-src/src/polyfills.ts b/Open-ILS/webby-src/src/polyfills.ts deleted file mode 100644 index 20d40751a6..0000000000 --- a/Open-ILS/webby-src/src/polyfills.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE9, IE10 and IE11 requires all of the following polyfills. **/ -// import 'core-js/es6/symbol'; -// import 'core-js/es6/object'; -// import 'core-js/es6/function'; -// import 'core-js/es6/parse-int'; -// import 'core-js/es6/parse-float'; -// import 'core-js/es6/number'; -// import 'core-js/es6/math'; -// import 'core-js/es6/string'; -// import 'core-js/es6/date'; -// import 'core-js/es6/array'; -// import 'core-js/es6/regexp'; -// import 'core-js/es6/map'; -// import 'core-js/es6/weak-map'; -// import 'core-js/es6/set'; - -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** IE10 and IE11 requires the following for the Reflect API. */ -// import 'core-js/es6/reflect'; - - -/** Evergreen browsers require these. **/ -// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. -import 'core-js/es7/reflect'; - - -/** - * Required to support Web Animations `@angular/platform-browser/animations`. - * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation - **/ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - - - -/*************************************************************************************************** - * Zone JS is required by Angular itself. - */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ - -/** - * Date, currency, decimal and percent pipes. - * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 - */ -// import 'intl'; // Run `npm install --save intl`. -/** - * Need to import at least one locale-data with intl. - */ -// import 'intl/locale-data/jsonp/en'; diff --git a/Open-ILS/webby-src/src/styles.css b/Open-ILS/webby-src/src/styles.css deleted file mode 100644 index c580fb05cb..0000000000 --- a/Open-ILS/webby-src/src/styles.css +++ /dev/null @@ -1,67 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ - -/** material design experiments -@import "~@angular/material/prebuilt-themes/indigo-pink.css"; -*/ - - -/** BS default fonts are huge */ -body, .form-control, .btn { - /* This more or less matches the font size of the angularjs client. - * The default BS4 font of 1rem is comically large. - */ - font-size: .88rem; -} -h2 {font-size: 1.25rem} -h3 {font-size: 1.15rem} -h4 {font-size: 1.05rem} -h5 {font-size: .95rem} - -.small-text-1 {font-size: 85%} - - -/** Ang5 routes on clicks to href's with no values, so we can't have - * bare href's to force anchor styling. Use this for anchors w/ no href. - * TODO: should we style all of them? a:not([href]) .... - * */ -.no-href { - cursor: pointer; - color: #007bff; -} - - -/** BS has flex utility classes, but none for specifying flex widths */ -.flex-1 {flex: 1} -.flex-2 {flex: 2} -.flex-3 {flex: 3} -.flex-4 {flex: 4} -.flex-5 {flex: 5} - - -/* usefulf for mat-icon buttons without any background or borders */ -.material-icon-button { - /* Transparent background */ - border: none; - background-color: rgba(0, 0, 0, 0.0); - padding-left: .25rem; - padding-right: .25rem; /* default .5rem */ -} - -.material-icons { - /** default is 24px which is pretty chunky */ - font-size: 22px; -} - -/* allow spans/labels to vertically orient with material icons */ -.label-with-material-icon { - display: inline-flex; - vertical-align: middle; - align-items: center; -} - -/* Default .card padding is extreme */ -.tight-card .card-body, -.tight-card .list-group-item { - padding: .25rem; -} - diff --git a/Open-ILS/webby-src/src/test.ts b/Open-ILS/webby-src/src/test.ts deleted file mode 100644 index cd612eeb0e..0000000000 --- a/Open-ILS/webby-src/src/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js/dist/long-stack-trace-zone'; -import 'zone.js/dist/proxy.js'; -import 'zone.js/dist/sync-test'; -import 'zone.js/dist/jasmine-patch'; -import 'zone.js/dist/async-test'; -import 'zone.js/dist/fake-async-test'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. -declare const __karma__: any; -declare const require: any; - -// Prevent Karma from running prematurely. -__karma__.loaded = function () {}; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); -// Finally, start Karma to run the tests. -__karma__.start(); diff --git a/Open-ILS/webby-src/src/tsconfig.app.json b/Open-ILS/webby-src/src/tsconfig.app.json deleted file mode 100644 index 39ba8dbacb..0000000000 --- a/Open-ILS/webby-src/src/tsconfig.app.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "baseUrl": "./", - "module": "es2015", - "types": [] - }, - "exclude": [ - "test.ts", - "**/*.spec.ts" - ] -} diff --git a/Open-ILS/webby-src/src/tsconfig.spec.json b/Open-ILS/webby-src/src/tsconfig.spec.json deleted file mode 100644 index 63d89ff283..0000000000 --- a/Open-ILS/webby-src/src/tsconfig.spec.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/spec", - "baseUrl": "./", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} diff --git a/Open-ILS/webby-src/src/typings.d.ts b/Open-ILS/webby-src/src/typings.d.ts deleted file mode 100644 index ef5c7bd620..0000000000 --- a/Open-ILS/webby-src/src/typings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* SystemJS module definition */ -declare var module: NodeModule; -interface NodeModule { - id: string; -} diff --git a/Open-ILS/webby-src/tsconfig.json b/Open-ILS/webby-src/tsconfig.json deleted file mode 100644 index 14a504dc91..0000000000 --- a/Open-ILS/webby-src/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es5", - "baseUrl": "src", - "paths": { - "@eg/*": ["app/*"], - "@env/*": ["environments/*"] - }, - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ] - } -} diff --git a/Open-ILS/webby-src/tslint.json b/Open-ILS/webby-src/tslint.json deleted file mode 100644 index c24dc293d7..0000000000 --- a/Open-ILS/webby-src/tslint.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "rulesDirectory": [ - "node_modules/codelyzer" - ], - "rules": { - "arrow-return-shorthand": true, - "callable-types": true, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": true, - "eofline": true, - "forin": true, - "import-blacklist": [ - true, - "rxjs", - "rxjs/Rx" - ], - "import-spacing": true, - "indent": [ - true, - "spaces" - ], - "interface-over-type-literal": true, - "label-position": true, - "max-line-length": [ - true, - 140 - ], - "member-access": false, - "member-ordering": [ - true, - { - "order": [ - "static-field", - "instance-field", - "static-method", - "instance-method" - ] - } - ], - "no-arg": true, - "no-bitwise": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, - "no-empty": false, - "no-empty-interface": true, - "no-eval": true, - "no-inferrable-types": [ - true, - "ignore-params" - ], - "no-misused-new": true, - "no-non-null-assertion": true, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-string-throw": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unnecessary-initializer": true, - "no-unused-expression": true, - "no-use-before-declare": true, - "no-var-keyword": true, - "object-literal-sort-keys": false, - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-else", - "check-whitespace" - ], - "prefer-const": true, - "quotemark": [ - true, - "single" - ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "typeof-compare": true, - "unified-signatures": true, - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ], - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ], - "use-input-property-decorator": true, - "use-output-property-decorator": true, - "use-host-property-decorator": true, - "no-input-rename": true, - "no-output-rename": true, - "use-life-cycle-interface": true, - "use-pipe-transform-interface": true, - "component-class-suffix": true, - "directive-class-suffix": true, - "invoke-injectable": true - } -}