LP#626157 Ang2 experiments
authorBill Erickson <berickxx@gmail.com>
Wed, 13 Dec 2017 19:11:06 +0000 (14:11 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 13 Dec 2017 19:11:06 +0000 (14:11 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
194 files changed:
Open-ILS/eg2-src/.angular-cli.json [new file with mode: 0644]
Open-ILS/eg2-src/.editorconfig [new file with mode: 0644]
Open-ILS/eg2-src/.gitignore [new file with mode: 0644]
Open-ILS/eg2-src/README.adoc [new file with mode: 0644]
Open-ILS/eg2-src/e2e/app.e2e-spec.ts [new file with mode: 0644]
Open-ILS/eg2-src/e2e/app.po.ts [new file with mode: 0644]
Open-ILS/eg2-src/e2e/tsconfig.e2e.json [new file with mode: 0644]
Open-ILS/eg2-src/karma.conf.js [new file with mode: 0644]
Open-ILS/eg2-src/package.json [new file with mode: 0644]
Open-ILS/eg2-src/protractor.conf.js [new file with mode: 0644]
Open-ILS/eg2-src/src/app/app.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/app.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/README [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/auth.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/event.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/idl.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/net.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/org.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/pcrud.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/core/store.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/resolver.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/README [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/catalog/catalog-url.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/catalog/catalog.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/catalog/search-context.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/org-select.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/org-select.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/unapi.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/share/util/pager.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/workstation/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/app.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/admin/workstation/workstations/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/app.component.css [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/app.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/app.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/app.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/app.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/app.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/app.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/app.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/copies.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/pagination.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/record/record.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/resolver.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/facets.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.css [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/pagination.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/record.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/result/results.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.css [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/catalog/search-form.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/app.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/circ/patron/bcsearch/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/circ/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/login.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/login.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/nav.component.css [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/nav.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/nav.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/resolver.service.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/routing.module.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/share/README [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/share/bib-summary.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/splash.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/staff/splash.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/app/welcome.component.html [new file with mode: 0644]
Open-ILS/eg2-src/src/app/welcome.component.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/assets/.gitkeep [new file with mode: 0644]
Open-ILS/eg2-src/src/environments/environment.prod.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/environments/environment.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/favicon.ico [new file with mode: 0644]
Open-ILS/eg2-src/src/index.html [new file with mode: 0644]
Open-ILS/eg2-src/src/main.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/polyfills.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/styles.css [new file with mode: 0644]
Open-ILS/eg2-src/src/test.ts [new file with mode: 0644]
Open-ILS/eg2-src/src/tsconfig.app.json [new file with mode: 0644]
Open-ILS/eg2-src/src/tsconfig.spec.json [new file with mode: 0644]
Open-ILS/eg2-src/src/typings.d.ts [new file with mode: 0644]
Open-ILS/eg2-src/tsconfig.json [new file with mode: 0644]
Open-ILS/eg2-src/tslint.json [new file with mode: 0644]
Open-ILS/webby-src/.angular-cli.json [deleted file]
Open-ILS/webby-src/.editorconfig [deleted file]
Open-ILS/webby-src/.gitignore [deleted file]
Open-ILS/webby-src/README.adoc [deleted file]
Open-ILS/webby-src/e2e/app.e2e-spec.ts [deleted file]
Open-ILS/webby-src/e2e/app.po.ts [deleted file]
Open-ILS/webby-src/e2e/tsconfig.e2e.json [deleted file]
Open-ILS/webby-src/karma.conf.js [deleted file]
Open-ILS/webby-src/package.json [deleted file]
Open-ILS/webby-src/protractor.conf.js [deleted file]
Open-ILS/webby-src/src/app/app.component.ts [deleted file]
Open-ILS/webby-src/src/app/app.module.ts [deleted file]
Open-ILS/webby-src/src/app/core/README [deleted file]
Open-ILS/webby-src/src/app/core/auth.ts [deleted file]
Open-ILS/webby-src/src/app/core/event.ts [deleted file]
Open-ILS/webby-src/src/app/core/idl.ts [deleted file]
Open-ILS/webby-src/src/app/core/net.ts [deleted file]
Open-ILS/webby-src/src/app/core/org.ts [deleted file]
Open-ILS/webby-src/src/app/core/pcrud.ts [deleted file]
Open-ILS/webby-src/src/app/core/store.ts [deleted file]
Open-ILS/webby-src/src/app/resolver.service.ts [deleted file]
Open-ILS/webby-src/src/app/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/share/README [deleted file]
Open-ILS/webby-src/src/app/share/catalog/catalog-url.service.ts [deleted file]
Open-ILS/webby-src/src/app/share/catalog/catalog.service.ts [deleted file]
Open-ILS/webby-src/src/app/share/catalog/search-context.ts [deleted file]
Open-ILS/webby-src/src/app/share/org-select.component.html [deleted file]
Open-ILS/webby-src/src/app/share/org-select.component.ts [deleted file]
Open-ILS/webby-src/src/app/share/unapi.ts [deleted file]
Open-ILS/webby-src/src/app/share/util/pager.ts [deleted file]
Open-ILS/webby-src/src/app/staff/admin/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/admin/workstation/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/app.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/admin/workstation/workstations/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/app.component.css [deleted file]
Open-ILS/webby-src/src/app/staff/app.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/app.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/app.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/app.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/app.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/app.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/app.service.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/copies.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/pagination.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/record.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/record/record.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/resolver.service.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/facets.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.css [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/pagination.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/record.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/record.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/results.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/result/results.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/search-form.component.css [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/search-form.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/catalog/search-form.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/app.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/circ/patron/bcsearch/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/circ/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/login.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/login.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/nav.component.css [deleted file]
Open-ILS/webby-src/src/app/staff/nav.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/nav.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/resolver.service.ts [deleted file]
Open-ILS/webby-src/src/app/staff/routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/share/README [deleted file]
Open-ILS/webby-src/src/app/staff/share/bib-summary.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/share/bib-summary.component.ts [deleted file]
Open-ILS/webby-src/src/app/staff/splash.component.html [deleted file]
Open-ILS/webby-src/src/app/staff/splash.component.ts [deleted file]
Open-ILS/webby-src/src/app/welcome.component.html [deleted file]
Open-ILS/webby-src/src/app/welcome.component.ts [deleted file]
Open-ILS/webby-src/src/assets/.gitkeep [deleted file]
Open-ILS/webby-src/src/environments/environment.prod.ts [deleted file]
Open-ILS/webby-src/src/environments/environment.ts [deleted file]
Open-ILS/webby-src/src/favicon.ico [deleted file]
Open-ILS/webby-src/src/index.html [deleted file]
Open-ILS/webby-src/src/main.ts [deleted file]
Open-ILS/webby-src/src/polyfills.ts [deleted file]
Open-ILS/webby-src/src/styles.css [deleted file]
Open-ILS/webby-src/src/test.ts [deleted file]
Open-ILS/webby-src/src/tsconfig.app.json [deleted file]
Open-ILS/webby-src/src/tsconfig.spec.json [deleted file]
Open-ILS/webby-src/src/typings.d.ts [deleted file]
Open-ILS/webby-src/tsconfig.json [deleted file]
Open-ILS/webby-src/tslint.json [deleted file]

diff --git a/Open-ILS/eg2-src/.angular-cli.json b/Open-ILS/eg2-src/.angular-cli.json
new file mode 100644 (file)
index 0000000..a90b800
--- /dev/null
@@ -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 (file)
index 0000000..6e87a00
--- /dev/null
@@ -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 (file)
index 0000000..54bfd20
--- /dev/null
@@ -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 (file)
index 0000000..fd58af9
--- /dev/null
@@ -0,0 +1,17 @@
+= EG Angular2 App =
+
+=== Apache Configuration ===
+
+[source,conf]
+---------------------------------------------------------------------
+<Directory "/openils/var/web/webby">
+    FallbackResource /webby/index.html
+</Directory>
+---------------------------------------------------------------------
+
+=== 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 (file)
index 0000000..c2a69a8
--- /dev/null
@@ -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 (file)
index 0000000..82ea75b
--- /dev/null
@@ -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 (file)
index 0000000..1d9e5ed
--- /dev/null
@@ -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 (file)
index 0000000..af139fa
--- /dev/null
@@ -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 (file)
index 0000000..41b5925
--- /dev/null
@@ -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 (file)
index 0000000..7ee3b5e
--- /dev/null
@@ -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 (file)
index 0000000..d049f7a
--- /dev/null
@@ -0,0 +1,11 @@
+import {Component} from '@angular/core';
+
+@Component({
+  selector: 'eg-root',
+  template: '<router-outlet></router-outlet>'
+})
+
+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 (file)
index 0000000..d9d06e3
--- /dev/null
@@ -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 (file)
index 0000000..58828be
--- /dev/null
@@ -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 (file)
index 0000000..611797a
--- /dev/null
@@ -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<any> {
+
+        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<any>( (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<void> {
+
+        return new Promise<void>((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<void> {
+
+        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<any> {
+        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<void> {
+        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 (file)
index 0000000..3f6afc7
--- /dev/null
@@ -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 (file)
index 0000000..8f46933
--- /dev/null
@@ -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 (file)
index 0000000..b037de1
--- /dev/null
@@ -0,0 +1,156 @@
+/**
+ * 
+ * constructor(private net : EgNetService) {
+ *   ...
+ *   egNet.request(service, method, param1 [, param2, ...])
+ *     .subscribe(
+ *       (res) => console.log('received one resopnse: ' + res),
+ *       (err) => console.error('recived request error: ' + err),
+ *       ()    => console.log('request complete')
+ *     )
+ *   );
+ *   ...
+ * }
+ *
+ * Each response is relayed via Observable onNext().  The interface is 
+ * the same for streaming and atomic requests.
+ */
+import { Injectable, EventEmitter } from '@angular/core';
+import { Observable, Observer } from 'rxjs/Rx';
+import { EgEventService, EgEvent } from './event';
+
+// Global vars from opensrf.js
+// These are availavble at runtime, but are not exported.
+declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
+
+export class EgNetRequest {
+    service    : String;
+    method     : String;
+    params     : any[];
+    observer   : Observer<any>;
+    superseded : Boolean = false;
+    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
+    session?   : any;
+
+    // Last EgEvent encountered by this request.
+    // Most callers will not need to import EgEvent since the parsed
+    // event will be available here.
+    evt: EgEvent;
+
+    constructor(service: String, method: String, params: any[], session?: any) {
+        this.service = service;
+        this.method = method;
+        this.params = params;
+        if (session) {
+            this.session = session;
+        } else {
+            this.session = new OpenSRF.ClientSession(service);
+        }
+    }
+}
+
+@Injectable()
+export class EgNetService {
+
+    permFailed$: EventEmitter<EgNetRequest>;
+    authExpired$: EventEmitter<EgNetRequest>;
+
+    // If true, permission failures are emitted via permFailed$ 
+    // and the active request is marked as superseded.
+    permFailedHasHandler: Boolean = false;
+
+    constructor(
+        private egEvt: EgEventService
+    ) { 
+        this.permFailed$ = new EventEmitter<EgNetRequest>();
+        this.authExpired$ = new EventEmitter<EgNetRequest>();
+    }
+
+    // Standard request call -- Variadic params version
+    request(service: String, method: String, ...params: any[]): Observable<any> {
+        return this.requestWithParamList(service, method, params);
+    }
+
+    // Array params version
+    requestWithParamList(service: String, 
+        method: String, params: any[]): Observable<any> {
+        return this.requestCompiled(
+            new EgNetRequest(service, method, params));
+    }
+
+    requestCompiled(request: EgNetRequest): Observable<any> {
+        return Observable.create(
+            observer => {
+                request.observer = observer;
+                this.sendCompiledRequest(request);
+            }
+        );
+    }
+
+    // Version with pre-compiled EgNetRequest object
+    sendCompiledRequest(request: EgNetRequest): void {
+        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
+        var this_ = this;
+
+        request.session.request({
+            async  : true,
+            method : request.method,
+            params : request.params,
+            oncomplete : function() {
+                // A superseded request will be complete()'ed by the 
+                // superseder at a later time.
+                if (!request.superseded)
+                    request.observer.complete();
+            },
+            onresponse : function(r) {
+                this_.dispatchResponse(request, r.recv().content());
+            },
+            onerror : function(errmsg) {
+                let msg = `${request.method} failed! See server logs. ${errmsg}`;
+                console.error(msg);
+                request.observer.error(msg);
+            },
+            onmethoderror : function(req, statCode, statMsg) { 
+                let msg = 
+                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
+                console.error(msg);
+
+                if (request.service == 'open-ils.pcrud' && statCode == 401) {
+                    // 401 is the PCRUD equivalent of a NO_SESSION event
+                    this_.authExpired$.emit(request);
+                }
+
+                request.observer.error(msg);
+            }
+
+        }).send();
+    }
+
+    // Relay response object to the caller for typical/successful responses.  
+    // Applies special handling to response events that require global attention.
+    private dispatchResponse = function(request, response) {
+        request.evt = this.egEvt.parse(response);
+
+        if (request.evt) {
+            switch(request.evt.textcode) {
+
+                case 'NO_SESSION':
+                    console.debug(`EgNet emitting event: ${request.evt}`);
+                    request.observer.error(request.evt.toString());
+                    this.authExpired$.emit(request);
+                    return;
+
+                case 'PERM_FAILURE':
+                    if (this.permFailedHasHandler) {
+                        console.debug(`EgNet emitting event: ${request.evt}`);
+                        request.superseded = true;
+                        this.permFailed$.emit(request);
+                        return;
+                    }
+            } 
+        }
+
+        // Pass the response to the caller.
+        request.observer.next(response);
+    };
+}
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 (file)
index 0000000..44eddd6
--- /dev/null
@@ -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<void> {
+        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 (file)
index 0000000..0cee7d3
--- /dev/null
@@ -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<EgPcrudResponse>;
+
+    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<EgPcrudContext> {
+        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<EgPcrudResponse> {
+        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<EgPcrudResponse> {
+        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<EgPcrudResponse> {
+        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<EgPcrudResponse> {
+        return this.cud('create', list)
+    }
+    update(list: EgIdlObject[]): Observable<EgPcrudResponse> {
+        return this.cud('update', list)
+    }
+    remove(list: EgIdlObject[]): Observable<EgPcrudResponse> {
+        return this.cud('delete', list)
+    }
+    autoApply(list: EgIdlObject[]): Observable<EgPcrudResponse> { // RENAMED
+        return this.cud('auto',   list)
+    }
+
+    xactClose(): Observable<EgPcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.' + this.xactCloseMode,
+            [this.token()]
+        );
+    };
+
+    xactBegin(): Observable<EgPcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.begin', [this.token()]
+        );
+    };
+
+    private dispatch(method: string, params: any[]): Observable<EgPcrudResponse> {
+        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<EgPcrudResponse>): Observable<EgPcrudResponse> {
+        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<EgPcrudResponse> {
+
+        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<EgPcrudResponse> {
+
+        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<EgPcrudContext> {
+        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<EgPcrudResponse> {
+        return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps);
+    }
+
+    retrieveAll(fmClass: string, pcrudOps?: any, 
+        reqOps?: EgPcrudReqOps): Observable<EgPcrudResponse> {
+        return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps);
+    }
+
+    search(fmClass: string, search: any, 
+        pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable<EgPcrudResponse> {
+        return this.newContext().search(fmClass, search, pcrudOps, reqOps);
+    }
+
+    create(list: EgIdlObject[]): Observable<EgPcrudResponse> {
+        return this.newContext().create(list);
+    }
+
+    update(list: EgIdlObject[]): Observable<EgPcrudResponse> {
+        return this.newContext().update(list);
+    }
+
+    remove(list: EgIdlObject[]): Observable<EgPcrudResponse> {
+        return this.newContext().remove(list);
+    }
+
+    autoApply(list: EgIdlObject[]): Observable<EgPcrudResponse> { 
+        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 (file)
index 0000000..e1a879b
--- /dev/null
@@ -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<any> {
+        // 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<any> {
+        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<any> {
+        // 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<any> {
+        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<any> {
+        // TODO: route keys appropriately
+        return Promise.resolve(this.removeLocalItem(key));
+    }
+
+    removeLocalItem(key: string): void {
+        window.localStorage.removeItem(key);
+    }
+
+    removeServerItem(key: string): Promise<any> {
+        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 (file)
index 0000000..7ffa74b
--- /dev/null
@@ -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<Promise<void>> {
+
+    constructor(
+        private router: Router, 
+        private idl: EgIdlService,
+        private org: EgOrgService,
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot, 
+        state: RouterStateSnapshot): Promise<void> {
+
+        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 (file)
index 0000000..7d7e70e
--- /dev/null
@@ -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 (file)
index 0000000..1a8b6e1
--- /dev/null
@@ -0,0 +1,7 @@
+Common Angular services and associated types/classes.  
+
+This collection of services MIGHT be used by practically all applications.
+They are NOT automatically imported/exported by the base module and should
+be loaded within the requesting application as needed.
+
+
diff --git a/Open-ILS/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 (file)
index 0000000..00f3203
--- /dev/null
@@ -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 (file)
index 0000000..96f8d24
--- /dev/null
@@ -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<void> {
+        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<any> {
+        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<void> {
+
+        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<void> {
+
+        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<void> {
+        // At the moment, we only need facet CMFs.
+        if (Object.keys(this.cmfMap).length) 
+            return Promise.resolve();
+
+        return new Promise((resolve, reject) => {
+            this.pcrud.search('cmf', 
+                {facet_field : 't'}, {}, {atomic : true}
+            ).subscribe(
+                cmfs => {
+                    cmfs.forEach(c => this.cmfMap[c.id()] = c);
+                    resolve();
+                }
+            )
+        });
+    }
+
+
+    /**
+     * Bib record via UNAPI as mods (for now) with holdings summary
+     * and record attributes.
+     */
+    getBibSummary(bibId: number, orgId?: number, depth?: number): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.unapi.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 (file)
index 0000000..b3c21e5
--- /dev/null
@@ -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 (file)
index 0000000..d7b9101
--- /dev/null
@@ -0,0 +1,15 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<input type="text" 
+  class="form-control" 
+  [placeholder]="placeholder"
+  [(ngModel)]="selected" 
+  [ngbTypeahead]="filter"
+  [resultTemplate]="displayTemplate"
+  [inputFormatter]="formatter"
+  (selectItem)="orgChanged($event)"
+/>
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 (file)
index 0000000..7738215
--- /dev/null
@@ -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<EgIdlObject>();
+
+    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<string>): Observable<OrgDisplay[]> => {
+        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 (file)
index 0000000..28c2589
--- /dev/null
@@ -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<XMLDocument> {
+        // XReq creates an XML document for us.  Seems like the right
+        // tool for the job.
+        let url = this.createUrl(params);
+        return new Promise((resolve, reject) => {
+            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 (file)
index 0000000..1c21a8d
--- /dev/null
@@ -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 (file)
index 0000000..4e4ef09
--- /dev/null
@@ -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 (file)
index 0000000..114c312
--- /dev/null
@@ -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 (file)
index 0000000..5b95268
--- /dev/null
@@ -0,0 +1,75 @@
+<div class="row">
+  <div class="col-8 offset-1">
+    <div class="alert alert-warning" *ngIf="removingWs" i18n>
+      Workstation {{removingWs}} is no longer valid.  Removing registration.
+    </div>
+    <div class="alert alert-danger" *ngIf="workstations.length == 0">
+      <span i18n>Please register a workstation.</span>
+    </div>
+
+    <div class="row">
+      <div class="col" i18n>Register a New Workstation For This Browser</div>
+    </div>
+    <div class="row mt-2">
+      <div class="col-2">
+        <eg-org-select 
+          (onChange)="orgOnChange"
+          [hideOrgs]="hideOrgs"
+          [disableOrgs]="disableOrgs"
+          [initialOrg]="initialOrg"
+          [placeholder]="'Owner'" >
+        </eg-org-select>
+      </div>
+      <div class="col-6">
+        <div class="input-group">
+          <input type='text'
+            class='form-control'
+            i18n-title
+            title="Workstation Name"
+            i18n-placeholder
+            placeholder="Workstation Name"
+            [(ngModel)]='newName'/>
+          <div class="input-group-btn">
+            <button class="btn btn-light" (click)="registerWorkstation()">
+              <span i18n>Register</span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="row mt-3 pt-3 border border-left-0 border-right-0 border-bottom-0 border-light">
+      <div class="col">
+        <span i18n>Workstations Registered With This Browser</span>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col-6">
+        <select
+          class="form-control"
+          [(ngModel)]="selectedId">
+          <option *ngFor="let ws of workstations" value="{{ws.id}}">
+            {{ws.name}}
+          </option>
+        </select>
+      </div>
+    </div>
+    <div class="row mt-2">
+      <div class="col-md-6">
+        <button i18n class="btn btn-success" 
+          (click)="useNow()" [disabled]="!selected">
+          Use Now
+        </button>
+        <button i18n class="btn btn-light" 
+          (click)="setDefault()" [disabled]="!selected">
+          Mark As Default
+        </button>
+        <button i18n class="btn btn-danger"
+          (click)="removeSelected()"
+          [disabled]="!selected || isRemoving || !canDeleteSelected()">
+          Remove
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+
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 (file)
index 0000000..b724dc0
--- /dev/null
@@ -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 (file)
index 0000000..c7051fb
--- /dev/null
@@ -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 (file)
index 0000000..f1ac37e
--- /dev/null
@@ -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 (file)
index 0000000..508d879
--- /dev/null
@@ -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 (file)
index 0000000..7bd463a
--- /dev/null
@@ -0,0 +1,8 @@
+<!-- top navigation bar -->
+<eg-staff-nav-bar></eg-staff-nav-bar>
+
+<div id='staff-content-container'>
+  <!-- page content -->
+  <router-outlet></router-outlet>
+</div>
+
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 (file)
index 0000000..3c90ab0
--- /dev/null
@@ -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 (file)
index 0000000..7b53d7f
--- /dev/null
@@ -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 (file)
index 0000000..1596454
--- /dev/null
@@ -0,0 +1,6 @@
+<!-- search form sits atop every catalog page -->
+<eg-catalog-search-form></eg-catalog-search-form>
+
+<!-- search results, record details, etc. -->
+<router-outlet></router-outlet>
+
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 (file)
index 0000000..a5ca68f
--- /dev/null
@@ -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 (file)
index 0000000..b76cc0b
--- /dev/null
@@ -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 (file)
index 0000000..625206e
--- /dev/null
@@ -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 (file)
index 0000000..84e9d8e
--- /dev/null
@@ -0,0 +1,71 @@
+<div class='eg-copies w-100'>
+  <ul class="pagination mb-1">
+    <li class="page-item" [ngClass]="{disabled : pager.offset == 0}">
+      <a class="no-href page-link" 
+        i18n-aria-label aria-label="Start" (click)="firstPage()">
+        <span i18n>Start</span>
+      </a>
+    </li>
+    <li class="page-item" [ngClass]="{disabled : pager.offset == 0}">
+      <a class="no-href page-link" 
+        i18n-aria-label aria-label="Previous" (click)="prevPage()">
+        <span i18n>Previous</span>
+      </a>
+    </li>
+    <!-- note disable logic is incomplete -->
+    <li class="page-item"
+      [ngClass]="{disabled: copies.length < pager.limit}">
+      <a class="no-href page-link" 
+        i18n-aria-label aria-label="Next" (click)="nextPage()">
+        <span i18n>Next</span>
+      </a>
+    </li>
+  </ul>
+  <div class='card tight-card w-100'>
+    <div class="card-header font-weight-bold d-flex bg-info">
+      <div class="flex-1" i18n>Location</div>
+      <div class="flex-1 pl-1" i18n>Call Number / Copy Notes</div>
+      <div class="flex-1 pl-1" i18n>Barcode</div>
+      <div class="flex-1 pl-1" i18n>Shelving Location</div>
+      <div class="flex-1 pl-1" i18n>Circulation Modifier</div>
+      <div class="flex-1 pl-1" i18n>Age Hold Protection</div>
+      <div class="flex-1 pl-1" i18n>Active/Create Date</div>
+      <div class="flex-1 pl-1" i18n>Holdable?</div>
+      <div class="flex-1 pl-1" i18n>Status</div>
+      <div class="flex-1 pl-1" i18n>Due Date</div>
+    </div>
+    <div class="card-body">
+      <ul class="list-group list-group-flush" *ngIf="copies && copies.length">
+        <li class="list-group-item" *ngFor="let copy of copies; let idx = index" 
+          [ngClass]="{'list-group-item-info': (idx % 2) == 1}">
+          <div class="d-flex">
+            <div class="flex-1" i18n>{{orgName(copy.circ_lib)}}</div>
+            <div class="flex-1 pl-1" i18n>
+              {{copy.call_number_prefix_label}}
+              {{copy.call_number_label}}
+              {{copy.call_number_suffix_label}}
+            </div>
+            <div class="flex-1 pl-1" i18n>
+              {{copy.barcode}}
+              <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}" i18n>View</a>
+              | 
+              <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}/edit" i18n>Edit</a>
+            </div>
+            <div class="flex-1 pl-1" i18n>{{copy.copy_location}}</div>
+            <div class="flex-1 pl-1" i18n>{{copy.circ_modifier || ''}}</div>
+            <div class="flex-1 pl-1" i18n>{{copy.age_protect}}</div>
+            <div class="flex-1 pl-1" i18n>
+              {{copy.active_date || copy.create_date | date:'shortDate'}}
+            </div>
+            <div class="flex-1 pl-1">
+              <span *ngIf="holdable(copy)" i18n>Yes</span>
+              <span *ngIf="!holdable(copy)" i18n>No</span>
+            </div>
+            <div class="flex-1 pl-1" i18n>{{copy.copy_status}}</div>
+            <div class="flex-1 pl-1" i18n>{{copy.due_date | date:'shortDate'}}</div>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
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 (file)
index 0000000..f234eba
--- /dev/null
@@ -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 (file)
index 0000000..0edcded
--- /dev/null
@@ -0,0 +1,36 @@
+<ul class="pagination mb-0" *ngIf="index !== null">
+  <li class="page-item" [ngClass]="{disabled : index == 0}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Start" (click)="firstRecord()">
+      <span i18n>Start</span>
+    </a>
+  </li>
+  <li class="page-item" [ngClass]="{disabled : index == 0}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Previous" (click)="prevRecord()">
+      <span i18n>Previous</span>
+    </a>
+  </li>
+  <li class="page-item"
+    [ngClass]="{disabled : index >= searchContext.result.count - 1}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Next" (click)="nextRecord()">
+      <span i18n>Next</span>
+    </a>
+  </li>
+  <li class="page-item"
+      [ngClass]="{disabled : index >= searchContext.result.count - 1}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="End" (click)="lastRecord()">
+      <span i18n>End</span>
+    </a>
+  </li>
+  <li class="page-item">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Back to Results" (click)="returnToSearch()">
+      <span i18n>
+        Back to Results ({{index + 1}} / {{searchContext.result.count}})
+      </span>
+    </a>
+  </li>
+</ul>
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 (file)
index 0000000..a7535f6
--- /dev/null
@@ -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<void> {
+        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<number> {
+
+        // 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<void> {
+
+        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 (file)
index 0000000..127254a
--- /dev/null
@@ -0,0 +1,18 @@
+
+<div id="staff-catalog-record-container">
+  <div id='staff-catalog-bib-navigation'>
+    <div *ngIf="searchContext.isSearchable()">
+      <eg-catalog-record-pagination [recordId]="recordId">
+      </eg-catalog-record-pagination>
+    </div>
+  </div>
+  <div id='staff-catalog-bib-summary-container' class='mt-1'>
+    <eg-bib-summary [bibSummary]="bibSummary">
+    </eg-bib-summary>
+  </div>
+  <div id='staff-catalog-copies-container' class='mt-3'>
+    <eg-catalog-copies [recordId]="recordId"></eg-catalog-copies>
+  </div>
+</div>
+
+
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 (file)
index 0000000..78552eb
--- /dev/null
@@ -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 (file)
index 0000000..8929d55
--- /dev/null
@@ -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<Promise<any[]>> {
+
+    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<any[]> {
+
+        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 (file)
index 0000000..188ae30
--- /dev/null
@@ -0,0 +1,43 @@
+<style>
+  .facet-selected {
+    background-color: #DDD;
+  }
+  .card {
+    width: 100%;
+  }
+  .list-group-item {padding: .5rem .75rem .5rem .75rem}
+</style>
+<div *ngIf="searchContext.result.facetData">
+  <div *ngFor="let facetConf of facetConfig.display">
+    <div *ngIf="searchContext.result.facetData[facetConf.facetClass]">
+      <div *ngFor="let name of facetConf.facetOrder">
+        <div class="row"
+          *ngIf="searchContext.result.facetData[facetConf.facetClass][name]">
+          <div class="card mb-2">
+            <h4 class="card-header">
+              {{searchContext.result.facetData[facetConf.facetClass][name].cmfLabel}}
+            </h4>
+            <ul class="list-group list-group-flush">
+              <li class="list-group-item" 
+                [ngClass]="{'facet-selected' :
+                  facetIsApplied(facetConf.facetClass, name, value.value)}"
+                *ngFor="
+                  let value of searchContext.result.facetData[facetConf.facetClass][name].valueList | slice:0:facetConfig.displayCount">
+                <div class="row">
+                  <div class="col-9">
+                    <a class="card-link"
+                      href='javascript:;'
+                      (click)="applyFacet(facetConf.facetClass, name, value.value)">
+                      {{value.value}}
+                    </a>
+                  </div>
+                  <div class="col-3">{{value.count}}</div>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
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 (file)
index 0000000..8101ced
--- /dev/null
@@ -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 (file)
index 0000000..c283ff4
--- /dev/null
@@ -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 (file)
index 0000000..55b63dd
--- /dev/null
@@ -0,0 +1,28 @@
+<!-- 
+Using bare BS pagination instead of ng-bootstrap, which seemed 
+unnecessary given we have to track paging externally anyway.
+-->
+<ul class="pagination">
+  <li class="page-item" 
+    [ngClass]="{disabled : searchContext.pager.isFirstPage()}">
+    <a (click)="prevPage()"
+      class="page-link" 
+      i18n-aria-label
+      aria-label="Previous">
+      <span aria-hidden="true">&laquo;</span>
+    </a>
+  </li>
+  <li class="page-item" 
+    *ngFor="let page of searchContext.pager.pageList()"
+    [ngClass]="{active : searchContext.pager.currentPage() == page}">
+    <a class="page-link" (click)="setPage(page)">
+      {{page}} <span class="sr-only" i18n>(current)</span></a>
+  </li>
+  <li class="page-item" 
+    [ngClass]="{disabled : searchContext.pager.isLastPage()}">
+    <a (click)="nextPage()"
+      class="page-link" aria-label="Next" i18n-aria-label>
+      <span aria-hidden="true">&raquo;</span>
+    </a>
+  </li>
+</ul>
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 (file)
index 0000000..8dbb4d8
--- /dev/null
@@ -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 (file)
index 0000000..c9a0cd9
--- /dev/null
@@ -0,0 +1,129 @@
+<!-- 
+  TODO
+  routerLink's
+  egDateFilter's
+-->
+
+<div class="col-12 card tight-card mb-2 bg-light">
+  <div class="card-body">
+    <div class="row">
+      <div class="col-1">
+        <!-- TODO router links -->
+        <a href="./cat/catalog/record/{{bibSummary.id}}">
+          <img style="height:80px"
+            src="/opac/extras/ac/jacket/small/r/{{bibSummary.id}}"/>
+        </a>
+      </div>
+      <div class="col-5">
+        <div class="row">
+          <div class="col-12 font-weight-bold">
+            <!-- nbsp allows the column to take shape when no value exists -->
+            <span class="font-weight-light font-italic">
+              #{{index + 1 + searchContext.pager.offset}}
+            </span>
+            <a href="javascript:void(0)"
+              (click)="navigatToRecord(bibSummary.id)">
+              {{bibSummary.title || '&nbsp;'}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-12">
+            <!-- nbsp allows the column to take shape when no value exists -->
+            <a href="javascript:void(0)"
+              (click)="searchAuthor(bibSummary)">
+              {{bibSummary.author || '&nbsp;'}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-12">
+            <span>
+              <img class="pad-right-min"
+                src="/images/format_icons/icon_format/{{bibSummary.ccvms.icon_format.code}}.png"/>
+              <span>{{bibSummary.ccvms.icon_format.label}}</span>
+            </span>
+            <span style='pl-2'>{{bibSummary.edition}}</span>
+            <span style='pl-2'>{{bibSummary.pubdate}}</span>
+          </div>
+        </div>
+      </div>
+      <div class="col-2">
+        <div class="row" [ngClass]="{'pt-2':copyIndex > 0}" 
+          *ngFor="let copyCount of bibSummary.copyCounts; let copyIdx = index">
+          <div class="w-100" *ngIf="copyCount.type == 'staff'">
+            <div class="float-left text-left w-50">
+              <span class="pr-1">
+              {{copyCount.available}} / {{copyCount.count}} items
+              </span>
+            </div>
+            <div class="float-left w-50">
+              @ {{orgName(copyCount.org_unit)}}
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="col-1">
+        <div class="row">
+          <div class="w-100">
+            TCN: {{bibSummary.tcn_value}}
+          </div>
+        </div>
+        <div class="row">
+          <div class="w-100">
+            Holds: {{bibSummary.holdCount}}
+          </div>
+        </div>
+      </div>
+      <div class="col-3">
+        <div class="row">
+          <div class="col-12">
+            <div class="float-right small-text-1">
+              Created {{bibSummary.create_date | date:'shortDate'}} by
+              <!-- creator if fleshed after the initial data set is loaded -->
+              <a *ngIf="bibSummary.creator.usrname" target="_self" 
+                href="./staff/circ/patron/{{bibSummary.creator.id()}}/checkout">
+                  {{bibSummary.creator.usrname()}}
+              </a>
+              <!-- add a spacer pending data to reduce page shuffle -->
+              <span *ngIf="!bibSummary.creator.usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-12">
+            <div class="float-right small-text-1">
+              Edited {{bibSummary.edit_date | date:'shortDate'}} by
+              <a *ngIf="bibSummary.editor.usrname" target="_self" 
+                href="./staff/circ/patron/{{bibSummary.editor.id()}}/checkout">
+                  {{bibSummary.editor.usrname()}}
+              </a>
+              <span *ngIf="!bibSummary.editor.usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-12">
+            <div class="float-right">
+              <span>
+                <button (click)="placeHold()"
+                  class="btn btn-sm btn-success label-with-material-icon small-text-1">
+                  <span class="material-icons">check</span>
+                  <span i18n>Place Hold</span>
+                </button>
+              </span>
+              <span>
+                <button (click)="addToList()" 
+                  class="btn btn-sm btn-info label-with-material-icon small-text-1">
+                  <span class="material-icons">playlist_add_check</span>
+                  <span i18n>Add to List</span>
+                </button>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div><!-- col -->
+    </div><!-- row -->
+  </div><!-- card-body -->
+</div><!-- card -->
+
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 (file)
index 0000000..beee4cf
--- /dev/null
@@ -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 (file)
index 0000000..be7c36a
--- /dev/null
@@ -0,0 +1,30 @@
+
+<div id="staff-catalog-results-container" *ngIf="searchIsDone()">
+  <div class="row">
+    <div class="col-2"><!--match pagination margin-->
+      <h3 i18n>Search Results ({{searchContext.result.count}})</h3>
+    </div>
+    <div class="col-1"></div>
+    <div class="col-9">
+      <div class="float-right">
+                               <eg-catalog-result-pagination></eg-catalog-result-pagination>
+      </div>
+    </div>
+  </div>
+       <div class="row mt-2">
+               <div class="col-2">
+      <eg-catalog-result-facets></eg-catalog-result-facets>
+               </div>
+               <div class="col-10">
+                       <div *ngIf="searchContext.result">
+                               <div *ngFor="let bibSummary of searchContext.result.records; let idx = index">
+          <div *ngIf="bibSummary">
+                                         <eg-catalog-result-record [bibSummary]="bibSummary" [index]="idx">
+                                         </eg-catalog-result-record>
+          </div>
+                               </div>
+                       </div>
+               </div>
+       </div>
+</div>
+
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 (file)
index 0000000..b87a2cd
--- /dev/null
@@ -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 (file)
index 0000000..467db52
--- /dev/null
@@ -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 (file)
index 0000000..f67d8fa
--- /dev/null
@@ -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 (file)
index 0000000..3ee4d21
--- /dev/null
@@ -0,0 +1,219 @@
+<!--
+TODO focus search input
+-->
+<div id='staffcat-search-form' class='pb-2 mb-3'>
+  <div class="row"
+    *ngFor="let q of searchContext.query; let idx = index; trackBy:trackByIdx">
+    <div class="col-9 d-flex flex-row">
+      <div class="flex-1">
+        <div *ngIf="idx == 0">
+          <select class="form-control" [(ngModel)]="searchContext.format">
+            <option value=''>All Formats</option>
+            <option *ngFor="let fmt of ccvmMap.search_format"
+              value="{{fmt.code()}}">{{fmt.value()}}</option>
+          </select>
+        </div>
+        <div *ngIf="idx > 0">
+          <select class="form-control"
+            [(ngModel)]="searchContext.joinOp[idx]">
+            <option value='&&'>And</option>
+            <option value='||'>Or</option>
+          </select>
+        </div>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.fieldClass[idx]">
+          <option value='keyword'>Keyword</option>
+          <option value='title'>Title</option>
+          <option value='jtitle'>Journal Title</option>
+          <option value='author'>Author</option>
+          <option value='subject'>Subject</option>
+          <option value='series'>Series</option>
+        </select>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.matchOp[idx]">
+          <option value='contains'>Contains</option>
+          <option value='nocontains'>Does not contain</option>
+          <option value='phrase'>Contains phrase</option>
+          <option value='exact'>Matches exactly</option>
+          <option value='starts'>Starts with</option>
+        </select>
+      </div>
+      <div class="flex-2 pl-1">
+        <div class="form-group">
+          <input type="text" class="form-control"
+            TODOfocus-me="searchContext.focus_query[idx]"
+            [(ngModel)]="searchContext.query[idx]"
+            (keyup)="checkEnter($event)"
+            placeholder="Query..."/>
+        </div>
+      </div>
+      <div class="flex-1 pl-1">
+        <button class="btn btn-sm material-icon-button"
+          (click)="addSearchRow(idx + 1)">
+          <span class="material-icons">add_circle_outline</span>
+        </button>
+        <button class="btn btn-sm material-icon-button"
+          [disabled]="searchContext.query.length < 2"
+          (click)="delSearchRow(idx)">
+          <span class="material-icons">remove_circle_outline</span>
+        </button>
+      </div>
+    </div><!-- col -->
+    <div class="col-3">
+      <div *ngIf="idx == 0" class="float-right">
+        <button class="btn btn-success" type="button"
+          [disabled]="searchIsActive()"
+          (click)="searchContext.pager.offset=0;searchByForm()">
+          Search
+        </button>
+        <button class="btn btn-warning" type="button"
+          [disabled]="searchIsActive()"
+          (click)="searchContext.reset()">
+          Clear Form
+        </button>
+        <button class="btn btn-outline-secondary" type="button"
+          *ngIf="!showAdvanced()"
+          [disabled]="searchIsActive()"
+          (click)="showAdvancedSearch=true">
+          More Filters
+        </button>
+        <button class="btn btn-outline-secondary" type="button"
+          *ngIf="showAdvanced()"
+          (click)="showAdvancedSearch=false">
+          Hide Filters
+        </button>
+      </div>
+    </div>
+  </div><!-- row -->
+
+  <div class="row">
+    <div class="col-9 d-flex flex-row">
+      <div class="flex-1">
+        <eg-org-select 
+          (onChange)="orgOnChange($event)"
+          [initialOrg]="searchContext.searchOrg"
+          [placeholder]="'Library'" >
+        </eg-org-select>
+      </div>
+      <div class="flex-3 pl-1">
+        <select class="form-control" [(ngModel)]="searchContext.sort">
+          <option value='' i18n>Sort by Relevance</option>
+          <optgroup label="Sort by Title" i18n-label>
+            <option value='titlesort' i18n>Title: A to Z</option>
+            <option value='titlesort.descending' i18n>Title: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Author" i18n-label>
+            <option value='authorsort' i18n>Author: A to Z</option>
+            <option value='authorsort.descending' i18n>Author: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Publication Date" i18n-label>
+            <option value='pubdate' i18n>Date: A to Z</option>
+            <option value='pubdate.descending' i18n>Date: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Popularity" i18n-label>
+            <option value='popularity' i18n>Most Popular</option>
+            <option value='poprel' i18n>Popularity Adjusted Relevance</option>
+          </optgroup>
+        </select>
+      </div>
+      <div class="flex-2 pl-2 align-self-end">
+        <div class="checkbox">
+          <label>
+            <input type="checkbox" [(ngModel)]="searchContext.available"/>
+            <span i18n>Limit to Available</span>
+          </label>
+        </div>
+      </div>
+      <div class="flex-4 pl-2 align-self-end">
+        <div class="checkbox">
+          <label>
+            <input type="checkbox" [(ngModel)]="searchContext.global"/>
+            <span i18n>Show Results from All Libraries</span>
+          </label>
+        </div>
+      </div>
+      <div class="flex-2 pl-1">
+        <!-- alignment -->
+      </div>
+    </div>
+    <div class="col-3">
+      <div *ngIf="searchIsActive()">
+        <div class="progress">
+          <div class="progress-bar progress-bar-striped active w-100"
+            role="progressbar" aria-valuenow="100" 
+            aria-valuemin="0" aria-valuemax="100">
+            <span class="sr-only" i18n>Searching..</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="row pt-2" *ngIf="showAdvanced()">
+    <div class="col-2">
+      <select class="form-control"  multiple="true"
+        [(ngModel)]="searchContext.ccvmFilters.item_type">
+        <option value='' i18n>All Item Types</option>
+        <option *ngFor="let itemType of ccvmMap.item_type"
+          value="{{itemType.code()}}">{{itemType.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <select class="form-control" multiple="true"
+        [(ngModel)]="searchContext.ccvmFilters.item_form">
+        <option value='' i18n>All Item Forms</option>
+        <option *ngFor="let itemForm of ccvmMap.item_form"
+          value="{{itemForm.code()}}">{{itemForm.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.item_lang" multiple="true">
+        <option value='' i18n>All Languages</option>
+        <option *ngFor="let lang of ccvmMap.item_lang"
+          value="{{lang.code()}}">{{lang.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.audience" multiple="true">
+        <option value='' i18n>All Audiences</option>
+        <option *ngFor="let audience of ccvmMap.audience"
+          value="{{audience.code()}}">{{audience.value()}}</option>
+      </select>
+    </div>
+  </div>
+  <div class="row pt-2" *ngIf="showAdvanced()">
+    <div class="col-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.vr_format" multiple="true">
+        <option value='' i18n>All Video Formats</option>
+        <option *ngFor="let vrFormat of ccvmMap.vr_format"
+          value="{{vrFormat.code()}}">{{vrFormat.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.bib_level" multiple="true">
+        <option value='' i18n>All Bib Levels</option>
+        <option *ngFor="let bibLevel of ccvmMap.bib_level"
+          value="{{bibLevel.code()}}">{{bibLevel.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.lit_form" multiple="true">
+        <option value='' i18n>All Literary Forms</option>
+        <option *ngFor="let litForm of ccvmMap.lit_form"
+          value="{{litForm.code()}}">{{litForm.value()}}</option>
+      </select>
+    </div>
+    <div class="col-2">
+      <i>Copy location filter goes here...</i>
+    </div>
+  </div>
+</div>
+
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 (file)
index 0000000..94ef0bf
--- /dev/null
@@ -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 (file)
index 0000000..1f55cb1
--- /dev/null
@@ -0,0 +1,8 @@
+<h2 i18n="Barcode Search Header">Search for Patron by Barcode</h2>
+
+<span i18n>Barcode:</span><input type='text' [ngModel]='barcode'/>
+
+<br/>
+<ul>
+    <li *ngFor="let str of strList">{{str}}</li>
+</ul>
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 (file)
index 0000000..43d36da
--- /dev/null
@@ -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 (file)
index 0000000..f119697
--- /dev/null
@@ -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 (file)
index 0000000..2a685f3
--- /dev/null
@@ -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 (file)
index 0000000..1b0a0f0
--- /dev/null
@@ -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 (file)
index 0000000..869fe87
--- /dev/null
@@ -0,0 +1,36 @@
+<div class="col-md-4 offset-md-4">
+  <fieldset>
+    <legend i18n>Sign In</legend>
+    <hr/>
+    <form (ngSubmit)="handleSubmit()" #loginForm="ngForm">
+
+      <div class="form-group">
+        <label for="username" i18n>Username</label>
+        <input 
+          type="text" 
+          class="form-control"
+          id="username" 
+          name="username"
+          required
+          i18n-placeholder
+          placeholder="Username" 
+          [(ngModel)]="args.username"/>
+      </div>
+
+      <div class="form-group">
+        <label for="password" i18n>Password</label>
+        <input 
+          type="password" 
+          class="form-control"
+          id="password" 
+          name="password"
+          required
+          i18n-placeholder
+          placeholder="Password" 
+          [(ngModel)]="args.password"/>
+      </div>
+
+      <button type="submit" class="btn btn-light" i18n>Sign in</button>
+    </form>
+  </fieldset>
+</div>
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 (file)
index 0000000..64ae6c5
--- /dev/null
@@ -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 (file)
index 0000000..ee4f93e
--- /dev/null
@@ -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 (file)
index 0000000..859ec7f
--- /dev/null
@@ -0,0 +1,203 @@
+<div id="staff-navbar" class="navbar fixed-top navbar-expand navbar-default">
+  <div class="collapse navbar-collapse">
+    <div class="navbar-nav">
+      <div class="nav-item">
+        <a i18n class="nav-link with-material-icon" routerLink="/staff/splash">
+          <span class="material-icons">home</span>
+        </a>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+         Search
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/search">
+            <span class="material-icons">person</span>
+            <span i18n>Search for Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/search">
+            <span class="material-icons">assignment</span>
+            <span i18n>Search for Copies by Barcode</span>
+          </a>
+          <a class="dropdown-item" routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>Search the Catalog</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle class="nav-link dropdown-toggle">
+         <span i18n>Circulation</span>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/bcsearch">
+            <span class="material-icons">trending_up</span>
+            <span i18n>Check Out</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/checkin/checkin">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Check In</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/checkin/capture">
+            <span class="material-icons">pin_drop</span>
+            <span i18n>Capture Holds</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/holds/pull">
+            <span class="material-icons">view_list</span>
+            <span i18n>Pull List for Hold Requests</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/renew/renew">
+            <span class="material-icons">autorenew</span>
+            <span i18n>Renew Items</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/register">
+            <span class="material-icons">person_add</span>
+            <span i18n>Register Patron</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/last">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Last Patron</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/search?show_recent=1">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Recent Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/pending/list">
+            <span class="material-icons">thumb_up</span>
+            <span i18n>Pending Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/bucket/view">
+            <span class="material-icons">list</span>
+            <span i18n>User Buckets</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/credentials">
+            <span class="material-icons">check_circle</span>
+            <span i18n>Verify Credentials</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/in_house_use/index">
+            <span class="material-icons">playlist_add</span>
+            <span i18n>Record In-House Use</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/holds/shelf">
+            <span class="material-icons">format_list_bulleted</span>
+            <span i18n>Holds Shelf</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/cat/item/replace_barcode/index">
+            <span class="material-icons">library_books</span>
+            <span i18n>Replace Barcode</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/search">
+            <span class="material-icons">question_answer</span>
+            <span i18n>Item Status</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
+            <span class="material-icons">grid_on</span>
+            <span i18n>Scan Item as Missing Pieces</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
+            <span class="material-icons">redo</span>
+            <span i18n>Reprint Last Receipt</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/offline-interface">
+            <span class="material-icons">signal_wifi_off</span>
+            <span i18n>Offline Circulation</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+         Cataloging
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>Search the Catalog</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Acquisitions
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>TODO</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Booking
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>TODO</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Administration
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>TODO</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+
+    <div class="navbar-nav mr-auto"></div>
+    <div class="navbar-nav">
+      <span i18n>{{user}} @ {{workstation}}</span>
+    </div>
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown" placement="bottom-right">
+        <a ngbDropdownToggle i18n 
+          i18n-title
+          title="Log out and more..."
+          class="nav-link dropdown-toggle no-caret with-material-icon">
+          <i class="material-icons">list</i>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a i18n class="dropdown-item" routerLink="/staff/login">
+            <span class="material-icons">lock_outline</span>
+            <span i18n>Logout</span>
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
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 (file)
index 0000000..62fb605
--- /dev/null
@@ -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 (file)
index 0000000..8c23030
--- /dev/null
@@ -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<Observable<any>> {
+
+    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<any> {
+
+        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<any>): Promise<void> {
+        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 (file)
index 0000000..81c0609
--- /dev/null
@@ -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 (file)
index 0000000..1d6d167
--- /dev/null
@@ -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 (file)
index 0000000..6626608
--- /dev/null
@@ -0,0 +1,66 @@
+
+<div class='eg-bib-summary card tight-card w-100' *ngIf="summary">
+  <div class="card-header d-flex">
+    <div class="font-weight-bold">
+      Record Summary
+    </div>
+    <div class="flex-1"></div>
+    <div>
+      <a class="with-material-icon no-href text-primary" 
+        title="Show More" i18n-title
+        *ngIf="!expandDisplay" (click)="expandDisplay=true">
+        <span class="material-icons">expand_more</span>
+      </a>
+      <a class="with-material-icon no-href text-primary" 
+        title="Show Less" i18n-title
+        *ngIf="expandDisplay" (click)="expandDisplay=false">
+        <span class="material-icons">expand_less</span>
+      </a>
+    </div>
+  </div>
+  <div class="card-body">
+    <ul class="list-group list-group-flush">
+      <li class="list-group-item">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Title:</div>
+          <div class="flex-3">{{summary.title}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
+          <div class="flex-1">{{summary.edition}}</div>
+          <div class="flex-1 font-weight-bold" i18n>TCN:</div>
+          <div class="flex-1">{{summary.tcn_value}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created By:</div>
+          <div class="flex-1" *ngIf="summary.creator.usrname">
+            {{summary.creator.usrname()}}
+          </div>
+        </div>
+      </li>
+      <li class="list-group-item" *ngIf="expandDisplay">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Author:</div>
+          <div class="flex-3">{{summary.author}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Pubdate:</div>
+          <div class="flex-1">{{summary.pubdate}}</div>
+          <div class="flex-1 font-weight-bold" i18n>Database ID:</div>
+          <div class="flex-1">{{summary.id}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited By:</div>
+          <div class="flex-1" *ngIf="summary.editor.usrname">
+            {{summary.editor.usrname()}}
+          </div>
+        </div>
+      </li>
+      <li class="list-group-item" *ngIf="expandDisplay">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Bib Call #:</div>
+          <div class="flex-3">{{summary.callNumber}}</div>
+          <div class="flex-1 font-weight-bold" i18n>Record Owner:</div>
+          <div class="flex-1">TODO</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created On:</div>
+          <div class="flex-1">{{summary.create_date | date:'shortDate'}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited On:</div>
+          <div class="flex-1">{{summary.edit_date | date:'shortDate'}}</div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</div>
+
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 (file)
index 0000000..877b18a
--- /dev/null
@@ -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 (file)
index 0000000..4846cc5
--- /dev/null
@@ -0,0 +1,121 @@
+
+
+<style>
+    /* TODO change BS color scheme so this isn't necessary */
+    .bg-evergreen {
+      background: -webkit-linear-gradient(#00593d, #007a54);
+      background-color: #007a54;
+      color: #fff;
+    }
+</style>
+
+<div class="container">
+
+  <!-- header icon -->
+  <div class="row mb-3">
+    <div class="col-12 text-center">
+      <img src="/images/portal/logo.png"/>
+    </div>
+  </div>
+
+  <div class="row" id="splash-nav">
+    <div class="col-4">
+      <div class="card">
+        <div class="card-header bg-evergreen">
+          <div class="panel-title text-center">Circulation and Patrons</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/forward.png"/>
+              <a href="/eg/staff/circ/patron/bcsearch">Check Out Items</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/back.png"/>
+              <a href="/eg/staff/circ/checkin/index">Check In Items</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/retreivepatron.png"/>
+              <a href="/eg/staff/circ/patron/search">Search For Patron By Name</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-4">
+      <div class="card">
+        <div class="card-header bg-evergreen">
+          <div class="panel-title text-center">Item Search and Cataloging</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <div class="input-group">
+                <input type="text" class="form-control" 
+                  [(ngModel)]="catSearchQuery"
+                  id='catalog-search-input'
+                  (keyup)="checkEnter($event)"
+                  i18n-placeholder placeholder="Search for...">
+                <span class="input-group-btn">
+                  <button class="btn btn-outline-secondary" 
+                    (click)="searchCatalog()" type="button">
+                    Search
+                  </button>
+                </span>
+                  <!--
+                  <input focus-me="focus_search" 
+                      class="form-control" ng-model="cat_query" type="text" 
+                      ng-keypress="catalog_search($event)"
+                      placeholder="Search catalog for..."/>
+                  <button class='btn btn-light' ng-click="catalog_search()">
+                      Search
+                  </button>
+                  -->
+              </div>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/bucket.png"/>
+              <a href="/eg/staff/cat/bucket/record/">Record Buckets</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/bucket.png"/>
+              <a href="/eg/staff/cat/bucket/copy/">Copy Buckets</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-4">
+      <div class="card">
+        <div class="card-header bg-evergreen">
+          <div class="panel-title text-center">Administration</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/helpdesk.png"/>
+              <a target="_top" href="http://docs.evergreen-ils.org/">
+                Evergreen Documentation
+              </a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/helpdesk.png"/>
+              <a target="_top" href="/eg/staff/admin/workstation/index">
+                Workstation Administration
+              </a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/reports.png"/>
+              <a target="_top" href="/eg/staff/reporter/legacy/main">
+                Reports
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
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 (file)
index 0000000..e113437
--- /dev/null
@@ -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 (file)
index 0000000..3ce97cc
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="jumbotron">
+  <h1 i18n class="display-3">Welcome to Webby</h1>
+  <p i18n class="lead">
+    If you see this page, you're probably in good shape...
+  </p>
+  <hr class="my-4"/>
+  <p i18n>
+    But maybe you meant to go to the <a routerLink="/staff/splash">staff page</a>
+    or <a routerLink="/catalog">the catalog.</a>
+  </p>
+</div>
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 (file)
index 0000000..398d127
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..3612073
--- /dev/null
@@ -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 (file)
index 0000000..b7f639a
--- /dev/null
@@ -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 (file)
index 0000000..8081c7c
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 (file)
index 0000000..a876726
--- /dev/null
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title i18n="Page Title">AngEG</title>
+  <base href="/webby">
+
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <!-- todo -->
+  <!-- see self-hosting options https://google.github.io/material-design-icons/#icon-font-for-the-web -->
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+  <!-- link to bootstrap manually for the time being.  With 
+        ng-bootstrap, we only need the CSS, not the JS -->
+  <link rel="stylesheet" 
+    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" 
+    integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" 
+    crossorigin="anonymous">
+</head>
+<body>
+  <eg-root></eg-root>
+  <script src="/IDL2js"></script>
+  <script src="/js/dojo/opensrf/JSON_v1.js"></script>
+  <script src="/js/dojo/opensrf/opensrf.js"></script>
+  <script src="/js/dojo/opensrf/opensrf_ws.js"></script>
+</body>
+</html>
diff --git a/Open-ILS/eg2-src/src/main.ts b/Open-ILS/eg2-src/src/main.ts
new file mode 100644 (file)
index 0000000..08b359c
--- /dev/null
@@ -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 (file)
index 0000000..20d4075
--- /dev/null
@@ -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 (file)
index 0000000..c580fb0
--- /dev/null
@@ -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 (file)
index 0000000..cd612ee
--- /dev/null
@@ -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 (file)
index 0000000..39ba8db
--- /dev/null
@@ -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 (file)
index 0000000..63d89ff
--- /dev/null
@@ -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 (file)
index 0000000..ef5c7bd
--- /dev/null
@@ -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 (file)
index 0000000..14a504d
--- /dev/null
@@ -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 (file)
index 0000000..c24dc29
--- /dev/null
@@ -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 (file)
index a90b800..0000000
+++ /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 (file)
index 6e87a00..0000000
+++ /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 (file)
index 54bfd20..0000000
+++ /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 (file)
index fd58af9..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-= EG Angular2 App =
-
-=== Apache Configuration ===
-
-[source,conf]
----------------------------------------------------------------------
-<Directory "/openils/var/web/webby">
-    FallbackResource /webby/index.html
-</Directory>
----------------------------------------------------------------------
-
-=== 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 (file)
index c2a69a8..0000000
+++ /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 (file)
index 82ea75b..0000000
+++ /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 (file)
index 1d9e5ed..0000000
+++ /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 (file)
index af139fa..0000000
+++ /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 (file)
index 41b5925..0000000
+++ /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 (file)
index 7ee3b5e..0000000
+++ /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 (file)
index d049f7a..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import {Component} from '@angular/core';
-
-@Component({
-  selector: 'eg-root',
-  template: '<router-outlet></router-outlet>'
-})
-
-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 (file)
index d9d06e3..0000000
+++ /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 (file)
index 58828be..0000000
+++ /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 (file)
index 611797a..0000000
+++ /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<any> {
-
-        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<any>( (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<void> {
-
-        return new Promise<void>((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<void> {
-
-        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<any> {
-        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<void> {
-        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 (file)
index 3f6afc7..0000000
+++ /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 (file)
index 8f46933..0000000
+++ /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 (file)
index b037de1..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-/**
- * 
- * constructor(private net : EgNetService) {
- *   ...
- *   egNet.request(service, method, param1 [, param2, ...])
- *     .subscribe(
- *       (res) => console.log('received one resopnse: ' + res),
- *       (err) => console.error('recived request error: ' + err),
- *       ()    => console.log('request complete')
- *     )
- *   );
- *   ...
- * }
- *
- * Each response is relayed via Observable onNext().  The interface is 
- * the same for streaming and atomic requests.
- */
-import { Injectable, EventEmitter } from '@angular/core';
-import { Observable, Observer } from 'rxjs/Rx';
-import { EgEventService, EgEvent } from './event';
-
-// Global vars from opensrf.js
-// These are availavble at runtime, but are not exported.
-declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
-
-export class EgNetRequest {
-    service    : String;
-    method     : String;
-    params     : any[];
-    observer   : Observer<any>;
-    superseded : Boolean = false;
-    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
-    session?   : any;
-
-    // Last EgEvent encountered by this request.
-    // Most callers will not need to import EgEvent since the parsed
-    // event will be available here.
-    evt: EgEvent;
-
-    constructor(service: String, method: String, params: any[], session?: any) {
-        this.service = service;
-        this.method = method;
-        this.params = params;
-        if (session) {
-            this.session = session;
-        } else {
-            this.session = new OpenSRF.ClientSession(service);
-        }
-    }
-}
-
-@Injectable()
-export class EgNetService {
-
-    permFailed$: EventEmitter<EgNetRequest>;
-    authExpired$: EventEmitter<EgNetRequest>;
-
-    // If true, permission failures are emitted via permFailed$ 
-    // and the active request is marked as superseded.
-    permFailedHasHandler: Boolean = false;
-
-    constructor(
-        private egEvt: EgEventService
-    ) { 
-        this.permFailed$ = new EventEmitter<EgNetRequest>();
-        this.authExpired$ = new EventEmitter<EgNetRequest>();
-    }
-
-    // Standard request call -- Variadic params version
-    request(service: String, method: String, ...params: any[]): Observable<any> {
-        return this.requestWithParamList(service, method, params);
-    }
-
-    // Array params version
-    requestWithParamList(service: String, 
-        method: String, params: any[]): Observable<any> {
-        return this.requestCompiled(
-            new EgNetRequest(service, method, params));
-    }
-
-    requestCompiled(request: EgNetRequest): Observable<any> {
-        return Observable.create(
-            observer => {
-                request.observer = observer;
-                this.sendCompiledRequest(request);
-            }
-        );
-    }
-
-    // Version with pre-compiled EgNetRequest object
-    sendCompiledRequest(request: EgNetRequest): void {
-        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
-        var this_ = this;
-
-        request.session.request({
-            async  : true,
-            method : request.method,
-            params : request.params,
-            oncomplete : function() {
-                // A superseded request will be complete()'ed by the 
-                // superseder at a later time.
-                if (!request.superseded)
-                    request.observer.complete();
-            },
-            onresponse : function(r) {
-                this_.dispatchResponse(request, r.recv().content());
-            },
-            onerror : function(errmsg) {
-                let msg = `${request.method} failed! See server logs. ${errmsg}`;
-                console.error(msg);
-                request.observer.error(msg);
-            },
-            onmethoderror : function(req, statCode, statMsg) { 
-                let msg = 
-                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
-                console.error(msg);
-
-                if (request.service == 'open-ils.pcrud' && statCode == 401) {
-                    // 401 is the PCRUD equivalent of a NO_SESSION event
-                    this_.authExpired$.emit(request);
-                }
-
-                request.observer.error(msg);
-            }
-
-        }).send();
-    }
-
-    // Relay response object to the caller for typical/successful responses.  
-    // Applies special handling to response events that require global attention.
-    private dispatchResponse = function(request, response) {
-        request.evt = this.egEvt.parse(response);
-
-        if (request.evt) {
-            switch(request.evt.textcode) {
-
-                case 'NO_SESSION':
-                    console.debug(`EgNet emitting event: ${request.evt}`);
-                    request.observer.error(request.evt.toString());
-                    this.authExpired$.emit(request);
-                    return;
-
-                case 'PERM_FAILURE':
-                    if (this.permFailedHasHandler) {
-                        console.debug(`EgNet emitting event: ${request.evt}`);
-                        request.superseded = true;
-                        this.permFailed$.emit(request);
-                        return;
-                    }
-            } 
-        }
-
-        // Pass the response to the caller.
-        request.observer.next(response);
-    };
-}
diff --git a/Open-ILS/webby-src/src/app/core/org.ts b/Open-ILS/webby-src/src/app/core/org.ts
deleted file mode 100644 (file)
index 44eddd6..0000000
+++ /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<void> {
-        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 (file)
index 0cee7d3..0000000
+++ /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<EgPcrudResponse>;
-
-    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<EgPcrudContext> {
-        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<EgPcrudResponse> {
-        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<EgPcrudResponse> {
-        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<EgPcrudResponse> {
-        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<EgPcrudResponse> {
-        return this.cud('create', list)
-    }
-    update(list: EgIdlObject[]): Observable<EgPcrudResponse> {
-        return this.cud('update', list)
-    }
-    remove(list: EgIdlObject[]): Observable<EgPcrudResponse> {
-        return this.cud('delete', list)
-    }
-    autoApply(list: EgIdlObject[]): Observable<EgPcrudResponse> { // RENAMED
-        return this.cud('auto',   list)
-    }
-
-    xactClose(): Observable<EgPcrudResponse> {
-        return this.sendRequest(
-            'open-ils.pcrud.transaction.' + this.xactCloseMode,
-            [this.token()]
-        );
-    };
-
-    xactBegin(): Observable<EgPcrudResponse> {
-        return this.sendRequest(
-            'open-ils.pcrud.transaction.begin', [this.token()]
-        );
-    };
-
-    private dispatch(method: string, params: any[]): Observable<EgPcrudResponse> {
-        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<EgPcrudResponse>): Observable<EgPcrudResponse> {
-        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<EgPcrudResponse> {
-
-        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<EgPcrudResponse> {
-
-        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<EgPcrudContext> {
-        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<EgPcrudResponse> {
-        return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps);
-    }
-
-    retrieveAll(fmClass: string, pcrudOps?: any, 
-        reqOps?: EgPcrudReqOps): Observable<EgPcrudResponse> {
-        return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps);
-    }
-
-    search(fmClass: string, search: any, 
-        pcrudOps?: any, reqOps?: EgPcrudReqOps): Observable<EgPcrudResponse> {
-        return this.newContext().search(fmClass, search, pcrudOps, reqOps);
-    }
-
-    create(list: EgIdlObject[]): Observable<EgPcrudResponse> {
-        return this.newContext().create(list);
-    }
-
-    update(list: EgIdlObject[]): Observable<EgPcrudResponse> {
-        return this.newContext().update(list);
-    }
-
-    remove(list: EgIdlObject[]): Observable<EgPcrudResponse> {
-        return this.newContext().remove(list);
-    }
-
-    autoApply(list: EgIdlObject[]): Observable<EgPcrudResponse> { 
-        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 (file)
index e1a879b..0000000
+++ /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<any> {
-        // 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<any> {
-        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<any> {
-        // 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<any> {
-        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<any> {
-        // TODO: route keys appropriately
-        return Promise.resolve(this.removeLocalItem(key));
-    }
-
-    removeLocalItem(key: string): void {
-        window.localStorage.removeItem(key);
-    }
-
-    removeServerItem(key: string): Promise<any> {
-        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 (file)
index 7ffa74b..0000000
+++ /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<Promise<void>> {
-
-    constructor(
-        private router: Router, 
-        private idl: EgIdlService,
-        private org: EgOrgService,
-    ) {}
-
-    resolve(
-        route: ActivatedRouteSnapshot, 
-        state: RouterStateSnapshot): Promise<void> {
-
-        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 (file)
index 7d7e70e..0000000
+++ /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 (file)
index 1a8b6e1..0000000
+++ /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 (file)
index 00f3203..0000000
+++ /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 (file)
index 96f8d24..0000000
+++ /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<void> {
-        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<any> {
-        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<void> {
-
-        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<void> {
-
-        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<void> {
-        // At the moment, we only need facet CMFs.
-        if (Object.keys(this.cmfMap).length) 
-            return Promise.resolve();
-
-        return new Promise((resolve, reject) => {
-            this.pcrud.search('cmf', 
-                {facet_field : 't'}, {}, {atomic : true}
-            ).subscribe(
-                cmfs => {
-                    cmfs.forEach(c => this.cmfMap[c.id()] = c);
-                    resolve();
-                }
-            )
-        });
-    }
-
-
-    /**
-     * Bib record via UNAPI as mods (for now) with holdings summary
-     * and record attributes.
-     */
-    getBibSummary(bibId: number, orgId?: number, depth?: number): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.unapi.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 (file)
index b3c21e5..0000000
+++ /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 (file)
index d7b9101..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-
-<!-- todo disabled -->
-<ng-template #displayTemplate let-r="result">
-{{r.label}}
-</ng-template>
-
-<input type="text" 
-  class="form-control" 
-  [placeholder]="placeholder"
-  [(ngModel)]="selected" 
-  [ngbTypeahead]="filter"
-  [resultTemplate]="displayTemplate"
-  [inputFormatter]="formatter"
-  (selectItem)="orgChanged($event)"
-/>
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 (file)
index 7738215..0000000
+++ /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<EgIdlObject>();
-
-    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<string>): Observable<OrgDisplay[]> => {
-        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 (file)
index 28c2589..0000000
+++ /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<XMLDocument> {
-        // XReq creates an XML document for us.  Seems like the right
-        // tool for the job.
-        let url = this.createUrl(params);
-        return new Promise((resolve, reject) => {
-            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 (file)
index 1c21a8d..0000000
+++ /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 (file)
index 4e4ef09..0000000
+++ /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 (file)
index 114c312..0000000
+++ /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 (file)
index 5b95268..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<div class="row">
-  <div class="col-8 offset-1">
-    <div class="alert alert-warning" *ngIf="removingWs" i18n>
-      Workstation {{removingWs}} is no longer valid.  Removing registration.
-    </div>
-    <div class="alert alert-danger" *ngIf="workstations.length == 0">
-      <span i18n>Please register a workstation.</span>
-    </div>
-
-    <div class="row">
-      <div class="col" i18n>Register a New Workstation For This Browser</div>
-    </div>
-    <div class="row mt-2">
-      <div class="col-2">
-        <eg-org-select 
-          (onChange)="orgOnChange"
-          [hideOrgs]="hideOrgs"
-          [disableOrgs]="disableOrgs"
-          [initialOrg]="initialOrg"
-          [placeholder]="'Owner'" >
-        </eg-org-select>
-      </div>
-      <div class="col-6">
-        <div class="input-group">
-          <input type='text'
-            class='form-control'
-            i18n-title
-            title="Workstation Name"
-            i18n-placeholder
-            placeholder="Workstation Name"
-            [(ngModel)]='newName'/>
-          <div class="input-group-btn">
-            <button class="btn btn-light" (click)="registerWorkstation()">
-              <span i18n>Register</span>
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="row mt-3 pt-3 border border-left-0 border-right-0 border-bottom-0 border-light">
-      <div class="col">
-        <span i18n>Workstations Registered With This Browser</span>
-      </div>
-    </div>
-    <div class="row">
-      <div class="col-6">
-        <select
-          class="form-control"
-          [(ngModel)]="selectedId">
-          <option *ngFor="let ws of workstations" value="{{ws.id}}">
-            {{ws.name}}
-          </option>
-        </select>
-      </div>
-    </div>
-    <div class="row mt-2">
-      <div class="col-md-6">
-        <button i18n class="btn btn-success" 
-          (click)="useNow()" [disabled]="!selected">
-          Use Now
-        </button>
-        <button i18n class="btn btn-light" 
-          (click)="setDefault()" [disabled]="!selected">
-          Mark As Default
-        </button>
-        <button i18n class="btn btn-danger"
-          (click)="removeSelected()"
-          [disabled]="!selected || isRemoving || !canDeleteSelected()">
-          Remove
-        </button>
-      </div>
-    </div>
-  </div>
-</div>
-
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 (file)
index b724dc0..0000000
+++ /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 (file)
index c7051fb..0000000
+++ /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 (file)
index f1ac37e..0000000
+++ /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 (file)
index 508d879..0000000
+++ /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 (file)
index 7bd463a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<!-- top navigation bar -->
-<eg-staff-nav-bar></eg-staff-nav-bar>
-
-<div id='staff-content-container'>
-  <!-- page content -->
-  <router-outlet></router-outlet>
-</div>
-
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 (file)
index 3c90ab0..0000000
+++ /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 (file)
index 7b53d7f..0000000
+++ /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 (file)
index 1596454..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<!-- search form sits atop every catalog page -->
-<eg-catalog-search-form></eg-catalog-search-form>
-
-<!-- search results, record details, etc. -->
-<router-outlet></router-outlet>
-
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 (file)
index a5ca68f..0000000
+++ /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 (file)
index b76cc0b..0000000
+++ /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 (file)
index 625206e..0000000
+++ /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 (file)
index 84e9d8e..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<div class='eg-copies w-100'>
-  <ul class="pagination mb-1">
-    <li class="page-item" [ngClass]="{disabled : pager.offset == 0}">
-      <a class="no-href page-link" 
-        i18n-aria-label aria-label="Start" (click)="firstPage()">
-        <span i18n>Start</span>
-      </a>
-    </li>
-    <li class="page-item" [ngClass]="{disabled : pager.offset == 0}">
-      <a class="no-href page-link" 
-        i18n-aria-label aria-label="Previous" (click)="prevPage()">
-        <span i18n>Previous</span>
-      </a>
-    </li>
-    <!-- note disable logic is incomplete -->
-    <li class="page-item"
-      [ngClass]="{disabled: copies.length < pager.limit}">
-      <a class="no-href page-link" 
-        i18n-aria-label aria-label="Next" (click)="nextPage()">
-        <span i18n>Next</span>
-      </a>
-    </li>
-  </ul>
-  <div class='card tight-card w-100'>
-    <div class="card-header font-weight-bold d-flex bg-info">
-      <div class="flex-1" i18n>Location</div>
-      <div class="flex-1 pl-1" i18n>Call Number / Copy Notes</div>
-      <div class="flex-1 pl-1" i18n>Barcode</div>
-      <div class="flex-1 pl-1" i18n>Shelving Location</div>
-      <div class="flex-1 pl-1" i18n>Circulation Modifier</div>
-      <div class="flex-1 pl-1" i18n>Age Hold Protection</div>
-      <div class="flex-1 pl-1" i18n>Active/Create Date</div>
-      <div class="flex-1 pl-1" i18n>Holdable?</div>
-      <div class="flex-1 pl-1" i18n>Status</div>
-      <div class="flex-1 pl-1" i18n>Due Date</div>
-    </div>
-    <div class="card-body">
-      <ul class="list-group list-group-flush" *ngIf="copies && copies.length">
-        <li class="list-group-item" *ngFor="let copy of copies; let idx = index" 
-          [ngClass]="{'list-group-item-info': (idx % 2) == 1}">
-          <div class="d-flex">
-            <div class="flex-1" i18n>{{orgName(copy.circ_lib)}}</div>
-            <div class="flex-1 pl-1" i18n>
-              {{copy.call_number_prefix_label}}
-              {{copy.call_number_label}}
-              {{copy.call_number_suffix_label}}
-            </div>
-            <div class="flex-1 pl-1" i18n>
-              {{copy.barcode}}
-              <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}" i18n>View</a>
-              | 
-              <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}/edit" i18n>Edit</a>
-            </div>
-            <div class="flex-1 pl-1" i18n>{{copy.copy_location}}</div>
-            <div class="flex-1 pl-1" i18n>{{copy.circ_modifier || ''}}</div>
-            <div class="flex-1 pl-1" i18n>{{copy.age_protect}}</div>
-            <div class="flex-1 pl-1" i18n>
-              {{copy.active_date || copy.create_date | date:'shortDate'}}
-            </div>
-            <div class="flex-1 pl-1">
-              <span *ngIf="holdable(copy)" i18n>Yes</span>
-              <span *ngIf="!holdable(copy)" i18n>No</span>
-            </div>
-            <div class="flex-1 pl-1" i18n>{{copy.copy_status}}</div>
-            <div class="flex-1 pl-1" i18n>{{copy.due_date | date:'shortDate'}}</div>
-          </div>
-        </li>
-      </ul>
-    </div>
-  </div>
-</div>
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 (file)
index f234eba..0000000
+++ /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 (file)
index 0edcded..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<ul class="pagination mb-0" *ngIf="index !== null">
-  <li class="page-item" [ngClass]="{disabled : index == 0}">
-    <a class="no-href page-link" 
-      i18n-aria-label aria-label="Start" (click)="firstRecord()">
-      <span i18n>Start</span>
-    </a>
-  </li>
-  <li class="page-item" [ngClass]="{disabled : index == 0}">
-    <a class="no-href page-link" 
-      i18n-aria-label aria-label="Previous" (click)="prevRecord()">
-      <span i18n>Previous</span>
-    </a>
-  </li>
-  <li class="page-item"
-    [ngClass]="{disabled : index >= searchContext.result.count - 1}">
-    <a class="no-href page-link" 
-      i18n-aria-label aria-label="Next" (click)="nextRecord()">
-      <span i18n>Next</span>
-    </a>
-  </li>
-  <li class="page-item"
-      [ngClass]="{disabled : index >= searchContext.result.count - 1}">
-    <a class="no-href page-link" 
-      i18n-aria-label aria-label="End" (click)="lastRecord()">
-      <span i18n>End</span>
-    </a>
-  </li>
-  <li class="page-item">
-    <a class="no-href page-link" 
-      i18n-aria-label aria-label="Back to Results" (click)="returnToSearch()">
-      <span i18n>
-        Back to Results ({{index + 1}} / {{searchContext.result.count}})
-      </span>
-    </a>
-  </li>
-</ul>
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 (file)
index a7535f6..0000000
+++ /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<void> {
-        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<number> {
-
-        // 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<void> {
-
-        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 (file)
index 127254a..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-
-<div id="staff-catalog-record-container">
-  <div id='staff-catalog-bib-navigation'>
-    <div *ngIf="searchContext.isSearchable()">
-      <eg-catalog-record-pagination [recordId]="recordId">
-      </eg-catalog-record-pagination>
-    </div>
-  </div>
-  <div id='staff-catalog-bib-summary-container' class='mt-1'>
-    <eg-bib-summary [bibSummary]="bibSummary">
-    </eg-bib-summary>
-  </div>
-  <div id='staff-catalog-copies-container' class='mt-3'>
-    <eg-catalog-copies [recordId]="recordId"></eg-catalog-copies>
-  </div>
-</div>
-
-
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 (file)
index 78552eb..0000000
+++ /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 (file)
index 8929d55..0000000
+++ /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<Promise<any[]>> {
-
-    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<any[]> {
-
-        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 (file)
index 188ae30..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<style>
-  .facet-selected {
-    background-color: #DDD;
-  }
-  .card {
-    width: 100%;
-  }
-  .list-group-item {padding: .5rem .75rem .5rem .75rem}
-</style>
-<div *ngIf="searchContext.result.facetData">
-  <div *ngFor="let facetConf of facetConfig.display">
-    <div *ngIf="searchContext.result.facetData[facetConf.facetClass]">
-      <div *ngFor="let name of facetConf.facetOrder">
-        <div class="row"
-          *ngIf="searchContext.result.facetData[facetConf.facetClass][name]">
-          <div class="card mb-2">
-            <h4 class="card-header">
-              {{searchContext.result.facetData[facetConf.facetClass][name].cmfLabel}}
-            </h4>
-            <ul class="list-group list-group-flush">
-              <li class="list-group-item" 
-                [ngClass]="{'facet-selected' :
-                  facetIsApplied(facetConf.facetClass, name, value.value)}"
-                *ngFor="
-                  let value of searchContext.result.facetData[facetConf.facetClass][name].valueList | slice:0:facetConfig.displayCount">
-                <div class="row">
-                  <div class="col-9">
-                    <a class="card-link"
-                      href='javascript:;'
-                      (click)="applyFacet(facetConf.facetClass, name, value.value)">
-                      {{value.value}}
-                    </a>
-                  </div>
-                  <div class="col-3">{{value.count}}</div>
-                </div>
-              </li>
-            </ul>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
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 (file)
index 8101ced..0000000
+++ /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 (file)
index c283ff4..0000000
+++ /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 (file)
index 55b63dd..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!-- 
-Using bare BS pagination instead of ng-bootstrap, which seemed 
-unnecessary given we have to track paging externally anyway.
--->
-<ul class="pagination">
-  <li class="page-item" 
-    [ngClass]="{disabled : searchContext.pager.isFirstPage()}">
-    <a (click)="prevPage()"
-      class="page-link" 
-      i18n-aria-label
-      aria-label="Previous">
-      <span aria-hidden="true">&laquo;</span>
-    </a>
-  </li>
-  <li class="page-item" 
-    *ngFor="let page of searchContext.pager.pageList()"
-    [ngClass]="{active : searchContext.pager.currentPage() == page}">
-    <a class="page-link" (click)="setPage(page)">
-      {{page}} <span class="sr-only" i18n>(current)</span></a>
-  </li>
-  <li class="page-item" 
-    [ngClass]="{disabled : searchContext.pager.isLastPage()}">
-    <a (click)="nextPage()"
-      class="page-link" aria-label="Next" i18n-aria-label>
-      <span aria-hidden="true">&raquo;</span>
-    </a>
-  </li>
-</ul>
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 (file)
index 8dbb4d8..0000000
+++ /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 (file)
index c9a0cd9..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-<!-- 
-  TODO
-  routerLink's
-  egDateFilter's
--->
-
-<div class="col-12 card tight-card mb-2 bg-light">
-  <div class="card-body">
-    <div class="row">
-      <div class="col-1">
-        <!-- TODO router links -->
-        <a href="./cat/catalog/record/{{bibSummary.id}}">
-          <img style="height:80px"
-            src="/opac/extras/ac/jacket/small/r/{{bibSummary.id}}"/>
-        </a>
-      </div>
-      <div class="col-5">
-        <div class="row">
-          <div class="col-12 font-weight-bold">
-            <!-- nbsp allows the column to take shape when no value exists -->
-            <span class="font-weight-light font-italic">
-              #{{index + 1 + searchContext.pager.offset}}
-            </span>
-            <a href="javascript:void(0)"
-              (click)="navigatToRecord(bibSummary.id)">
-              {{bibSummary.title || '&nbsp;'}}
-            </a>
-          </div>
-        </div>
-        <div class="row pt-2">
-          <div class="col-12">
-            <!-- nbsp allows the column to take shape when no value exists -->
-            <a href="javascript:void(0)"
-              (click)="searchAuthor(bibSummary)">
-              {{bibSummary.author || '&nbsp;'}}
-            </a>
-          </div>
-        </div>
-        <div class="row pt-2">
-          <div class="col-12">
-            <span>
-              <img class="pad-right-min"
-                src="/images/format_icons/icon_format/{{bibSummary.ccvms.icon_format.code}}.png"/>
-              <span>{{bibSummary.ccvms.icon_format.label}}</span>
-            </span>
-            <span style='pl-2'>{{bibSummary.edition}}</span>
-            <span style='pl-2'>{{bibSummary.pubdate}}</span>
-          </div>
-        </div>
-      </div>
-      <div class="col-2">
-        <div class="row" [ngClass]="{'pt-2':copyIndex > 0}" 
-          *ngFor="let copyCount of bibSummary.copyCounts; let copyIdx = index">
-          <div class="w-100" *ngIf="copyCount.type == 'staff'">
-            <div class="float-left text-left w-50">
-              <span class="pr-1">
-              {{copyCount.available}} / {{copyCount.count}} items
-              </span>
-            </div>
-            <div class="float-left w-50">
-              @ {{orgName(copyCount.org_unit)}}
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="col-1">
-        <div class="row">
-          <div class="w-100">
-            TCN: {{bibSummary.tcn_value}}
-          </div>
-        </div>
-        <div class="row">
-          <div class="w-100">
-            Holds: {{bibSummary.holdCount}}
-          </div>
-        </div>
-      </div>
-      <div class="col-3">
-        <div class="row">
-          <div class="col-12">
-            <div class="float-right small-text-1">
-              Created {{bibSummary.create_date | date:'shortDate'}} by
-              <!-- creator if fleshed after the initial data set is loaded -->
-              <a *ngIf="bibSummary.creator.usrname" target="_self" 
-                href="./staff/circ/patron/{{bibSummary.creator.id()}}/checkout">
-                  {{bibSummary.creator.usrname()}}
-              </a>
-              <!-- add a spacer pending data to reduce page shuffle -->
-              <span *ngIf="!bibSummary.creator.usrname"> ... </span>
-            </div>
-          </div>
-        </div>
-        <div class="row pt-2">
-          <div class="col-12">
-            <div class="float-right small-text-1">
-              Edited {{bibSummary.edit_date | date:'shortDate'}} by
-              <a *ngIf="bibSummary.editor.usrname" target="_self" 
-                href="./staff/circ/patron/{{bibSummary.editor.id()}}/checkout">
-                  {{bibSummary.editor.usrname()}}
-              </a>
-              <span *ngIf="!bibSummary.editor.usrname"> ... </span>
-            </div>
-          </div>
-        </div>
-        <div class="row pt-2">
-          <div class="col-12">
-            <div class="float-right">
-              <span>
-                <button (click)="placeHold()"
-                  class="btn btn-sm btn-success label-with-material-icon small-text-1">
-                  <span class="material-icons">check</span>
-                  <span i18n>Place Hold</span>
-                </button>
-              </span>
-              <span>
-                <button (click)="addToList()" 
-                  class="btn btn-sm btn-info label-with-material-icon small-text-1">
-                  <span class="material-icons">playlist_add_check</span>
-                  <span i18n>Add to List</span>
-                </button>
-              </span>
-            </div>
-          </div>
-        </div>
-      </div><!-- col -->
-    </div><!-- row -->
-  </div><!-- card-body -->
-</div><!-- card -->
-
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 (file)
index beee4cf..0000000
+++ /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 (file)
index be7c36a..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-
-<div id="staff-catalog-results-container" *ngIf="searchIsDone()">
-  <div class="row">
-    <div class="col-2"><!--match pagination margin-->
-      <h3 i18n>Search Results ({{searchContext.result.count}})</h3>
-    </div>
-    <div class="col-1"></div>
-    <div class="col-9">
-      <div class="float-right">
-                               <eg-catalog-result-pagination></eg-catalog-result-pagination>
-      </div>
-    </div>
-  </div>
-       <div class="row mt-2">
-               <div class="col-2">
-      <eg-catalog-result-facets></eg-catalog-result-facets>
-               </div>
-               <div class="col-10">
-                       <div *ngIf="searchContext.result">
-                               <div *ngFor="let bibSummary of searchContext.result.records; let idx = index">
-          <div *ngIf="bibSummary">
-                                         <eg-catalog-result-record [bibSummary]="bibSummary" [index]="idx">
-                                         </eg-catalog-result-record>
-          </div>
-                               </div>
-                       </div>
-               </div>
-       </div>
-</div>
-
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 (file)
index b87a2cd..0000000
+++ /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 (file)
index 467db52..0000000
+++ /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 (file)
index f67d8fa..0000000
+++ /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 (file)
index 3ee4d21..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-<!--
-TODO focus search input
--->
-<div id='staffcat-search-form' class='pb-2 mb-3'>
-  <div class="row"
-    *ngFor="let q of searchContext.query; let idx = index; trackBy:trackByIdx">
-    <div class="col-9 d-flex flex-row">
-      <div class="flex-1">
-        <div *ngIf="idx == 0">
-          <select class="form-control" [(ngModel)]="searchContext.format">
-            <option value=''>All Formats</option>
-            <option *ngFor="let fmt of ccvmMap.search_format"
-              value="{{fmt.code()}}">{{fmt.value()}}</option>
-          </select>
-        </div>
-        <div *ngIf="idx > 0">
-          <select class="form-control"
-            [(ngModel)]="searchContext.joinOp[idx]">
-            <option value='&&'>And</option>
-            <option value='||'>Or</option>
-          </select>
-        </div>
-      </div>
-      <div class="flex-1 pl-1">
-        <select class="form-control" 
-          [(ngModel)]="searchContext.fieldClass[idx]">
-          <option value='keyword'>Keyword</option>
-          <option value='title'>Title</option>
-          <option value='jtitle'>Journal Title</option>
-          <option value='author'>Author</option>
-          <option value='subject'>Subject</option>
-          <option value='series'>Series</option>
-        </select>
-      </div>
-      <div class="flex-1 pl-1">
-        <select class="form-control" 
-          [(ngModel)]="searchContext.matchOp[idx]">
-          <option value='contains'>Contains</option>
-          <option value='nocontains'>Does not contain</option>
-          <option value='phrase'>Contains phrase</option>
-          <option value='exact'>Matches exactly</option>
-          <option value='starts'>Starts with</option>
-        </select>
-      </div>
-      <div class="flex-2 pl-1">
-        <div class="form-group">
-          <input type="text" class="form-control"
-            TODOfocus-me="searchContext.focus_query[idx]"
-            [(ngModel)]="searchContext.query[idx]"
-            (keyup)="checkEnter($event)"
-            placeholder="Query..."/>
-        </div>
-      </div>
-      <div class="flex-1 pl-1">
-        <button class="btn btn-sm material-icon-button"
-          (click)="addSearchRow(idx + 1)">
-          <span class="material-icons">add_circle_outline</span>
-        </button>
-        <button class="btn btn-sm material-icon-button"
-          [disabled]="searchContext.query.length < 2"
-          (click)="delSearchRow(idx)">
-          <span class="material-icons">remove_circle_outline</span>
-        </button>
-      </div>
-    </div><!-- col -->
-    <div class="col-3">
-      <div *ngIf="idx == 0" class="float-right">
-        <button class="btn btn-success" type="button"
-          [disabled]="searchIsActive()"
-          (click)="searchContext.pager.offset=0;searchByForm()">
-          Search
-        </button>
-        <button class="btn btn-warning" type="button"
-          [disabled]="searchIsActive()"
-          (click)="searchContext.reset()">
-          Clear Form
-        </button>
-        <button class="btn btn-outline-secondary" type="button"
-          *ngIf="!showAdvanced()"
-          [disabled]="searchIsActive()"
-          (click)="showAdvancedSearch=true">
-          More Filters
-        </button>
-        <button class="btn btn-outline-secondary" type="button"
-          *ngIf="showAdvanced()"
-          (click)="showAdvancedSearch=false">
-          Hide Filters
-        </button>
-      </div>
-    </div>
-  </div><!-- row -->
-
-  <div class="row">
-    <div class="col-9 d-flex flex-row">
-      <div class="flex-1">
-        <eg-org-select 
-          (onChange)="orgOnChange($event)"
-          [initialOrg]="searchContext.searchOrg"
-          [placeholder]="'Library'" >
-        </eg-org-select>
-      </div>
-      <div class="flex-3 pl-1">
-        <select class="form-control" [(ngModel)]="searchContext.sort">
-          <option value='' i18n>Sort by Relevance</option>
-          <optgroup label="Sort by Title" i18n-label>
-            <option value='titlesort' i18n>Title: A to Z</option>
-            <option value='titlesort.descending' i18n>Title: Z to A</option>
-          </optgroup>
-          <optgroup label="Sort by Author" i18n-label>
-            <option value='authorsort' i18n>Author: A to Z</option>
-            <option value='authorsort.descending' i18n>Author: Z to A</option>
-          </optgroup>
-          <optgroup label="Sort by Publication Date" i18n-label>
-            <option value='pubdate' i18n>Date: A to Z</option>
-            <option value='pubdate.descending' i18n>Date: Z to A</option>
-          </optgroup>
-          <optgroup label="Sort by Popularity" i18n-label>
-            <option value='popularity' i18n>Most Popular</option>
-            <option value='poprel' i18n>Popularity Adjusted Relevance</option>
-          </optgroup>
-        </select>
-      </div>
-      <div class="flex-2 pl-2 align-self-end">
-        <div class="checkbox">
-          <label>
-            <input type="checkbox" [(ngModel)]="searchContext.available"/>
-            <span i18n>Limit to Available</span>
-          </label>
-        </div>
-      </div>
-      <div class="flex-4 pl-2 align-self-end">
-        <div class="checkbox">
-          <label>
-            <input type="checkbox" [(ngModel)]="searchContext.global"/>
-            <span i18n>Show Results from All Libraries</span>
-          </label>
-        </div>
-      </div>
-      <div class="flex-2 pl-1">
-        <!-- alignment -->
-      </div>
-    </div>
-    <div class="col-3">
-      <div *ngIf="searchIsActive()">
-        <div class="progress">
-          <div class="progress-bar progress-bar-striped active w-100"
-            role="progressbar" aria-valuenow="100" 
-            aria-valuemin="0" aria-valuemax="100">
-            <span class="sr-only" i18n>Searching..</span>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-  <div class="row pt-2" *ngIf="showAdvanced()">
-    <div class="col-2">
-      <select class="form-control"  multiple="true"
-        [(ngModel)]="searchContext.ccvmFilters.item_type">
-        <option value='' i18n>All Item Types</option>
-        <option *ngFor="let itemType of ccvmMap.item_type"
-          value="{{itemType.code()}}">{{itemType.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <select class="form-control" multiple="true"
-        [(ngModel)]="searchContext.ccvmFilters.item_form">
-        <option value='' i18n>All Item Forms</option>
-        <option *ngFor="let itemForm of ccvmMap.item_form"
-          value="{{itemForm.code()}}">{{itemForm.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <select class="form-control" 
-        [(ngModel)]="searchContext.ccvmFilters.item_lang" multiple="true">
-        <option value='' i18n>All Languages</option>
-        <option *ngFor="let lang of ccvmMap.item_lang"
-          value="{{lang.code()}}">{{lang.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <select class="form-control" 
-        [(ngModel)]="searchContext.ccvmFilters.audience" multiple="true">
-        <option value='' i18n>All Audiences</option>
-        <option *ngFor="let audience of ccvmMap.audience"
-          value="{{audience.code()}}">{{audience.value()}}</option>
-      </select>
-    </div>
-  </div>
-  <div class="row pt-2" *ngIf="showAdvanced()">
-    <div class="col-2">
-      <select class="form-control" 
-        [(ngModel)]="searchContext.ccvmFilters.vr_format" multiple="true">
-        <option value='' i18n>All Video Formats</option>
-        <option *ngFor="let vrFormat of ccvmMap.vr_format"
-          value="{{vrFormat.code()}}">{{vrFormat.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <select class="form-control" 
-        [(ngModel)]="searchContext.ccvmFilters.bib_level" multiple="true">
-        <option value='' i18n>All Bib Levels</option>
-        <option *ngFor="let bibLevel of ccvmMap.bib_level"
-          value="{{bibLevel.code()}}">{{bibLevel.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <select class="form-control" 
-        [(ngModel)]="searchContext.ccvmFilters.lit_form" multiple="true">
-        <option value='' i18n>All Literary Forms</option>
-        <option *ngFor="let litForm of ccvmMap.lit_form"
-          value="{{litForm.code()}}">{{litForm.value()}}</option>
-      </select>
-    </div>
-    <div class="col-2">
-      <i>Copy location filter goes here...</i>
-    </div>
-  </div>
-</div>
-
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 (file)
index 94ef0bf..0000000
+++ /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 (file)
index 1f55cb1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<h2 i18n="Barcode Search Header">Search for Patron by Barcode</h2>
-
-<span i18n>Barcode:</span><input type='text' [ngModel]='barcode'/>
-
-<br/>
-<ul>
-    <li *ngFor="let str of strList">{{str}}</li>
-</ul>
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 (file)
index 43d36da..0000000
+++ /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 (file)
index f119697..0000000
+++ /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 (file)
index 2a685f3..0000000
+++ /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 (file)
index 1b0a0f0..0000000
+++ /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 (file)
index 869fe87..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="col-md-4 offset-md-4">
-  <fieldset>
-    <legend i18n>Sign In</legend>
-    <hr/>
-    <form (ngSubmit)="handleSubmit()" #loginForm="ngForm">
-
-      <div class="form-group">
-        <label for="username" i18n>Username</label>
-        <input 
-          type="text" 
-          class="form-control"
-          id="username" 
-          name="username"
-          required
-          i18n-placeholder
-          placeholder="Username" 
-          [(ngModel)]="args.username"/>
-      </div>
-
-      <div class="form-group">
-        <label for="password" i18n>Password</label>
-        <input 
-          type="password" 
-          class="form-control"
-          id="password" 
-          name="password"
-          required
-          i18n-placeholder
-          placeholder="Password" 
-          [(ngModel)]="args.password"/>
-      </div>
-
-      <button type="submit" class="btn btn-light" i18n>Sign in</button>
-    </form>
-  </fieldset>
-</div>
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 (file)
index 64ae6c5..0000000
+++ /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 (file)
index ee4f93e..0000000
+++ /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 (file)
index 859ec7f..0000000
+++ /dev/null
@@ -1,203 +0,0 @@
-<div id="staff-navbar" class="navbar fixed-top navbar-expand navbar-default">
-  <div class="collapse navbar-collapse">
-    <div class="navbar-nav">
-      <div class="nav-item">
-        <a i18n class="nav-link with-material-icon" routerLink="/staff/splash">
-          <span class="material-icons">home</span>
-        </a>
-      </div>
-    </div>
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
-         Search
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/search">
-            <span class="material-icons">person</span>
-            <span i18n>Search for Patrons</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/cat/item/search">
-            <span class="material-icons">assignment</span>
-            <span i18n>Search for Copies by Barcode</span>
-          </a>
-          <a class="dropdown-item" routerLink="/staff/catalog/search">
-            <span class="material-icons">search</span>
-            <span i18n>Search the Catalog</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle class="nav-link dropdown-toggle">
-         <span i18n>Circulation</span>
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/bcsearch">
-            <span class="material-icons">trending_up</span>
-            <span i18n>Check Out</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/checkin/checkin">
-            <span class="material-icons">trending_down</span>
-            <span i18n>Check In</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/checkin/capture">
-            <span class="material-icons">pin_drop</span>
-            <span i18n>Capture Holds</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/holds/pull">
-            <span class="material-icons">view_list</span>
-            <span i18n>Pull List for Hold Requests</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/renew/renew">
-            <span class="material-icons">autorenew</span>
-            <span i18n>Renew Items</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/register">
-            <span class="material-icons">person_add</span>
-            <span i18n>Register Patron</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/last">
-            <span class="material-icons">redo</span>
-            <span i18n>Retrieve Last Patron</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/search?show_recent=1">
-            <span class="material-icons">redo</span>
-            <span i18n>Retrieve Recent Patrons</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/pending/list">
-            <span class="material-icons">thumb_up</span>
-            <span i18n>Pending Patrons</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/bucket/view">
-            <span class="material-icons">list</span>
-            <span i18n>User Buckets</span>
-          </a>
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="/eg/staff/circ/patron/credentials">
-            <span class="material-icons">check_circle</span>
-            <span i18n>Verify Credentials</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/in_house_use/index">
-            <span class="material-icons">playlist_add</span>
-            <span i18n>Record In-House Use</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/circ/holds/shelf">
-            <span class="material-icons">format_list_bulleted</span>
-            <span i18n>Holds Shelf</span>
-          </a>
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="/eg/staff/cat/item/replace_barcode/index">
-            <span class="material-icons">library_books</span>
-            <span i18n>Replace Barcode</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/cat/item/search">
-            <span class="material-icons">question_answer</span>
-            <span i18n>Item Status</span>
-          </a>
-          <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
-            <span class="material-icons">grid_on</span>
-            <span i18n>Scan Item as Missing Pieces</span>
-          </a>
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
-            <span class="material-icons">redo</span>
-            <span i18n>Reprint Last Receipt</span>
-          </a>
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="/eg/staff/offline-interface">
-            <span class="material-icons">signal_wifi_off</span>
-            <span i18n>Offline Circulation</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
-         Cataloging
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item"
-              routerLink="/staff/catalog/search">
-            <span class="material-icons">search</span>
-            <span i18n>Search the Catalog</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
-          Acquisitions
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item"
-              routerLink="/staff/catalog/search">
-            <span class="material-icons">search</span>
-            <span i18n>TODO</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
-          Booking
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item"
-              routerLink="/staff/catalog/search">
-            <span class="material-icons">search</span>
-            <span i18n>TODO</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown">
-        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
-          Administration
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item"
-              routerLink="/staff/catalog/search">
-            <span class="material-icons">search</span>
-            <span i18n>TODO</span>
-          </a>
-        </div>
-      </div>
-    </div>
-
-
-    <div class="navbar-nav mr-auto"></div>
-    <div class="navbar-nav">
-      <span i18n>{{user}} @ {{workstation}}</span>
-    </div>
-    <div class="navbar-nav">
-      <div ngbDropdown class="nav-item dropdown" placement="bottom-right">
-        <a ngbDropdownToggle i18n 
-          i18n-title
-          title="Log out and more..."
-          class="nav-link dropdown-toggle no-caret with-material-icon">
-          <i class="material-icons">list</i>
-        </a>
-        <div class="dropdown-menu" ngbDropdownMenu>
-          <a i18n class="dropdown-item" routerLink="/staff/login">
-            <span class="material-icons">lock_outline</span>
-            <span i18n>Logout</span>
-          </a>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
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 (file)
index 62fb605..0000000
+++ /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 (file)
index 8c23030..0000000
+++ /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<Observable<any>> {
-
-    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<any> {
-
-        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<any>): Promise<void> {
-        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 (file)
index 81c0609..0000000
+++ /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 (file)
index 1d6d167..0000000
+++ /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 (file)
index 6626608..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-
-<div class='eg-bib-summary card tight-card w-100' *ngIf="summary">
-  <div class="card-header d-flex">
-    <div class="font-weight-bold">
-      Record Summary
-    </div>
-    <div class="flex-1"></div>
-    <div>
-      <a class="with-material-icon no-href text-primary" 
-        title="Show More" i18n-title
-        *ngIf="!expandDisplay" (click)="expandDisplay=true">
-        <span class="material-icons">expand_more</span>
-      </a>
-      <a class="with-material-icon no-href text-primary" 
-        title="Show Less" i18n-title
-        *ngIf="expandDisplay" (click)="expandDisplay=false">
-        <span class="material-icons">expand_less</span>
-      </a>
-    </div>
-  </div>
-  <div class="card-body">
-    <ul class="list-group list-group-flush">
-      <li class="list-group-item">
-        <div class="d-flex">
-          <div class="flex-1 font-weight-bold" i18n>Title:</div>
-          <div class="flex-3">{{summary.title}}</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
-          <div class="flex-1">{{summary.edition}}</div>
-          <div class="flex-1 font-weight-bold" i18n>TCN:</div>
-          <div class="flex-1">{{summary.tcn_value}}</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Created By:</div>
-          <div class="flex-1" *ngIf="summary.creator.usrname">
-            {{summary.creator.usrname()}}
-          </div>
-        </div>
-      </li>
-      <li class="list-group-item" *ngIf="expandDisplay">
-        <div class="d-flex">
-          <div class="flex-1 font-weight-bold" i18n>Author:</div>
-          <div class="flex-3">{{summary.author}}</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Pubdate:</div>
-          <div class="flex-1">{{summary.pubdate}}</div>
-          <div class="flex-1 font-weight-bold" i18n>Database ID:</div>
-          <div class="flex-1">{{summary.id}}</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited By:</div>
-          <div class="flex-1" *ngIf="summary.editor.usrname">
-            {{summary.editor.usrname()}}
-          </div>
-        </div>
-      </li>
-      <li class="list-group-item" *ngIf="expandDisplay">
-        <div class="d-flex">
-          <div class="flex-1 font-weight-bold" i18n>Bib Call #:</div>
-          <div class="flex-3">{{summary.callNumber}}</div>
-          <div class="flex-1 font-weight-bold" i18n>Record Owner:</div>
-          <div class="flex-1">TODO</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Created On:</div>
-          <div class="flex-1">{{summary.create_date | date:'shortDate'}}</div>
-          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited On:</div>
-          <div class="flex-1">{{summary.edit_date | date:'shortDate'}}</div>
-        </div>
-      </li>
-    </ul>
-  </div>
-</div>
-
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 (file)
index 877b18a..0000000
+++ /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 (file)
index 4846cc5..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-
-
-<style>
-    /* TODO change BS color scheme so this isn't necessary */
-    .bg-evergreen {
-      background: -webkit-linear-gradient(#00593d, #007a54);
-      background-color: #007a54;
-      color: #fff;
-    }
-</style>
-
-<div class="container">
-
-  <!-- header icon -->
-  <div class="row mb-3">
-    <div class="col-12 text-center">
-      <img src="/images/portal/logo.png"/>
-    </div>
-  </div>
-
-  <div class="row" id="splash-nav">
-    <div class="col-4">
-      <div class="card">
-        <div class="card-header bg-evergreen">
-          <div class="panel-title text-center">Circulation and Patrons</div>
-        </div>
-        <div class="card-body">
-          <div class="list-group">
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/forward.png"/>
-              <a href="/eg/staff/circ/patron/bcsearch">Check Out Items</a>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/back.png"/>
-              <a href="/eg/staff/circ/checkin/index">Check In Items</a>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/retreivepatron.png"/>
-              <a href="/eg/staff/circ/patron/search">Search For Patron By Name</a>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="col-4">
-      <div class="card">
-        <div class="card-header bg-evergreen">
-          <div class="panel-title text-center">Item Search and Cataloging</div>
-        </div>
-        <div class="card-body">
-          <div class="list-group">
-            <div class="list-group-item border-0 p-2">
-              <div class="input-group">
-                <input type="text" class="form-control" 
-                  [(ngModel)]="catSearchQuery"
-                  id='catalog-search-input'
-                  (keyup)="checkEnter($event)"
-                  i18n-placeholder placeholder="Search for...">
-                <span class="input-group-btn">
-                  <button class="btn btn-outline-secondary" 
-                    (click)="searchCatalog()" type="button">
-                    Search
-                  </button>
-                </span>
-                  <!--
-                  <input focus-me="focus_search" 
-                      class="form-control" ng-model="cat_query" type="text" 
-                      ng-keypress="catalog_search($event)"
-                      placeholder="Search catalog for..."/>
-                  <button class='btn btn-light' ng-click="catalog_search()">
-                      Search
-                  </button>
-                  -->
-              </div>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/bucket.png"/>
-              <a href="/eg/staff/cat/bucket/record/">Record Buckets</a>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/bucket.png"/>
-              <a href="/eg/staff/cat/bucket/copy/">Copy Buckets</a>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="col-4">
-      <div class="card">
-        <div class="card-header bg-evergreen">
-          <div class="panel-title text-center">Administration</div>
-        </div>
-        <div class="card-body">
-          <div class="list-group">
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/helpdesk.png"/>
-              <a target="_top" href="http://docs.evergreen-ils.org/">
-                Evergreen Documentation
-              </a>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/helpdesk.png"/>
-              <a target="_top" href="/eg/staff/admin/workstation/index">
-                Workstation Administration
-              </a>
-            </div>
-            <div class="list-group-item border-0 p-2">
-              <img src="/images/portal/reports.png"/>
-              <a target="_top" href="/eg/staff/reporter/legacy/main">
-                Reports
-              </a>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
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 (file)
index e113437..0000000
+++ /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 (file)
index 3ce97cc..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="jumbotron">
-  <h1 i18n class="display-3">Welcome to Webby</h1>
-  <p i18n class="lead">
-    If you see this page, you're probably in good shape...
-  </p>
-  <hr class="my-4"/>
-  <p i18n>
-    But maybe you meant to go to the <a routerLink="/staff/splash">staff page</a>
-    or <a routerLink="/catalog">the catalog.</a>
-  </p>
-</div>
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 (file)
index 398d127..0000000
+++ /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 (file)
index e69de29..0000000
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 (file)
index 3612073..0000000
+++ /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 (file)
index b7f639a..0000000
+++ /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 (file)
index 8081c7c..0000000
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 (file)
index a876726..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-  <meta charset="utf-8">
-  <title i18n="Page Title">AngEG</title>
-  <base href="/webby">
-
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <!-- todo -->
-  <!-- see self-hosting options https://google.github.io/material-design-icons/#icon-font-for-the-web -->
-  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-  <!-- link to bootstrap manually for the time being.  With 
-        ng-bootstrap, we only need the CSS, not the JS -->
-  <link rel="stylesheet" 
-    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" 
-    integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" 
-    crossorigin="anonymous">
-</head>
-<body>
-  <eg-root></eg-root>
-  <script src="/IDL2js"></script>
-  <script src="/js/dojo/opensrf/JSON_v1.js"></script>
-  <script src="/js/dojo/opensrf/opensrf.js"></script>
-  <script src="/js/dojo/opensrf/opensrf_ws.js"></script>
-</body>
-</html>
diff --git a/Open-ILS/webby-src/src/main.ts b/Open-ILS/webby-src/src/main.ts
deleted file mode 100644 (file)
index 08b359c..0000000
+++ /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 (file)
index 20d4075..0000000
+++ /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 (file)
index c580fb0..0000000
+++ /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 (file)
index cd612ee..0000000
+++ /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 (file)
index 39ba8db..0000000
+++ /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 (file)
index 63d89ff..0000000
+++ /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 (file)
index ef5c7bd..0000000
+++ /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 (file)
index 14a504d..0000000
+++ /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 (file)
index c24dc29..0000000
+++ /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
-  }
-}