LP#1775466 Angular(6) base application
authorBill Erickson <berickxx@gmail.com>
Wed, 5 Sep 2018 20:11:41 +0000 (16:11 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 6 Sep 2018 20:56:04 +0000 (16:56 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

208 files changed:
.gitignore
Open-ILS/src/eg2/.editorconfig [new file with mode: 0644]
Open-ILS/src/eg2/.gitignore [new file with mode: 0644]
Open-ILS/src/eg2/CHEAT_SHEET.adoc [new file with mode: 0644]
Open-ILS/src/eg2/angular.json [new file with mode: 0644]
Open-ILS/src/eg2/e2e/app.e2e-spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/e2e/app.po.ts [new file with mode: 0644]
Open-ILS/src/eg2/e2e/tsconfig.e2e.json [new file with mode: 0644]
Open-ILS/src/eg2/karma.conf.js [new file with mode: 0644]
Open-ILS/src/eg2/package-lock.json [new file with mode: 0644]
Open-ILS/src/eg2/package.json [new file with mode: 0644]
Open-ILS/src/eg2/protractor.conf.js [new file with mode: 0644]
Open-ILS/src/eg2/src/app/app.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/app.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/common.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/README [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/auth.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/event.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/event.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/format.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/format.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/idl.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/idl.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/locale.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/net.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/org.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/org.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/pcrud.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/perm.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/server-store.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/store.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/core/store.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/resolver.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/README [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/print/print.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/print/print.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/print/print.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/string/string.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/string/string.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/toast/toast.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/toast/toast.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/toast/toast.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/toast/toast.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/util/audio.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/util/pager.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/about.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/about.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/common.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/common.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/login.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/login.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/nav.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/nav.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/nav.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/resolver.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/sandbox/README [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/README [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/splash.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/splash.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/staff.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/staff.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/staff.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/staff.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/welcome.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/welcome.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/assets/.gitkeep [new file with mode: 0644]
Open-ILS/src/eg2/src/environments/environment.prod.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/environments/environment.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/favicon.ico [new file with mode: 0644]
Open-ILS/src/eg2/src/index.html [new file with mode: 0644]
Open-ILS/src/eg2/src/locale/.gitkeep [new file with mode: 0644]
Open-ILS/src/eg2/src/main.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/polyfills.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/styles.css [new file with mode: 0644]
Open-ILS/src/eg2/src/test.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/test_data/eg_mock.js [new file with mode: 0644]
Open-ILS/src/eg2/src/test_data/idl2js.pl [new file with mode: 0644]
Open-ILS/src/eg2/src/tsconfig.app.json [new file with mode: 0644]
Open-ILS/src/eg2/src/tsconfig.spec.json [new file with mode: 0644]
Open-ILS/src/eg2/src/typings.d.ts [new file with mode: 0644]
Open-ILS/src/eg2/tsconfig.json [new file with mode: 0644]
Open-ILS/src/eg2/tslint.json [new file with mode: 0644]

index 62dea57..bfcf501 100644 (file)
@@ -360,3 +360,4 @@ Open-ILS/web/js/ui/default/staff/build/
 Open-ILS/web/js/ui/default/staff/node_modules/
 Open-ILS/web/js/ui/default/staff/bower_components/
 Open-ILS/web/js/ui/default/common/build/
+Open-ILS/web/eg2/
diff --git a/Open-ILS/src/eg2/.editorconfig b/Open-ILS/src/eg2/.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/src/eg2/.gitignore b/Open-ILS/src/eg2/.gitignore
new file mode 100644 (file)
index 0000000..f59404c
--- /dev/null
@@ -0,0 +1,49 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# ------
+# Added locally...
+# ------
+
+src/test_data/IDL2js.js
+
+# ------
+# 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/src/eg2/CHEAT_SHEET.adoc b/Open-ILS/src/eg2/CHEAT_SHEET.adoc
new file mode 100644 (file)
index 0000000..84f4e5d
--- /dev/null
@@ -0,0 +1,31 @@
+= Evergreen Angular App Cheatsheet
+
+== Basics
+
+[source,sh]
+---------------------------------------------------------------------
+npm update
+npm install
+ng lint             # check code formatting
+npm run test        # unit tests
+ng build --watch    # compile dev mode
+ng build --prod     # compile production mode
+---------------------------------------------------------------------
+
+== OPTIONAL: Adding a Locale 
+
+* Using fr-CA as an example.
+* An fr-CA configuration is supplied by default.  Additional configs
+  must be added where needed.
+* Currently translation builds are only available on --prod build mode.
+* Uncomment the locale lines in eg_vhost.conf and restart apache.
+* TODO: expand docs on package.json file changes required to add locales.
+
+[source,sh]
+---------------------------------------------------------------------
+npm run export-strings
+npm run merge-strings -- fr-CA
+# APPLY TRANSLATIONS TO src/locale/messages.fr-CA.xlf
+npm run build-fr-CA # modify package.json for other locales
+---------------------------------------------------------------------
+
diff --git a/Open-ILS/src/eg2/angular.json b/Open-ILS/src/eg2/angular.json
new file mode 100644 (file)
index 0000000..e50c8db
--- /dev/null
@@ -0,0 +1,155 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "projects",
+  "projects": {
+    "eg": {
+      "root": "",
+      "sourceRoot": "src",
+      "projectType": "application",
+      "architect": {
+        "build": {
+          "builder": "@angular-devkit/build-angular:browser",
+          "options": {
+            "baseHref": "/eg2/en-US",
+            "deployUrl": "/eg2/en-US/",
+            "outputPath": "../../web/eg2/en-US",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "tsConfig": "src/tsconfig.app.json",
+            "polyfills": "src/polyfills.ts",
+            "assets": [
+              "src/assets",
+              "src/favicon.ico"
+            ],
+            "styles": [
+              "src/styles.css"
+            ],
+            "scripts": []
+          },
+          "configurations": {
+            "production": {
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "extractCss": true,
+              "namedChunks": false,
+              "aot": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true,
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ]
+            },
+            "production-fr-CA": {
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "extractCss": true,
+              "namedChunks": false,
+              "aot": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true,
+              "i18nFile": "src/locale/messages.fr-CA.xlf",
+              "i18nFormat": "xlf",
+              "i18nLocale": "fr-CA",
+              "i18nMissingTranslation": "ignore",
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ]
+            }
+          }
+        },
+        "serve": {
+          "builder": "@angular-devkit/build-angular:dev-server",
+          "options": {
+            "browserTarget": "eg:build"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "eg:build:production"
+            }
+          }
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "eg:build"
+          }
+        },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "main": "src/test.ts",
+            "karmaConfig": "./karma.conf.js",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "src/tsconfig.spec.json",
+            "scripts": [],
+            "styles": [
+              "src/styles.css"
+            ],
+            "assets": [
+              "src/assets",
+              "src/favicon.ico"
+            ]
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": [
+              "src/tsconfig.app.json",
+              "src/tsconfig.spec.json"
+            ],
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    },
+    "eg-e2e": {
+      "root": "",
+      "sourceRoot": "",
+      "projectType": "application",
+      "architect": {
+        "e2e": {
+          "builder": "@angular-devkit/build-angular:protractor",
+          "options": {
+            "protractorConfig": "./protractor.conf.js",
+            "devServerTarget": "eg:serve"
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": [
+              "e2e/tsconfig.e2e.json"
+            ],
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    }
+  },
+  "defaultProject": "eg",
+  "schematics": {
+    "@schematics/angular:component": {
+      "prefix": "eg",
+      "styleext": "css"
+    },
+    "@schematics/angular:directive": {
+      "prefix": "eg"
+    }
+  }
+}
diff --git a/Open-ILS/src/eg2/e2e/app.e2e-spec.ts b/Open-ILS/src/eg2/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/src/eg2/e2e/app.po.ts b/Open-ILS/src/eg2/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/src/eg2/e2e/tsconfig.e2e.json b/Open-ILS/src/eg2/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/src/eg2/karma.conf.js b/Open-ILS/src/eg2/karma.conf.js
new file mode 100644 (file)
index 0000000..63982de
--- /dev/null
@@ -0,0 +1,43 @@
+// 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-devkit/build-angular'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('karma-phantomjs-launcher'),
+      require('karma-jasmine-html-reporter'),
+      require('karma-coverage-istanbul-reporter'),
+      require('@angular-devkit/build-angular/plugins/karma')
+    ],
+    client:{
+      clearContext: false // leave Jasmine Spec Runner output visible in browser
+    },
+    coverageIstanbulReporter: {
+      dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
+      fixWebpackSourcePaths: true
+    },
+    angularCli: {
+      environment: 'dev'
+    },
+    reporters: ['progress', 'kjhtml'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['PhantomJS'],
+    singleRun: true,
+    files: [
+      '/openils/lib/javascript/md5.js',
+      '/openils/lib/javascript/JSON_v1.js',
+      '/openils/lib/javascript/opensrf.js',
+      '/openils/lib/javascript/opensrf_ws.js',
+      // mock data for testing only
+      'src/test_data/IDL2js.js',
+      'src/test_data/eg_mock.js',
+    ]
+  });
+};
diff --git a/Open-ILS/src/eg2/package-lock.json b/Open-ILS/src/eg2/package-lock.json
new file mode 100644 (file)
index 0000000..e7ce3aa
--- /dev/null
@@ -0,0 +1,10689 @@
+{
+  "name": "eg",
+  "version": "0.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@angular-devkit/architect": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.7.5.tgz",
+      "integrity": "sha512-zwCpGdx3JDE+Y+LiWh9ErRX+fpFPTRHtEd2PDJmfQsdlIWfjxSR5U9vi3+bSRW2n6IFiH2GCYMS31R64rfMwbg==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular-devkit/build-angular": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.7.5.tgz",
+      "integrity": "sha512-FYd2RigCbvm1i0aM1p+jO2145qm56iPgcW2TK3LBxllWFoz5v+wb086/aDzATG+2ETDZO1uENiVTWu5RSkYcSw==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/build-optimizer": "0.7.5",
+        "@angular-devkit/build-webpack": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "@ngtools/webpack": "6.1.5",
+        "ajv": "~6.4.0",
+        "autoprefixer": "^8.4.1",
+        "circular-dependency-plugin": "^5.0.2",
+        "clean-css": "^4.1.11",
+        "copy-webpack-plugin": "^4.5.2",
+        "file-loader": "^1.1.11",
+        "glob": "^7.0.3",
+        "html-webpack-plugin": "^3.0.6",
+        "istanbul": "^0.4.5",
+        "istanbul-instrumenter-loader": "^3.0.1",
+        "karma-source-map-support": "^1.2.0",
+        "less": "^3.7.1",
+        "less-loader": "^4.1.0",
+        "license-webpack-plugin": "^1.3.1",
+        "loader-utils": "^1.1.0",
+        "mini-css-extract-plugin": "~0.4.0",
+        "minimatch": "^3.0.4",
+        "node-sass": "^4.9.3",
+        "opn": "^5.1.0",
+        "parse5": "^4.0.0",
+        "portfinder": "^1.0.13",
+        "postcss": "^6.0.22",
+        "postcss-import": "^11.1.0",
+        "postcss-loader": "^2.1.5",
+        "postcss-url": "^7.3.2",
+        "raw-loader": "^0.5.1",
+        "rxjs": "^6.0.0",
+        "sass-loader": "~6.0.7",
+        "semver": "^5.5.0",
+        "source-map-loader": "^0.2.3",
+        "source-map-support": "^0.5.0",
+        "stats-webpack-plugin": "^0.6.2",
+        "style-loader": "^0.21.0",
+        "stylus": "^0.54.5",
+        "stylus-loader": "^3.0.2",
+        "tree-kill": "^1.2.0",
+        "uglifyjs-webpack-plugin": "^1.2.5",
+        "url-loader": "^1.0.1",
+        "webpack": "~4.9.2",
+        "webpack-dev-middleware": "^3.1.3",
+        "webpack-dev-server": "^3.1.4",
+        "webpack-merge": "^4.1.2",
+        "webpack-sources": "^1.1.0",
+        "webpack-subresource-integrity": "^1.1.0-rc.4"
+      }
+    },
+    "@angular-devkit/build-optimizer": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.7.5.tgz",
+      "integrity": "sha512-iZYUjNax6epTA4JjnDxhs6MQUtmwM04ZkJkTE3tVc01e80+wJ/f3+ja22BBVul2MsqchOsTUSQIJY3HxbV5aWw==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "source-map": "^0.5.6",
+        "typescript": "~2.9.1",
+        "webpack-sources": "^1.1.0"
+      },
+      "dependencies": {
+        "typescript": {
+          "version": "2.9.2",
+          "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
+          "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
+          "dev": true
+        }
+      }
+    },
+    "@angular-devkit/build-webpack": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.7.5.tgz",
+      "integrity": "sha512-PSkhBwJBLRMiBUGlK15CaVwbU4RzfCdF/GFS/CZSCsA3plLDJy+vXAPrUiuGvqYt/sVKBRavsNaEBCbK1t+1ig==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular-devkit/core": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.7.5.tgz",
+      "integrity": "sha512-r99BZvvuNAqSRm05jXfx0sb3Ip0zvHPtAM6NReXzWPoqaVFpjVUdj/CKA+9HWG/Zt9meG9pEQt/HKK8UXaZDVA==",
+      "dev": true,
+      "requires": {
+        "ajv": "~6.4.0",
+        "chokidar": "^2.0.3",
+        "rxjs": "^6.0.0",
+        "source-map": "^0.5.6"
+      }
+    },
+    "@angular-devkit/schematics": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.7.5.tgz",
+      "integrity": "sha512-E7HkQeJawUskf2gPnogMc+cTdjJ2Iv3QEZOgprh/ExEmBYByWkGDRX5fQOuy8wME8VZqUBvQACZaVkEredn5EA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular/animations": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.1.6.tgz",
+      "integrity": "sha512-fK7onQeVsPgUx/sFcBvcGisuIuxvodzATpoKV9SnsQc6xWE5qsvJRZijrzZIN+Hxy/DgsLaVWRCPn1hG75/D2Q==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/cli": {
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.1.5.tgz",
+      "integrity": "sha512-QNVUSC8mPdiaxubneqNZISy+wec3gwbKoXjcaQ9/45baOnp662j2iJXwiMh6Atn0YUM4u1iUsz1uHyARMtgZmw==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "@schematics/angular": "0.7.5",
+        "@schematics/update": "0.7.5",
+        "opn": "^5.3.0",
+        "rxjs": "^6.0.0",
+        "semver": "^5.1.0",
+        "symbol-observable": "^1.2.0",
+        "yargs-parser": "^10.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "@angular/common": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.6.tgz",
+      "integrity": "sha512-aFQcfCB2vFfNqR6/e6R34JjFpIFmF3zqr6Ubti1PJOsRuhITZHG/qRYIYA7mh1KVkkf0VXC56B+8QzYbdGcKOQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/compiler": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.1.6.tgz",
+      "integrity": "sha512-Z9Og0DVH5krG/xMhfcRJMr5GF2HzqnG3f6Hr+e6d6FB8oehnCX/w9b34zZfVGUWAydAYj32SpXJLE6fQm/ljzA==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/compiler-cli": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.1.6.tgz",
+      "integrity": "sha512-CvgQXuuUJDfmCwnuhZec41aMAiY7nJMSMJxvZWNbFLRiwq+05LiHc7EJYDc6uVQmddWmSqGwfyghjVaiaKJGMg==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^1.4.2",
+        "minimist": "^1.2.0",
+        "reflect-metadata": "^0.1.2",
+        "tsickle": "^0.32.1"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+          "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+          "dev": true,
+          "requires": {
+            "micromatch": "^2.1.5",
+            "normalize-path": "^2.0.0"
+          }
+        },
+        "arr-diff": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+          "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+          "dev": true,
+          "requires": {
+            "arr-flatten": "^1.0.1"
+          }
+        },
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "1.8.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+          "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^1.8.1",
+            "preserve": "^0.2.0",
+            "repeat-element": "^1.1.2"
+          }
+        },
+        "chokidar": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+          "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+          "dev": true,
+          "requires": {
+            "anymatch": "^1.3.0",
+            "async-each": "^1.0.0",
+            "fsevents": "^1.0.0",
+            "glob-parent": "^2.0.0",
+            "inherits": "^2.0.1",
+            "is-binary-path": "^1.0.0",
+            "is-glob": "^2.0.0",
+            "path-is-absolute": "^1.0.0",
+            "readdirp": "^2.0.0"
+          }
+        },
+        "expand-brackets": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+          "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+          "dev": true,
+          "requires": {
+            "is-posix-bracket": "^0.1.0"
+          }
+        },
+        "extglob": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+          "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        },
+        "micromatch": {
+          "version": "2.3.11",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+          "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+          "dev": true,
+          "requires": {
+            "arr-diff": "^2.0.0",
+            "array-unique": "^0.2.1",
+            "braces": "^1.8.2",
+            "expand-brackets": "^0.1.4",
+            "extglob": "^0.3.1",
+            "filename-regex": "^2.0.0",
+            "is-extglob": "^1.0.0",
+            "is-glob": "^2.0.1",
+            "kind-of": "^3.0.2",
+            "normalize-path": "^2.0.1",
+            "object.omit": "^2.0.0",
+            "parse-glob": "^3.0.4",
+            "regex-cache": "^0.4.2"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "@angular/core": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.1.6.tgz",
+      "integrity": "sha512-RFkxNDq8iIfO1SaOuUYqOGD/pujMqifJ9FeVg8M2v7ucW01coXAG0IwqUEMMShQj3GGJGHj+F9BNswN7aD2uvw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/forms": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.1.6.tgz",
+      "integrity": "sha512-6ddk8bhsEtSONctj9PUrEJnTTRL1xHCULaxo2N4GQh5XyV8ScRM0ewOTLcpoL0IU4lgtQmU0VsLWdQvKr3g3Ng==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/http": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.1.6.tgz",
+      "integrity": "sha512-V4qF68tUSsc3cKvQERJmpfXgZSKgxhb67I2jAfmwU9mEH66wh9FNfZ0b0GPV9hXoCulw3POz4ZUwZZ1E6mLy4A==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/language-service": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-6.1.6.tgz",
+      "integrity": "sha512-EEtM6mJtiEgmmm3VjzJxv5BavvonaBFtBrPUcevIW851DtIqn4CS8yDcLcGFiSvSLtAYxRX8dkacPv9vvM1Khg==",
+      "dev": true
+    },
+    "@angular/platform-browser": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.1.6.tgz",
+      "integrity": "sha512-fwI/w+MhdolVJEfdoCSZFarQo+SctG1pNa+V3PxMkXhxnAbv7oWPQdxzdCrhTWdxJTJ5enSfumMmlJEZtg1bag==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/platform-browser-dynamic": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.1.6.tgz",
+      "integrity": "sha512-Ep4vq2ssb1r8XOAw7dJW530vzFKKVY5fj0CYp7VMPfDkwYolEG4TBKQ/ouJkF8n/jdDVFP73+MzU1TLa9/lMQQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/router": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.1.6.tgz",
+      "integrity": "sha512-fOFeOe3uBrSRUYhXdWxHjDPf80eq3ZNCeWfujzfBADtcmiezlO7cxc1v5Eu81t577frU/3z+w8JvmF257p4RZg==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@babel/code-frame": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz",
+      "integrity": "sha1-vXHZsZKvl435FYKdOdQJRFZDmgw=",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "7.0.0-beta.51"
+      }
+    },
+    "@babel/generator": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.51.tgz",
+      "integrity": "sha1-bHV1/952HQdIXgS67cA5LG2eMPY=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.5",
+        "source-map": "^0.5.0",
+        "trim-right": "^1.0.1"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz",
+          "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.51.tgz",
+      "integrity": "sha1-IbSHSiJ8+Z7K/MMKkDAtpaJkBWE=",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "7.0.0-beta.51",
+        "@babel/template": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.51.tgz",
+      "integrity": "sha1-MoGy0EWvlcFyzpGyCCXYXqRnZBE=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.51.tgz",
+      "integrity": "sha1-imw/ZsTSZTUvwHdIT59ugKUauXg=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.51.tgz",
+      "integrity": "sha1-6IRK4loVlcz9QriWI7Q3bKBtIl0=",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.51.tgz",
+      "integrity": "sha1-J87C30Cd9gr1gnDtj2qlVAnqhvY=",
+      "dev": true
+    },
+    "@babel/template": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.51.tgz",
+      "integrity": "sha1-lgKkCuvPNXrpZ34lMu9fyBD1+/8=",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "7.0.0-beta.51",
+        "@babel/parser": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51",
+        "lodash": "^4.17.5"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.51.tgz",
+      "integrity": "sha1-mB2vLOw0emIx06odnhgDsDqqpKg=",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "7.0.0-beta.51",
+        "@babel/generator": "7.0.0-beta.51",
+        "@babel/helper-function-name": "7.0.0-beta.51",
+        "@babel/helper-split-export-declaration": "7.0.0-beta.51",
+        "@babel/parser": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51",
+        "debug": "^3.1.0",
+        "globals": "^11.1.0",
+        "invariant": "^2.2.0",
+        "lodash": "^4.17.5"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "globals": {
+          "version": "11.7.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+          "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/types": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.51.tgz",
+      "integrity": "sha1-2AK3tUO1g2x3iqaReXq/APPZfqk=",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.5",
+        "to-fast-properties": "^2.0.0"
+      },
+      "dependencies": {
+        "to-fast-properties": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+          "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+          "dev": true
+        }
+      }
+    },
+    "@ng-bootstrap/ng-bootstrap": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-3.2.0.tgz",
+      "integrity": "sha512-P+baWRj0Fs2Hm6ZKN2Mtw/xdC6yeuQ0wv2pXGkI231vUb7Jaso28n+9Qc9HSSkfup2Xpm9WVQzhv8AJ4KUOpyA==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@ngtools/webpack": {
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.1.5.tgz",
+      "integrity": "sha512-vrvFFvUqo4hlrLRBTG7a3gsAneitd0/tj2zHsiN97RmefxHSS+3m0pkVw8G3BMAagp2L42AiVfNV4wvYDe+TXA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0",
+        "tree-kill": "^1.0.0",
+        "webpack-sources": "^1.1.0"
+      }
+    },
+    "@schematics/angular": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.7.5.tgz",
+      "integrity": "sha512-NrtvFwHCoWon8KInsvA1jdPu4pVJGa8GAWM/jqnE7HpwPwM7hMML08lV0P8r3NX5t2/i0CKvfp4AAEr5MXorEQ==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "typescript": ">=2.6.2 <2.10"
+      }
+    },
+    "@schematics/update": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.7.5.tgz",
+      "integrity": "sha512-pwNkXGtlzyCV6tsTPe8AgUuMCkmubcz94zgL6pSMdEe122yXBcKnr/PKqG9QzD/gGwmOcHUE9EWcuRtU5kdFpA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "npm-registry-client": "^8.5.1",
+        "rxjs": "^6.0.0",
+        "semver": "^5.3.0",
+        "semver-intersect": "^1.1.2"
+      }
+    },
+    "@types/jasmine": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz",
+      "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==",
+      "dev": true
+    },
+    "@types/jasminewd2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz",
+      "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==",
+      "dev": true,
+      "requires": {
+        "@types/jasmine": "*"
+      }
+    },
+    "@types/node": {
+      "version": "8.9.5",
+      "resolved": "http://registry.npmjs.org/@types/node/-/node-8.9.5.tgz",
+      "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==",
+      "dev": true
+    },
+    "@types/q": {
+      "version": "0.0.32",
+      "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+      "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
+      "dev": true
+    },
+    "@types/selenium-webdriver": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.10.tgz",
+      "integrity": "sha512-ikB0JHv6vCR1KYUQAzTO4gi/lXLElT4Tx+6De2pc/OZwizE9LRNiTa+U8TBFKBD/nntPnr/MPSHSnOTybjhqNA==",
+      "dev": true
+    },
+    "@types/xmldom": {
+      "version": "0.1.29",
+      "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.29.tgz",
+      "integrity": "sha1-xEKLDKhtO4gUdXJv2UmAs4onw4E=",
+      "dev": true
+    },
+    "@webassemblyjs/ast": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.4.3.tgz",
+      "integrity": "sha512-S6npYhPcTHDYe9nlsKa9CyWByFi8Vj8HovcAgtmMAQZUOczOZbQ8CnwMYKYC5HEZzxEE+oY0jfQk4cVlI3J59Q==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "debug": "^3.1.0",
+        "webassemblyjs": "1.4.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.4.3.tgz",
+      "integrity": "sha512-3zTkSFswwZOPNHnzkP9ONq4bjJSeKVMcuahGXubrlLmZP8fmTIJ58dW7h/zOVWiFSuG2em3/HH3BlCN7wyu9Rw==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-buffer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.4.3.tgz",
+      "integrity": "sha512-e8+KZHh+RV8MUvoSRtuT1sFXskFnWG9vbDy47Oa166xX+l0dD5sERJ21g5/tcH8Yo95e9IN3u7Jc3NbhnUcSkw==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/helper-code-frame": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.4.3.tgz",
+      "integrity": "sha512-9FgHEtNsZQYaKrGCtsjswBil48Qp1agrzRcPzCbQloCoaTbOXLJ9IRmqT+uEZbenpULLRNFugz3I4uw18hJM8w==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/wast-printer": "1.4.3"
+      }
+    },
+    "@webassemblyjs/helper-fsm": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.4.3.tgz",
+      "integrity": "sha512-JINY76U+702IRf7ePukOt037RwmtH59JHvcdWbTTyHi18ixmQ+uOuNhcdCcQHTquDAH35/QgFlp3Y9KqtyJsCQ==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.4.3.tgz",
+      "integrity": "sha512-I7bS+HaO0K07Io89qhJv+z1QipTpuramGwUSDkwEaficbSvCcL92CUZEtgykfNtk5wb0CoLQwWlmXTwGbNZUeQ==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-section": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.4.3.tgz",
+      "integrity": "sha512-p0yeeO/h2r30PyjnJX9xXSR6EDcvJd/jC6xa/Pxg4lpfcNi7JUswOpqDToZQ55HMMVhXDih/yqkaywHWGLxqyQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/leb128": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.4.3.tgz",
+      "integrity": "sha512-4u0LJLSPzuRDWHwdqsrThYn+WqMFVqbI2ltNrHvZZkzFPO8XOZ0HFQ5eVc4jY/TNHgXcnwrHjONhPGYuuf//KQ==",
+      "dev": true,
+      "requires": {
+        "leb": "^0.3.0"
+      }
+    },
+    "@webassemblyjs/validation": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/validation/-/validation-1.4.3.tgz",
+      "integrity": "sha512-R+rRMKfhd9mq0rj2mhU9A9NKI2l/Rw65vIYzz4lui7eTKPcCu1l7iZNi4b9Gen8D42Sqh/KGiaQNk/x5Tn/iBQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wasm-edit": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.4.3.tgz",
+      "integrity": "sha512-qzuwUn771PV6/LilqkXcS0ozJYAeY/OKbXIWU3a8gexuqb6De2p4ya/baBeH5JQ2WJdfhWhSvSbu86Vienttpw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/helper-wasm-section": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "@webassemblyjs/wasm-opt": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "@webassemblyjs/wast-printer": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/wasm-gen": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.4.3.tgz",
+      "integrity": "sha512-eR394T8dHZfpLJ7U/Z5pFSvxl1L63JdREebpv9gYc55zLhzzdJPAuxjBYT4XqevUdW67qU2s0nNA3kBuNJHbaQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/leb128": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wasm-opt": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.4.3.tgz",
+      "integrity": "sha512-7Gp+nschuKiDuAL1xmp4Xz0rgEbxioFXw4nCFYEmy+ytynhBnTeGc9W9cB1XRu1w8pqRU2lbj2VBBA4cL5Z2Kw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/wasm-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.4.3.tgz",
+      "integrity": "sha512-KXBjtlwA3BVukR/yWHC9GF+SCzBcgj0a7lm92kTOaa4cbjaTaa47bCjXw6cX4SGQpkncB9PU2hHGYVyyI7wFRg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/leb128": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "webassemblyjs": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wast-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.4.3.tgz",
+      "integrity": "sha512-QhCsQzqV0CpsEkRYyTzQDilCNUZ+5j92f+g35bHHNqS22FppNTywNFfHPq8ZWZfYCgbectc+PoghD+xfzVFh1Q==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/floating-point-hex-parser": "1.4.3",
+        "@webassemblyjs/helper-code-frame": "1.4.3",
+        "@webassemblyjs/helper-fsm": "1.4.3",
+        "long": "^3.2.0",
+        "webassemblyjs": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wast-printer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.4.3.tgz",
+      "integrity": "sha512-EgXk4anf8jKmuZJsqD8qy5bz2frEQhBvZruv+bqwNoLWUItjNSFygk8ywL3JTEz9KtxTlAmqTXNrdD1d9gNDtg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "long": "^3.2.0"
+      }
+    },
+    "abbrev": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+      "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.18",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz",
+      "integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==",
+      "dev": true
+    },
+    "acorn-dynamic-import": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
+      "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==",
+      "dev": true,
+      "requires": {
+        "acorn": "^5.0.0"
+      }
+    },
+    "adm-zip": {
+      "version": "0.4.11",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz",
+      "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+      "dev": true
+    },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz",
+      "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^1.0.0",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.3.0",
+        "uri-js": "^3.0.2"
+      }
+    },
+    "ajv-errors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz",
+      "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=",
+      "dev": true
+    },
+    "ajv-keywords": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+      "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+      "dev": true
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "dev": true
+    },
+    "ansi-colors": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.0.5.tgz",
+      "integrity": "sha512-VVjWpkfaphxUBFarydrQ3n26zX5nIK7hcbT3/ielrvwDDyBBjuh2vuSw1P9zkPq0cfqvdw7lkYHnu+OLSfIBsg==",
+      "dev": true
+    },
+    "ansi-html": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
+      "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+      "dev": true,
+      "requires": {
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      }
+    },
+    "app-root-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz",
+      "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=",
+      "dev": true
+    },
+    "append-transform": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz",
+      "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==",
+      "dev": true,
+      "requires": {
+        "default-require-extensions": "^2.0.0"
+      }
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+      "dev": true
+    },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "dev": true,
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^2.0.6"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-flatten": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
+      "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
+      "dev": true
+    },
+    "array-slice": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+      "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+      "dev": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+      "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+      "dev": true,
+      "optional": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true
+    },
+    "async": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+      "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+      "dev": true
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true,
+      "optional": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "8.6.5",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.5.tgz",
+      "integrity": "sha512-PLWJN3Xo/rycNkx+mp8iBDMTm3FeWe4VmYaZDSqL5QQB9sLsQkG5k8n+LNDFnhh9kdq2K+egL/icpctOmDHwig==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^3.2.8",
+        "caniuse-lite": "^1.0.30000864",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^6.0.23",
+        "postcss-value-parser": "^3.2.3"
+      }
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+      "dev": true
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+      "dev": true,
+      "requires": {
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "detect-indent": "^4.0.0",
+        "jsesc": "^1.3.0",
+        "lodash": "^4.17.4",
+        "source-map": "^0.5.7",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "debug": "^2.6.8",
+        "globals": "^9.18.0",
+        "invariant": "^2.2.2",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+      "dev": true
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "dev": true,
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "big.js": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+      "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+      "dev": true
+    },
+    "binary-extensions": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz",
+      "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
+      "dev": true
+    },
+    "blob": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+      "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=",
+      "dev": true
+    },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "inherits": "~2.0.0"
+      }
+    },
+    "blocking-proxy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
+      "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "bluebird": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz",
+      "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==",
+      "dev": true
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "dev": true
+    },
+    "body-parser": {
+      "version": "1.18.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.1",
+        "http-errors": "~1.6.2",
+        "iconv-lite": "0.4.19",
+        "on-finished": "~2.3.0",
+        "qs": "6.5.1",
+        "raw-body": "2.3.2",
+        "type-is": "~1.6.15"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+          "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+          "dev": true
+        }
+      }
+    },
+    "bonjour": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+      "dev": true,
+      "requires": {
+        "array-flatten": "^2.1.0",
+        "deep-equal": "^1.0.1",
+        "dns-equal": "^1.0.0",
+        "dns-txt": "^2.0.2",
+        "multicast-dns": "^6.0.1",
+        "multicast-dns-service-types": "^1.1.0"
+      }
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+      "dev": true
+    },
+    "bootstrap-css-only": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/bootstrap-css-only/-/bootstrap-css-only-4.1.1.tgz",
+      "integrity": "sha512-I/zU5T/KANTy+NNmsNC4lESiHWrdLtvgp9gutOVqbDKY0d4rycmX9fUp1jkFZ0vNd6dhY8oxY9j8RWZkjHVAuA=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "^1.0.4",
+        "browserify-des": "^1.0.0",
+        "evp_bytestokey": "^1.0.0"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+      "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "des.js": "^1.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.1",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.2",
+        "elliptic": "^6.0.0",
+        "inherits": "^2.0.1",
+        "parse-asn1": "^5.0.0"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
+    "browserslist": {
+      "version": "3.2.8",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+      "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30000844",
+        "electron-to-chromium": "^1.3.47"
+      }
+    },
+    "browserstack": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.1.tgz",
+      "integrity": "sha512-O8VMT64P9NOLhuIoD4YngyxBURefaSdR4QdhG8l6HZ9VxtU7jc3m6jLufFwKA5gaf7fetfB2TnRJnMxyob+heg==",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "^2.2.1"
+      }
+    },
+    "buffer": {
+      "version": "4.9.1",
+      "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+      "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
+      }
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
+      "dev": true
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
+      "dev": true
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "buffer-indexof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+      "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
+      "dev": true
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "builtins": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+      "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "cacache": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+      "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.1",
+        "chownr": "^1.0.1",
+        "glob": "^7.1.2",
+        "graceful-fs": "^4.1.11",
+        "lru-cache": "^4.1.1",
+        "mississippi": "^2.0.0",
+        "mkdirp": "^0.5.1",
+        "move-concurrently": "^1.0.1",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^2.6.2",
+        "ssri": "^5.2.4",
+        "unique-filename": "^1.1.0",
+        "y18n": "^4.0.0"
+      }
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+      "dev": true
+    },
+    "camel-case": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+      "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0",
+        "upper-case": "^1.1.1"
+      }
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true,
+      "optional": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^2.0.0",
+        "map-obj": "^1.0.0"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30000884",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000884.tgz",
+      "integrity": "sha512-ibROerckpTH6U5zReSjbaitlH4gl5V4NWNCBzRNCa3GEDmzzkfStk+2k5mO4ZDM6pwtdjbZ3hjvsYhPGVLWgNw==",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      }
+    },
+    "chokidar": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
+      "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+      "dev": true,
+      "requires": {
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.0",
+        "braces": "^2.3.0",
+        "fsevents": "^1.2.2",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.1",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "lodash.debounce": "^4.0.8",
+        "normalize-path": "^2.1.1",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.0.0",
+        "upath": "^1.0.5"
+      }
+    },
+    "chownr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
+      "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
+      "dev": true
+    },
+    "chrome-trace-event": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-0.1.3.tgz",
+      "integrity": "sha512-sjndyZHrrWiu4RY7AkHgjn80GfAM2ZSzUkZLV/Js59Ldmh6JDThf0SUmOHU53rFu2rVxxfCzJ30Ukcfch3Gb/A==",
+      "dev": true
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "circular-dependency-plugin": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz",
+      "integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==",
+      "dev": true
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "clean-css": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+      "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+      "dev": true,
+      "requires": {
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wrap-ansi": "^2.0.0"
+      }
+    },
+    "clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+      "dev": true
+    },
+    "clone-deep": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
+      "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
+      "dev": true,
+      "requires": {
+        "for-own": "^1.0.0",
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.0",
+        "shallow-clone": "^1.0.0"
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "codelyzer": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz",
+      "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==",
+      "dev": true,
+      "requires": {
+        "app-root-path": "^2.0.1",
+        "css-selector-tokenizer": "^0.7.0",
+        "cssauron": "^1.4.0",
+        "semver-dsl": "^1.0.1",
+        "source-map": "^0.5.6",
+        "sprintf-js": "^1.0.3"
+      }
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "colors": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+      "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+      "dev": true
+    },
+    "combine-lists": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
+      "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.5.0"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+      "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.17.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+      "dev": true
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "compare-versions": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz",
+      "integrity": "sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg==",
+      "dev": true
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+      "dev": true
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "dev": true
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+      "dev": true
+    },
+    "compressible": {
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz",
+      "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=",
+      "dev": true,
+      "requires": {
+        "mime-db": ">= 1.34.0 < 2"
+      }
+    },
+    "compression": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz",
+      "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "bytes": "3.0.0",
+        "compressible": "~2.0.14",
+        "debug": "2.6.9",
+        "on-headers": "~1.0.1",
+        "safe-buffer": "5.1.2",
+        "vary": "~1.1.2"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "~1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "finalhandler": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+          "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+          "dev": true,
+          "requires": {
+            "debug": "2.6.9",
+            "encodeurl": "~1.0.1",
+            "escape-html": "~1.0.3",
+            "on-finished": "~2.3.0",
+            "parseurl": "~1.3.2",
+            "statuses": "~1.3.1",
+            "unpipe": "~1.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+          "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+          "dev": true
+        }
+      }
+    },
+    "connect-history-api-fallback": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz",
+      "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=",
+      "dev": true
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "^0.1.4"
+      }
+    },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+      "dev": true
+    },
+    "copy-concurrently": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+      "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "fs-write-stream-atomic": "^1.0.8",
+        "iferr": "^0.1.5",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.0"
+      }
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true
+    },
+    "copy-webpack-plugin": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz",
+      "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "globby": "^7.1.1",
+        "is-glob": "^4.0.0",
+        "loader-utils": "^1.1.0",
+        "minimatch": "^3.0.4",
+        "p-limit": "^1.0.0",
+        "serialize-javascript": "^1.4.0"
+      }
+    },
+    "core-js": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "cosmiconfig": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz",
+      "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==",
+      "dev": true,
+      "requires": {
+        "is-directory": "^0.3.1",
+        "js-yaml": "^3.9.0",
+        "parse-json": "^4.0.0",
+        "require-from-string": "^2.0.1"
+      },
+      "dependencies": {
+        "parse-json": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+          "dev": true,
+          "requires": {
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1"
+          }
+        }
+      }
+    },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "elliptic": "^6.0.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cross-spawn": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+      "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "lru-cache": "^4.0.1",
+        "which": "^1.2.9"
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "^1.0.0",
+        "browserify-sign": "^4.0.0",
+        "create-ecdh": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.0",
+        "diffie-hellman": "^5.0.0",
+        "inherits": "^2.0.1",
+        "pbkdf2": "^3.0.3",
+        "public-encrypt": "^4.0.0",
+        "randombytes": "^2.0.0",
+        "randomfill": "^1.0.3"
+      }
+    },
+    "css-parse": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
+      "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=",
+      "dev": true
+    },
+    "css-select": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+      "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0",
+        "css-what": "2.1",
+        "domutils": "1.5.1",
+        "nth-check": "~1.0.1"
+      }
+    },
+    "css-selector-tokenizer": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz",
+      "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=",
+      "dev": true,
+      "requires": {
+        "cssesc": "^0.1.0",
+        "fastparse": "^1.1.1",
+        "regexpu-core": "^1.0.0"
+      }
+    },
+    "css-what": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
+      "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=",
+      "dev": true
+    },
+    "cssauron": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
+      "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
+      "dev": true,
+      "requires": {
+        "through": "X.X.X"
+      }
+    },
+    "cssesc": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
+      "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
+      "dev": true
+    },
+    "cuint": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
+      "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
+      "dev": true
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "^1.0.1"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+      "dev": true
+    },
+    "cyclist": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
+      "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
+      "dev": true
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true
+    },
+    "deep-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
+      "dev": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "default-gateway": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
+      "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==",
+      "dev": true,
+      "requires": {
+        "execa": "^0.10.0",
+        "ip-regex": "^2.1.0"
+      }
+    },
+    "default-require-extensions": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
+      "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
+      "dev": true,
+      "requires": {
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        }
+      }
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "del": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
+      "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
+      "dev": true,
+      "requires": {
+        "globby": "^6.1.0",
+        "is-path-cwd": "^1.0.0",
+        "is-path-in-cwd": "^1.0.0",
+        "p-map": "^1.1.1",
+        "pify": "^3.0.0",
+        "rimraf": "^2.2.8"
+      },
+      "dependencies": {
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "dev": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "detect-node": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+      "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
+      "dev": true
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+      "dev": true
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "miller-rabin": "^4.0.0",
+        "randombytes": "^2.0.0"
+      }
+    },
+    "dir-glob": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
+      "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "path-type": "^3.0.0"
+      }
+    },
+    "dns-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+      "dev": true
+    },
+    "dns-packet": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
+      "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+      "dev": true,
+      "requires": {
+        "ip": "^1.1.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "dns-txt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+      "dev": true,
+      "requires": {
+        "buffer-indexof": "^1.0.0"
+      }
+    },
+    "dom-converter": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz",
+      "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=",
+      "dev": true,
+      "requires": {
+        "utila": "~0.3"
+      },
+      "dependencies": {
+        "utila": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz",
+          "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=",
+          "dev": true
+        }
+      }
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "dev": true,
+      "requires": {
+        "custom-event": "~1.0.0",
+        "ent": "~2.2.0",
+        "extend": "^3.0.0",
+        "void-elements": "^2.0.0"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "~1.1.1",
+        "entities": "~1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
+          "dev": true
+        }
+      }
+    },
+    "domain-browser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+      "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+      "dev": true
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz",
+      "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+      "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "duplexify": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz",
+      "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "ejs": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
+      "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
+      "dev": true
+    },
+    "electron-to-chromium": {
+      "version": "1.3.62",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.62.tgz",
+      "integrity": "sha512-x09ndL/Gjnuk3unlAyoGyUg3wbs4w/bXurgL7wL913vXHAOWmMhrLf1VNGRaMLngmadd5Q8gsV9BFuIr6rP+Xg==",
+      "dev": true
+    },
+    "elliptic": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+      "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      }
+    },
+    "emojis-list": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+      "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "dev": true,
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz",
+      "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.3",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "2.3.3",
+        "engine.io-parser": "1.3.2",
+        "ws": "1.1.2"
+      },
+      "dependencies": {
+        "accepts": {
+          "version": "1.3.3",
+          "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
+          "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
+          "dev": true,
+          "requires": {
+            "mime-types": "~2.1.11",
+            "negotiator": "0.6.1"
+          }
+        },
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz",
+      "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "2.3.3",
+        "engine.io-parser": "1.3.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parsejson": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "1.1.2",
+        "xmlhttprequest-ssl": "1.5.3",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz",
+      "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=",
+      "dev": true,
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "0.0.6",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.4",
+        "has-binary": "0.1.7",
+        "wtf-8": "1.0.0"
+      }
+    },
+    "enhanced-resolve": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
+      "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "memory-fs": "^0.4.0",
+        "tapable": "^1.0.0"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+      "dev": true
+    },
+    "errno": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+      "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "dev": true,
+      "requires": {
+        "prr": "~1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
+      "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.1.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.1",
+        "is-callable": "^1.1.3",
+        "is-regex": "^1.0.4"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
+      "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.1",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.1"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
+      "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "escodegen": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+      "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=",
+      "dev": true,
+      "requires": {
+        "esprima": "^2.7.1",
+        "estraverse": "^1.9.1",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.2.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+          "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "eslint-scope": {
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+      "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+          "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+          "dev": true
+        }
+      }
+    },
+    "esprima": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+      "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+      "dev": true
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+          "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+          "dev": true
+        }
+      }
+    },
+    "estraverse": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+      "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "eventemitter3": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
+      "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "dev": true
+    },
+    "eventsource": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz",
+      "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
+      "dev": true,
+      "requires": {
+        "original": ">=0.0.5"
+      }
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "execa": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
+      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.0",
+        "get-stream": "^3.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      },
+      "dependencies": {
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        }
+      }
+    },
+    "exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+      "dev": true
+    },
+    "expand-braces": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
+      "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
+      "dev": true,
+      "requires": {
+        "array-slice": "^0.2.3",
+        "array-unique": "^0.2.1",
+        "braces": "^0.1.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
+          "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^0.1.0"
+          }
+        },
+        "expand-range": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
+          "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
+          "dev": true,
+          "requires": {
+            "is-number": "^0.1.1",
+            "repeat-string": "^0.2.2"
+          }
+        },
+        "is-number": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
+          "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
+          "dev": true
+        },
+        "repeat-string": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
+          "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
+          "dev": true
+        }
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "^2.1.0"
+      },
+      "dependencies": {
+        "fill-range": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+          "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+          "dev": true,
+          "requires": {
+            "is-number": "^2.1.0",
+            "isobject": "^2.0.0",
+            "randomatic": "^3.0.0",
+            "repeat-element": "^1.1.2",
+            "repeat-string": "^1.5.2"
+          }
+        },
+        "is-number": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+          "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+          "dev": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          }
+        },
+        "isobject": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+          "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+          "dev": true,
+          "requires": {
+            "isarray": "1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "express": {
+      "version": "4.16.3",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
+      "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.2",
+        "content-disposition": "0.5.2",
+        "content-type": "~1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.1.1",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.3",
+        "qs": "6.5.1",
+        "range-parser": "~1.2.0",
+        "safe-buffer": "5.1.1",
+        "send": "0.16.2",
+        "serve-static": "1.13.2",
+        "setprototypeof": "1.1.0",
+        "statuses": "~1.4.0",
+        "type-is": "~1.6.16",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "array-flatten": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+          "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.5.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+          "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+          "dev": true
+        },
+        "safe-buffer": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+          "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+          "dev": true
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "dev": true,
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "extract-zip": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
+      "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
+      "dev": true,
+      "requires": {
+        "concat-stream": "1.6.2",
+        "debug": "2.6.9",
+        "mkdirp": "0.5.1",
+        "yauzl": "2.4.1"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastparse": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz",
+      "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=",
+      "dev": true
+    },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "dev": true,
+      "requires": {
+        "websocket-driver": ">=0.5.1"
+      }
+    },
+    "fd-slicer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+      "dev": true,
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "file-loader": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
+      "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "schema-utils": "^0.4.5"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "dev": true
+    },
+    "fileset": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
+      "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.3",
+        "minimatch": "^3.0.3"
+      }
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.4.0",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "find-cache-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
+      "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+      "dev": true,
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^1.0.0",
+        "pkg-dir": "^2.0.0"
+      }
+    },
+    "find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "dev": true,
+      "requires": {
+        "locate-path": "^2.0.0"
+      }
+    },
+    "flush-write-stream": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
+      "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.4"
+      }
+    },
+    "follow-redirects": {
+      "version": "1.5.7",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz",
+      "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.1"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+      "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+      "dev": true
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "dev": true,
+      "requires": {
+        "null-check": "^1.0.0"
+      }
+    },
+    "fs-extra": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz",
+      "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^2.1.0",
+        "klaw": "^1.0.0"
+      }
+    },
+    "fs-write-stream-atomic": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+      "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "iferr": "^0.1.5",
+        "imurmurhash": "^0.1.4",
+        "readable-stream": "1 || 2"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
+      "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.9.2",
+        "node-pre-gyp": "^0.10.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-extend": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.21",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": "^2.1.0"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "dev": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "dev": true
+        },
+        "minipass": {
+          "version": "2.2.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "safe-buffer": "^5.1.1",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^2.1.2",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.10.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.0",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.1.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.1.10",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.7",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.5.1",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.0.5"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.5.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.0.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.2.4",
+            "minizlib": "^1.1.0",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.1",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true
+        }
+      }
+    },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "inherits": "~2.0.0",
+        "mkdirp": ">=0.5 0",
+        "rimraf": "2"
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.0.3",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.0",
+        "object-assign": "^4.1.0",
+        "signal-exit": "^3.0.0",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wide-align": "^1.1.0"
+      }
+    },
+    "gaze": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+      "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "globule": "^1.0.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+      "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+      "dev": true
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "dev": true,
+      "requires": {
+        "glob-parent": "^2.0.0",
+        "is-glob": "^2.0.0"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        }
+      }
+    },
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+      "dev": true
+    },
+    "globby": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
+      "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
+      "dev": true,
+      "requires": {
+        "array-union": "^1.0.1",
+        "dir-glob": "^2.0.0",
+        "glob": "^7.1.2",
+        "ignore": "^3.3.5",
+        "pify": "^3.0.0",
+        "slash": "^1.0.0"
+      }
+    },
+    "globule": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+      "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "~7.1.1",
+        "lodash": "~4.17.10",
+        "minimatch": "~3.0.2"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+      "dev": true
+    },
+    "handle-thing": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
+      "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
+      "dev": true
+    },
+    "handlebars": {
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
+      "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.5.0",
+        "optimist": "^0.6.1",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
+      "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^5.3.0",
+        "har-schema": "^2.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        }
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-binary": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz",
+      "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "dev": true,
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
+      "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "hasha": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
+      "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
+      "dev": true,
+      "requires": {
+        "is-stream": "^1.0.1",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "obuf": "^1.0.0",
+        "readable-stream": "^2.0.1",
+        "wbuf": "^1.1.0"
+      }
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "dev": true
+    },
+    "html-minifier": {
+      "version": "3.5.20",
+      "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.20.tgz",
+      "integrity": "sha512-ZmgNLaTp54+HFKkONyLFEfs5dd/ZOtlquKaTnqIWFmx3Av5zG6ZPcV2d0o9XM2fXOTxxIf6eDcwzFFotke/5zA==",
+      "dev": true,
+      "requires": {
+        "camel-case": "3.0.x",
+        "clean-css": "4.2.x",
+        "commander": "2.17.x",
+        "he": "1.1.x",
+        "param-case": "2.1.x",
+        "relateurl": "0.2.x",
+        "uglify-js": "3.4.x"
+      }
+    },
+    "html-webpack-plugin": {
+      "version": "3.2.0",
+      "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
+      "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
+      "dev": true,
+      "requires": {
+        "html-minifier": "^3.2.3",
+        "loader-utils": "^0.2.16",
+        "lodash": "^4.17.3",
+        "pretty-error": "^2.0.2",
+        "tapable": "^1.0.0",
+        "toposort": "^1.0.0",
+        "util.promisify": "1.0.0"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "0.2.17",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+          "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+          "dev": true,
+          "requires": {
+            "big.js": "^3.1.3",
+            "emojis-list": "^2.0.0",
+            "json5": "^0.5.0",
+            "object-assign": "^4.0.1"
+          }
+        }
+      }
+    },
+    "htmlparser2": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz",
+      "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1",
+        "domhandler": "2.1",
+        "domutils": "1.1",
+        "readable-stream": "1.0"
+      },
+      "dependencies": {
+        "domutils": {
+          "version": "1.1.6",
+          "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz",
+          "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=",
+          "dev": true,
+          "requires": {
+            "domelementtype": "1"
+          }
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "http-deceiver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
+      "dev": true
+    },
+    "http-errors": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+      "dev": true,
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": ">= 1.4.0 < 2"
+      }
+    },
+    "http-parser-js": {
+      "version": "0.4.13",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz",
+      "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=",
+      "dev": true
+    },
+    "http-proxy": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
+      "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "^3.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "http-proxy-middleware": {
+      "version": "0.18.0",
+      "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
+      "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
+      "dev": true,
+      "requires": {
+        "http-proxy": "^1.16.2",
+        "is-glob": "^4.0.0",
+        "lodash": "^4.17.5",
+        "micromatch": "^3.1.9"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
+    },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+      "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
+      "dev": true
+    },
+    "ieee754": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "dev": true
+    },
+    "iferr": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
+      "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
+      "dev": true
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+      "dev": true
+    },
+    "image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+      "dev": true,
+      "optional": true
+    },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
+      "dev": true
+    },
+    "import-cwd": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
+      "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
+      "dev": true,
+      "requires": {
+        "import-from": "^2.1.0"
+      }
+    },
+    "import-from": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz",
+      "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^3.0.0"
+      }
+    },
+    "import-local": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
+      "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==",
+      "dev": true,
+      "requires": {
+        "pkg-dir": "^2.0.0",
+        "resolve-cwd": "^2.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+      "dev": true,
+      "optional": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+      "dev": true
+    },
+    "internal-ip": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz",
+      "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==",
+      "dev": true,
+      "requires": {
+        "default-gateway": "^2.6.0",
+        "ipaddr.js": "^1.5.2"
+      }
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dev": true,
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
+      "dev": true
+    },
+    "ip-regex": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
+      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
+      "dev": true
+    },
+    "ipaddr.js": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+      "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=",
+      "dev": true
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "^1.0.0"
+      }
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+      "dev": true
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+      "dev": true
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "dev": true,
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "is-directory": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+      "dev": true
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "dev": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "dev": true,
+      "requires": {
+        "is-primitive": "^2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+      "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+      "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+      "dev": true
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+      "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+      "dev": true,
+      "requires": {
+        "is-path-inside": "^1.0.0"
+      }
+    },
+    "is-path-inside": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "dev": true,
+      "requires": {
+        "path-is-inside": "^1.0.1"
+      }
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "dev": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+      "dev": true
+    },
+    "is-symbol": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
+      "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
+      "dev": true
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true
+    },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
+    "isbinaryfile": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
+      "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc": "^1.2.0"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "istanbul": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+      "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1.0.x",
+        "async": "1.x",
+        "escodegen": "1.8.x",
+        "esprima": "2.7.x",
+        "glob": "^5.0.15",
+        "handlebars": "^4.0.1",
+        "js-yaml": "3.x",
+        "mkdirp": "0.5.x",
+        "nopt": "3.x",
+        "once": "1.x",
+        "resolve": "1.1.x",
+        "supports-color": "^3.1.0",
+        "which": "^1.1.1",
+        "wordwrap": "^1.0.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "5.0.15",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+          "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+          "dev": true,
+          "requires": {
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "2 || 3",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-api": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.0.5.tgz",
+      "integrity": "sha512-GE5gqFpZsHKgsAbVsgPXlcWKV7fAKP7Bbrma4BJbzBQ+O7KVd/o94WjXOTn4m6eThMhBjWOGOKmaWPwJ3tHVIA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.6.1",
+        "compare-versions": "^3.2.1",
+        "fileset": "^2.0.3",
+        "istanbul-lib-coverage": "^2.0.1",
+        "istanbul-lib-hook": "^2.0.1",
+        "istanbul-lib-instrument": "^2.3.2",
+        "istanbul-lib-report": "^2.0.1",
+        "istanbul-lib-source-maps": "^2.0.1",
+        "istanbul-reports": "^2.0.0",
+        "js-yaml": "^3.12.0",
+        "make-dir": "^1.3.0",
+        "once": "^1.4.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        },
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        },
+        "istanbul-lib-instrument": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-2.3.2.tgz",
+          "integrity": "sha512-l7TD/VnBsIB2OJvSyxaLW/ab1+92dxZNH9wLH7uHPPioy3JZ8tnx2UXUdKmdkgmP2EFPzg64CToUP6dAS3U32Q==",
+          "dev": true,
+          "requires": {
+            "@babel/generator": "7.0.0-beta.51",
+            "@babel/parser": "7.0.0-beta.51",
+            "@babel/template": "7.0.0-beta.51",
+            "@babel/traverse": "7.0.0-beta.51",
+            "@babel/types": "7.0.0-beta.51",
+            "istanbul-lib-coverage": "^2.0.1",
+            "semver": "^5.5.0"
+          }
+        }
+      }
+    },
+    "istanbul-instrumenter-loader": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz",
+      "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "^1.5.0",
+        "istanbul-lib-instrument": "^1.7.3",
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.3.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "schema-utils": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
+          "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-lib-coverage": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz",
+      "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==",
+      "dev": true
+    },
+    "istanbul-lib-hook": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.1.tgz",
+      "integrity": "sha512-ufiZoiJ8CxY577JJWEeFuxXZoMqiKpq/RqZtOAYuQLvlkbJWscq9n3gc4xrCGH9n4pW0qnTxOz1oyMmVtk8E1w==",
+      "dev": true,
+      "requires": {
+        "append-transform": "^1.0.0"
+      }
+    },
+    "istanbul-lib-instrument": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz",
+      "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==",
+      "dev": true,
+      "requires": {
+        "babel-generator": "^6.18.0",
+        "babel-template": "^6.16.0",
+        "babel-traverse": "^6.18.0",
+        "babel-types": "^6.18.0",
+        "babylon": "^6.18.0",
+        "istanbul-lib-coverage": "^1.2.0",
+        "semver": "^5.3.0"
+      }
+    },
+    "istanbul-lib-report": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.1.tgz",
+      "integrity": "sha512-pXYOWwpDNc5AHIY93WjFTuxzkDOOZ7B8eSa0cBHTmTnKRst5ccc/xBfWu/5wcNJqB6/Qy0lDMhpn+Uy0qyyUjA==",
+      "dev": true,
+      "requires": {
+        "istanbul-lib-coverage": "^2.0.1",
+        "make-dir": "^1.3.0",
+        "supports-color": "^5.4.0"
+      },
+      "dependencies": {
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-lib-source-maps": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.0.1.tgz",
+      "integrity": "sha512-30l40ySg+gvBLcxTrLzR4Z2XTRj3HgRCA/p2rnbs/3OiTaoj054gAbuP5DcLOtwqmy4XW8qXBHzrmP2/bQ9i3A==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "istanbul-lib-coverage": "^2.0.1",
+        "make-dir": "^1.3.0",
+        "rimraf": "^2.6.2",
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-reports": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.0.0.tgz",
+      "integrity": "sha512-d2YRSnAOHHb+6vMc5qjJEyPN4VapkgUMhKlMmr3BzKdMDWdJbyYGEi/7m5AjDjkvRRTjs68ttPRZ7W2jBZ31SQ==",
+      "dev": true,
+      "requires": {
+        "handlebars": "^4.0.11"
+      }
+    },
+    "jasmine": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
+      "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
+      "dev": true,
+      "requires": {
+        "exit": "^0.1.2",
+        "glob": "^7.0.6",
+        "jasmine-core": "~2.8.0"
+      },
+      "dependencies": {
+        "jasmine-core": {
+          "version": "2.8.0",
+          "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
+          "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
+          "dev": true
+        }
+      }
+    },
+    "jasmine-core": {
+      "version": "2.99.1",
+      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz",
+      "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
+      "dev": true
+    },
+    "jasmine-diff": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/jasmine-diff/-/jasmine-diff-0.1.3.tgz",
+      "integrity": "sha1-k8zC3MQQKMXd1GBlWAdIOfLe6qg=",
+      "dev": true,
+      "requires": {
+        "diff": "^3.2.0"
+      }
+    },
+    "jasmine-spec-reporter": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
+      "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
+      "dev": true,
+      "requires": {
+        "colors": "1.1.2"
+      }
+    },
+    "jasminewd2": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz",
+      "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
+      "dev": true
+    },
+    "js-base64": {
+      "version": "2.4.9",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz",
+      "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==",
+      "dev": true,
+      "optional": true
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+      "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        }
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true,
+      "optional": true
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+      "dev": true
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json3": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
+      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+      "dev": true
+    },
+    "jsonfile": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jszip": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+      "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+      "dev": true,
+      "requires": {
+        "core-js": "~2.3.0",
+        "es6-promise": "~3.0.2",
+        "lie": "~3.1.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.0.6"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+          "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
+          "dev": true
+        },
+        "es6-promise": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+          "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
+          "dev": true
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~1.0.6",
+            "string_decoder": "~0.10.x",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "karma": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
+      "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.3.0",
+        "body-parser": "^1.16.1",
+        "chokidar": "^1.4.1",
+        "colors": "^1.1.0",
+        "combine-lists": "^1.0.0",
+        "connect": "^3.6.0",
+        "core-js": "^2.2.0",
+        "di": "^0.0.1",
+        "dom-serialize": "^2.2.0",
+        "expand-braces": "^0.1.1",
+        "glob": "^7.1.1",
+        "graceful-fs": "^4.1.2",
+        "http-proxy": "^1.13.0",
+        "isbinaryfile": "^3.0.0",
+        "lodash": "^3.8.0",
+        "log4js": "^0.6.31",
+        "mime": "^1.3.4",
+        "minimatch": "^3.0.2",
+        "optimist": "^0.6.1",
+        "qjobs": "^1.1.4",
+        "range-parser": "^1.2.0",
+        "rimraf": "^2.6.0",
+        "safe-buffer": "^5.0.1",
+        "socket.io": "1.7.3",
+        "source-map": "^0.5.3",
+        "tmp": "0.0.31",
+        "useragent": "^2.1.12"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+          "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+          "dev": true,
+          "requires": {
+            "micromatch": "^2.1.5",
+            "normalize-path": "^2.0.0"
+          }
+        },
+        "arr-diff": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+          "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+          "dev": true,
+          "requires": {
+            "arr-flatten": "^1.0.1"
+          }
+        },
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "1.8.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+          "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^1.8.1",
+            "preserve": "^0.2.0",
+            "repeat-element": "^1.1.2"
+          }
+        },
+        "chokidar": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+          "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+          "dev": true,
+          "requires": {
+            "anymatch": "^1.3.0",
+            "async-each": "^1.0.0",
+            "fsevents": "^1.0.0",
+            "glob-parent": "^2.0.0",
+            "inherits": "^2.0.1",
+            "is-binary-path": "^1.0.0",
+            "is-glob": "^2.0.0",
+            "path-is-absolute": "^1.0.0",
+            "readdirp": "^2.0.0"
+          }
+        },
+        "expand-brackets": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+          "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+          "dev": true,
+          "requires": {
+            "is-posix-bracket": "^0.1.0"
+          }
+        },
+        "extglob": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+          "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        },
+        "lodash": {
+          "version": "3.10.1",
+          "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+          "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
+          "dev": true
+        },
+        "micromatch": {
+          "version": "2.3.11",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+          "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+          "dev": true,
+          "requires": {
+            "arr-diff": "^2.0.0",
+            "array-unique": "^0.2.1",
+            "braces": "^1.8.2",
+            "expand-brackets": "^0.1.4",
+            "extglob": "^0.3.1",
+            "filename-regex": "^2.0.0",
+            "is-extglob": "^1.0.0",
+            "is-glob": "^2.0.1",
+            "kind-of": "^3.0.2",
+            "normalize-path": "^2.0.1",
+            "object.omit": "^2.0.0",
+            "parse-glob": "^3.0.4",
+            "regex-cache": "^0.4.2"
+          }
+        }
+      }
+    },
+    "karma-chrome-launcher": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
+      "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+      "dev": true,
+      "requires": {
+        "fs-access": "^1.0.0",
+        "which": "^1.2.1"
+      }
+    },
+    "karma-coverage-istanbul-reporter": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.3.tgz",
+      "integrity": "sha512-UVs9IDulfwkBxjEnUzfR/nIc3oBneOPuorpLVBvEMtz2hy0wnVLhCMxpkqAtuQWqvOZRQlGqs+dDtMUeRydTQA==",
+      "dev": true,
+      "requires": {
+        "istanbul-api": "^2.0.5",
+        "minimatch": "^3.0.4"
+      }
+    },
+    "karma-jasmine": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz",
+      "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=",
+      "dev": true
+    },
+    "karma-jasmine-html-reporter": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz",
+      "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=",
+      "dev": true,
+      "requires": {
+        "karma-jasmine": "^1.0.2"
+      }
+    },
+    "karma-phantomjs-launcher": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz",
+      "integrity": "sha1-0jyjSAG9qYY60xjju0vUBisTrNI=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.0.1",
+        "phantomjs-prebuilt": "^2.1.7"
+      }
+    },
+    "karma-source-map-support": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz",
+      "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==",
+      "dev": true,
+      "requires": {
+        "source-map-support": "^0.5.5"
+      }
+    },
+    "kew": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
+      "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=",
+      "dev": true
+    },
+    "killable": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz",
+      "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=",
+      "dev": true
+    },
+    "kind-of": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+      "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+      "dev": true
+    },
+    "klaw": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+      "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.9"
+      }
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "^1.0.0"
+      }
+    },
+    "leb": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz",
+      "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=",
+      "dev": true
+    },
+    "less": {
+      "version": "3.8.1",
+      "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz",
+      "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==",
+      "dev": true,
+      "requires": {
+        "clone": "^2.1.2",
+        "errno": "^0.1.1",
+        "graceful-fs": "^4.1.2",
+        "image-size": "~0.5.0",
+        "mime": "^1.4.1",
+        "mkdirp": "^0.5.0",
+        "promise": "^7.1.1",
+        "request": "^2.83.0",
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "less-loader": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz",
+      "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==",
+      "dev": true,
+      "requires": {
+        "clone": "^2.1.1",
+        "loader-utils": "^1.1.0",
+        "pify": "^3.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "license-webpack-plugin": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.4.0.tgz",
+      "integrity": "sha512-iwuNFMWbXS76WiQXJBTs8/7Tby4NQnY8AIkBMuJG5El79UT8zWrJQMfpW+KRXt4Y2Bs5uk+Myg/MO7ROSF8jzA==",
+      "dev": true,
+      "requires": {
+        "ejs": "^2.5.7"
+      }
+    },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "dev": true,
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "loader-runner": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz",
+      "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=",
+      "dev": true
+    },
+    "loader-utils": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
+      "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
+      "dev": true,
+      "requires": {
+        "big.js": "^3.1.3",
+        "emojis-list": "^2.0.0",
+        "json5": "^0.5.0"
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.10",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+      "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+      "dev": true
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+      "dev": true,
+      "optional": true
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+      "dev": true
+    },
+    "lodash.mergewith": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+      "dev": true,
+      "optional": true
+    },
+    "lodash.tail": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
+      "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
+      "dev": true
+    },
+    "log4js": {
+      "version": "0.6.38",
+      "resolved": "http://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz",
+      "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "~1.0.2",
+        "semver": "~4.3.3"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "semver": {
+          "version": "4.3.6",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
+          "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+          "dev": true
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "loglevel": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
+      "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=",
+      "dev": true
+    },
+    "long": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
+      "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
+      "dev": true
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "^0.4.1",
+        "signal-exit": "^3.0.0"
+      }
+    },
+    "lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+      "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+      "dev": true,
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "make-dir": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "make-error": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+      "dev": true
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "dev": true,
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "math-random": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
+      "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
+      "dev": true
+    },
+    "md5.js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+      "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
+    "mem": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+      "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^1.0.0"
+      }
+    },
+    "memory-fs": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+      "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "dev": true,
+      "requires": {
+        "errno": "^0.1.3",
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase-keys": "^2.0.0",
+        "decamelize": "^1.1.2",
+        "loud-rejection": "^1.0.0",
+        "map-obj": "^1.0.1",
+        "minimist": "^1.1.3",
+        "normalize-package-data": "^2.3.4",
+        "object-assign": "^4.0.1",
+        "read-pkg-up": "^1.0.1",
+        "redent": "^1.0.0",
+        "trim-newlines": "^1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+      "dev": true
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "brorand": "^1.0.1"
+      }
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.36.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
+      "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.20",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
+      "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+      "dev": true,
+      "requires": {
+        "mime-db": "~1.36.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "dev": true
+    },
+    "mini-css-extract-plugin": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz",
+      "integrity": "sha512-ots7URQH4wccfJq9Ssrzu2+qupbncAce4TmTzunI9CIwlQMp2XI+WNUw6xWF6MMAGAm1cbUVINrSjATaVMyKXg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^1.0.0",
+        "webpack-sources": "^1.1.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "dev": true
+    },
+    "mississippi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz",
+      "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.5.0",
+        "duplexify": "^3.4.2",
+        "end-of-stream": "^1.1.0",
+        "flush-write-stream": "^1.0.0",
+        "from2": "^2.1.0",
+        "parallel-transform": "^1.1.0",
+        "pump": "^2.0.1",
+        "pumpify": "^1.3.3",
+        "stream-each": "^1.1.0",
+        "through2": "^2.0.0"
+      }
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mixin-object": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
+      "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
+      "dev": true,
+      "requires": {
+        "for-in": "^0.1.3",
+        "is-extendable": "^0.1.1"
+      },
+      "dependencies": {
+        "for-in": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
+          "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=",
+          "dev": true
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "move-concurrently": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+      "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "copy-concurrently": "^1.0.0",
+        "fs-write-stream-atomic": "^1.0.8",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.3"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
+    },
+    "multicast-dns": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
+      "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+      "dev": true,
+      "requires": {
+        "dns-packet": "^1.3.1",
+        "thunky": "^1.0.2"
+      }
+    },
+    "multicast-dns-service-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
+      "dev": true
+    },
+    "nan": {
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
+      "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==",
+      "dev": true,
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
+      "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==",
+      "dev": true
+    },
+    "ngx-cookie": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-4.0.2.tgz",
+      "integrity": "sha512-YCak+Itql8EDkMfr9lzCNd2wEeV+uflbv2V1mi9LCzUyFcO+W53S/BbuZS5r9M8MZzUiBl4AmpEDEKYiXrb3Sw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "ngx-i18nsupport": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/ngx-i18nsupport/-/ngx-i18nsupport-0.17.0.tgz",
+      "integrity": "sha512-iGH3CnEehukzuU9OFai3Kwi06CsNRMI3zquIjUTBUDlVgRwpfGse0BGrr/RRJ359i9P0aeNWtjnDKsW4apSAOw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "commander": "^2.15.1",
+        "he": "^1.1.1",
+        "ngx-i18nsupport-lib": "^1.10.0",
+        "request": "^2.85.0",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "ngx-i18nsupport-lib": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/ngx-i18nsupport-lib/-/ngx-i18nsupport-lib-1.10.0.tgz",
+      "integrity": "sha512-J+0EvMrG31o5SJrb3sZS9WPdc34Qbmq9CglVlemV68kPcCvPbe8yj3qJthOmtoVz8t9ksGugYkB42KZPEMTSeA==",
+      "dev": true,
+      "requires": {
+        "@types/xmldom": "^0.1.29",
+        "tokenizr": "^1.3.4",
+        "xmldom": "^0.1.27"
+      }
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "no-case": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+      "dev": true,
+      "requires": {
+        "lower-case": "^1.1.1"
+      }
+    },
+    "node-forge": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
+      "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
+      "dev": true
+    },
+    "node-gyp": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+      "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "fstream": "^1.0.0",
+        "glob": "^7.0.3",
+        "graceful-fs": "^4.1.2",
+        "mkdirp": "^0.5.0",
+        "nopt": "2 || 3",
+        "npmlog": "0 || 1 || 2 || 3 || 4",
+        "osenv": "0",
+        "request": "^2.87.0",
+        "rimraf": "2",
+        "semver": "~5.3.0",
+        "tar": "^2.0.0",
+        "which": "1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+          "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "node-libs-browser": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
+      "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==",
+      "dev": true,
+      "requires": {
+        "assert": "^1.1.1",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^4.3.0",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "crypto-browserify": "^3.11.0",
+        "domain-browser": "^1.1.1",
+        "events": "^1.0.0",
+        "https-browserify": "^1.0.0",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "0.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.2.4",
+        "querystring-es3": "^0.2.0",
+        "readable-stream": "^2.3.3",
+        "stream-browserify": "^2.0.1",
+        "stream-http": "^2.7.2",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "^0.11.0",
+        "util": "^0.10.3",
+        "vm-browserify": "0.0.4"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "node-sass": {
+      "version": "4.9.3",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz",
+      "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "async-foreach": "^0.1.3",
+        "chalk": "^1.1.1",
+        "cross-spawn": "^3.0.0",
+        "gaze": "^1.0.0",
+        "get-stdin": "^4.0.1",
+        "glob": "^7.0.3",
+        "in-publish": "^2.0.0",
+        "lodash.assign": "^4.2.0",
+        "lodash.clonedeep": "^4.3.2",
+        "lodash.mergewith": "^4.6.0",
+        "meow": "^3.7.0",
+        "mkdirp": "^0.5.1",
+        "nan": "^2.10.0",
+        "node-gyp": "^3.8.0",
+        "npmlog": "^4.0.0",
+        "request": "2.87.0",
+        "sass-graph": "^2.2.4",
+        "stdout-stream": "^1.4.0",
+        "true-case-path": "^1.0.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true,
+          "optional": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "har-validator": {
+          "version": "5.0.3",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+          "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ajv": "^5.1.0",
+            "har-schema": "^2.0.0"
+          }
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+          "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+          "dev": true,
+          "optional": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true,
+          "optional": true
+        },
+        "request": {
+          "version": "2.87.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
+          "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aws-sign2": "~0.7.0",
+            "aws4": "^1.6.0",
+            "caseless": "~0.12.0",
+            "combined-stream": "~1.0.5",
+            "extend": "~3.0.1",
+            "forever-agent": "~0.6.1",
+            "form-data": "~2.3.1",
+            "har-validator": "~5.0.3",
+            "http-signature": "~1.2.0",
+            "is-typedarray": "~1.0.0",
+            "isstream": "~0.1.2",
+            "json-stringify-safe": "~5.0.1",
+            "mime-types": "~2.1.17",
+            "oauth-sign": "~0.8.2",
+            "performance-now": "^2.1.0",
+            "qs": "~6.5.1",
+            "safe-buffer": "^5.1.1",
+            "tough-cookie": "~2.3.3",
+            "tunnel-agent": "^0.6.0",
+            "uuid": "^3.1.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true,
+          "optional": true
+        },
+        "tough-cookie": {
+          "version": "2.3.4",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+          "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "punycode": "^1.4.1"
+          }
+        }
+      }
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "is-builtin-module": "^1.0.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "npm-package-arg": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz",
+      "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.6.0",
+        "osenv": "^0.1.5",
+        "semver": "^5.5.0",
+        "validate-npm-package-name": "^3.0.0"
+      }
+    },
+    "npm-registry-client": {
+      "version": "8.6.0",
+      "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.6.0.tgz",
+      "integrity": "sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.5.2",
+        "graceful-fs": "^4.1.6",
+        "normalize-package-data": "~1.0.1 || ^2.0.0",
+        "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
+        "npmlog": "2 || ^3.1.0 || ^4.0.0",
+        "once": "^1.3.3",
+        "request": "^2.74.0",
+        "retry": "^0.10.0",
+        "safe-buffer": "^5.1.1",
+        "semver": "2 >=2.2.1 || 3.x || 4 || 5",
+        "slide": "^1.1.3",
+        "ssri": "^5.2.4"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "dev": true,
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "dev": true,
+      "requires": {
+        "are-we-there-yet": "~1.1.2",
+        "console-control-strings": "~1.1.0",
+        "gauge": "~2.7.3",
+        "set-blocking": "~2.0.0"
+      }
+    },
+    "nth-check": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
+      "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0"
+      }
+    },
+    "null-check": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
+      "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
+      "dev": true
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
+      "dev": true
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "dev": true,
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-keys": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+      "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
+      "dev": true
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.5.1"
+      }
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "dev": true,
+      "requires": {
+        "for-own": "^0.1.4",
+        "is-extendable": "^0.1.1"
+      },
+      "dependencies": {
+        "for-own": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+          "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+          "dev": true,
+          "requires": {
+            "for-in": "^1.0.1"
+          }
+        }
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "obuf": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+      "dev": true
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "opn": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
+      "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
+      "dev": true,
+      "requires": {
+        "is-wsl": "^1.1.0"
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "options": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
+      "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=",
+      "dev": true
+    },
+    "original": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
+      "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
+      "dev": true,
+      "requires": {
+        "url-parse": "^1.4.3"
+      }
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+      "dev": true
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "lcid": "^1.0.0"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+      "dev": true,
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.0"
+      }
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-map": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
+      "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
+      "dev": true
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+      "dev": true
+    },
+    "parallel-transform": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
+      "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+      "dev": true,
+      "requires": {
+        "cyclist": "~0.2.2",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.1.5"
+      }
+    },
+    "param-case": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+      "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+      "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+      "dev": true,
+      "requires": {
+        "asn1.js": "^4.0.0",
+        "browserify-aes": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.0",
+        "pbkdf2": "^3.0.3"
+      }
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "dev": true,
+      "requires": {
+        "glob-base": "^0.3.0",
+        "is-dotfile": "^1.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.0"
+      },
+      "dependencies": {
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        }
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parse5": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+      "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+      "dev": true
+    },
+    "parsejson": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
+      "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+      "dev": true
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+      "dev": true
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+      "dev": true
+    },
+    "path-type": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "pbkdf2": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",
+      "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==",
+      "dev": true,
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "phantomjs-prebuilt": {
+      "version": "2.1.16",
+      "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz",
+      "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3",
+        "extract-zip": "^1.6.5",
+        "fs-extra": "^1.0.0",
+        "hasha": "^2.2.0",
+        "kew": "^0.7.0",
+        "progress": "^1.1.8",
+        "request": "^2.81.0",
+        "request-progress": "^2.0.1",
+        "which": "^1.2.10"
+      }
+    },
+    "pify": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+      "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "portfinder": {
+      "version": "1.0.17",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.17.tgz",
+      "integrity": "sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ==",
+      "dev": true,
+      "requires": {
+        "async": "^1.5.2",
+        "debug": "^2.2.0",
+        "mkdirp": "0.5.x"
+      }
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+      "dev": true
+    },
+    "postcss": {
+      "version": "6.0.23",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+      "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "source-map": "^0.6.1",
+        "supports-color": "^5.4.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-import": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz",
+      "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==",
+      "dev": true,
+      "requires": {
+        "postcss": "^6.0.1",
+        "postcss-value-parser": "^3.2.3",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      }
+    },
+    "postcss-load-config": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz",
+      "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==",
+      "dev": true,
+      "requires": {
+        "cosmiconfig": "^4.0.0",
+        "import-cwd": "^2.0.0"
+      }
+    },
+    "postcss-loader": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.6.tgz",
+      "integrity": "sha512-hgiWSc13xVQAq25cVw80CH0l49ZKlAnU1hKPOdRrNj89bokRr/bZF2nT+hebPPF9c9xs8c3gw3Fr2nxtmXYnNg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "postcss": "^6.0.0",
+        "postcss-load-config": "^2.0.0",
+        "schema-utils": "^0.4.0"
+      }
+    },
+    "postcss-url": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz",
+      "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==",
+      "dev": true,
+      "requires": {
+        "mime": "^1.4.1",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.0",
+        "postcss": "^6.0.1",
+        "xxhashjs": "^0.2.1"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz",
+      "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "dev": true
+    },
+    "pretty-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz",
+      "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=",
+      "dev": true,
+      "requires": {
+        "renderkid": "^2.0.1",
+        "utila": "~0.4"
+      }
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+      "dev": true
+    },
+    "progress": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+      "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+      "dev": true
+    },
+    "promise": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+      "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "asap": "~2.0.3"
+      }
+    },
+    "promise-inflight": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+      "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
+      "dev": true
+    },
+    "protractor": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.0.tgz",
+      "integrity": "sha512-6TSYqMhUUzxr4/wN0ttSISqPMKvcVRXF4k8jOEpGWD8OioLak4KLgfzHK9FJ49IrjzRrZ+Mx1q2Op8Rk0zEcnQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "^6.0.46",
+        "@types/q": "^0.0.32",
+        "@types/selenium-webdriver": "^3.0.0",
+        "blocking-proxy": "^1.0.0",
+        "browserstack": "^1.5.1",
+        "chalk": "^1.1.3",
+        "glob": "^7.0.3",
+        "jasmine": "2.8.0",
+        "jasminewd2": "^2.1.0",
+        "optimist": "~0.6.0",
+        "q": "1.4.1",
+        "saucelabs": "^1.5.0",
+        "selenium-webdriver": "3.6.0",
+        "source-map-support": "~0.4.0",
+        "webdriver-js-extender": "2.0.0",
+        "webdriver-manager": "^12.0.6"
+      },
+      "dependencies": {
+        "@types/node": {
+          "version": "6.0.117",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.117.tgz",
+          "integrity": "sha512-sihk0SnN8PpiS5ihu5xJQ5ddnURNq4P+XPmW+nORlKkHy21CoZO/IVHK/Wq/l3G8fFW06Fkltgnqx229uPlnRg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "del": {
+          "version": "2.2.2",
+          "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+          "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+          "dev": true,
+          "requires": {
+            "globby": "^5.0.0",
+            "is-path-cwd": "^1.0.0",
+            "is-path-in-cwd": "^1.0.0",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0",
+            "rimraf": "^2.2.8"
+          }
+        },
+        "globby": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+          "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "arrify": "^1.0.0",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        },
+        "source-map-support": {
+          "version": "0.4.18",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+          "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+          "dev": true,
+          "requires": {
+            "source-map": "^0.5.6"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        },
+        "webdriver-manager": {
+          "version": "12.1.0",
+          "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.0.tgz",
+          "integrity": "sha512-oEc5fmkpz6Yh6udhwir5m0eN5mgRPq9P/NU5YWuT3Up5slt6Zz+znhLU7q4+8rwCZz/Qq3Fgpr/4oao7NPCm2A==",
+          "dev": true,
+          "requires": {
+            "adm-zip": "^0.4.9",
+            "chalk": "^1.1.1",
+            "del": "^2.2.0",
+            "glob": "^7.0.3",
+            "ini": "^1.3.4",
+            "minimist": "^1.2.0",
+            "q": "^1.4.1",
+            "request": "^2.87.0",
+            "rimraf": "^2.5.2",
+            "semver": "^5.3.0",
+            "xml2js": "^0.4.17"
+          }
+        }
+      }
+    },
+    "proxy-addr": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+      "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+      "dev": true,
+      "requires": {
+        "forwarded": "~0.1.2",
+        "ipaddr.js": "1.8.0"
+      }
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "dev": true
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.1.29",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+      "dev": true
+    },
+    "public-encrypt": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
+      "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "parse-asn1": "^5.0.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "pumpify": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+      "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+      "dev": true,
+      "requires": {
+        "duplexify": "^3.6.0",
+        "inherits": "^2.0.3",
+        "pump": "^2.0.0"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "q": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+      "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+      "dev": true
+    },
+    "qjobs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+      "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "dev": true
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "querystringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
+      "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==",
+      "dev": true
+    },
+    "randomatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz",
+      "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^4.0.0",
+        "kind-of": "^6.0.0",
+        "math-random": "^1.0.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+          "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+          "dev": true
+        }
+      }
+    },
+    "randombytes": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+      "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+      "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+      "dev": true,
+      "requires": {
+        "randombytes": "^2.0.5",
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+      "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "depd": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+          "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+          "dev": true
+        },
+        "http-errors": {
+          "version": "1.6.2",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+          "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+          "dev": true,
+          "requires": {
+            "depd": "1.1.1",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.0.3",
+            "statuses": ">= 1.3.1 < 2"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+          "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
+          "dev": true
+        }
+      }
+    },
+    "raw-loader": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
+      "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=",
+      "dev": true
+    },
+    "read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+      "dev": true,
+      "requires": {
+        "pify": "^2.3.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "^2.0.0"
+          }
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+      "dev": true,
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
+      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "minimatch": "^3.0.2",
+        "readable-stream": "^2.0.2",
+        "set-immediate-shim": "^1.0.1"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "indent-string": "^2.1.0",
+        "strip-indent": "^1.0.1"
+      }
+    },
+    "reflect-metadata": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
+      "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==",
+      "dev": true
+    },
+    "regenerate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+      "dev": true
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "dev": true
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "^0.1.3"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexpu-core": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
+      "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
+      "dev": true,
+      "requires": {
+        "regenerate": "^1.2.1",
+        "regjsgen": "^0.2.0",
+        "regjsparser": "^0.1.4"
+      }
+    },
+    "regjsgen": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+      "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+      "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+      "dev": true,
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        }
+      }
+    },
+    "relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+      "dev": true
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "renderkid": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz",
+      "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=",
+      "dev": true,
+      "requires": {
+        "css-select": "^1.1.0",
+        "dom-converter": "~0.1",
+        "htmlparser2": "~3.3.0",
+        "strip-ansi": "^3.0.0",
+        "utila": "~0.3"
+      },
+      "dependencies": {
+        "utila": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz",
+          "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=",
+          "dev": true
+        }
+      }
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "request-progress": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
+      "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
+      "dev": true,
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+      "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+      "dev": true
+    },
+    "resolve-cwd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+      "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^3.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+      "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
+    "retry": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
+      "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+      "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.5"
+      }
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "run-queue": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
+      "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1"
+      }
+    },
+    "rxjs": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.2.tgz",
+      "integrity": "sha512-hV7criqbR0pe7EeL3O66UYVg92IR0XsA97+9y+BWTePK9SKmEI5Qd3Zj6uPnGkNzXsBywBQWTvujPl+1Kn9Zjw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "rxjs-compat": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.3.2.tgz",
+      "integrity": "sha512-eH0ANsX4ufMSDmSDwWbsWYgZDDDxxLHxsSwApbQumHTFm83RP4AI594QtXv3Jup+hVjXfE2dRSAVKbMh2a2hcw=="
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "sass-graph": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+      "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "lodash": "^4.0.0",
+        "scss-tokenizer": "^0.2.3",
+        "yargs": "^7.0.0"
+      }
+    },
+    "sass-loader": {
+      "version": "6.0.7",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz",
+      "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^2.0.1",
+        "loader-utils": "^1.0.1",
+        "lodash.tail": "^4.1.1",
+        "neo-async": "^2.5.0",
+        "pify": "^3.0.0"
+      }
+    },
+    "saucelabs": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz",
+      "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "^2.2.1"
+      }
+    },
+    "sax": {
+      "version": "0.5.8",
+      "resolved": "http://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
+      "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=",
+      "dev": true
+    },
+    "schema-utils": {
+      "version": "0.4.7",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
+      "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0"
+      }
+    },
+    "scss-tokenizer": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+      "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "js-base64": "^2.1.8",
+        "source-map": "^0.4.2"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "select-hose": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
+      "dev": true
+    },
+    "selenium-webdriver": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
+      "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
+      "dev": true,
+      "requires": {
+        "jszip": "^3.1.3",
+        "rimraf": "^2.5.4",
+        "tmp": "0.0.30",
+        "xml2js": "^0.4.17"
+      },
+      "dependencies": {
+        "tmp": {
+          "version": "0.0.30",
+          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
+          "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
+          "dev": true,
+          "requires": {
+            "os-tmpdir": "~1.0.1"
+          }
+        }
+      }
+    },
+    "selfsigned": {
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz",
+      "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==",
+      "dev": true,
+      "requires": {
+        "node-forge": "0.7.5"
+      }
+    },
+    "semver": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+      "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+      "dev": true
+    },
+    "semver-dsl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz",
+      "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=",
+      "dev": true,
+      "requires": {
+        "semver": "^5.3.0"
+      }
+    },
+    "semver-intersect": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.4.0.tgz",
+      "integrity": "sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==",
+      "dev": true,
+      "requires": {
+        "semver": "^5.0.0"
+      }
+    },
+    "send": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "~1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.0",
+        "statuses": "~1.4.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+          "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+          "dev": true
+        }
+      }
+    },
+    "serialize-javascript": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
+      "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==",
+      "dev": true
+    },
+    "serve-index": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+      "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "batch": "0.6.1",
+        "debug": "2.6.9",
+        "escape-html": "~1.0.3",
+        "http-errors": "~1.6.2",
+        "mime-types": "~2.1.17",
+        "parseurl": "~1.3.2"
+      }
+    },
+    "serve-static": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "dev": true,
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.2",
+        "send": "0.16.2"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+      "dev": true
+    },
+    "setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shallow-clone": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
+      "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
+      "dev": true,
+      "requires": {
+        "is-extendable": "^0.1.1",
+        "kind-of": "^5.0.0",
+        "mixin-object": "^2.0.1"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+      "dev": true
+    },
+    "slide": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
+      "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
+      "dev": true
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "dev": true,
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "socket.io": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz",
+      "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=",
+      "dev": true,
+      "requires": {
+        "debug": "2.3.3",
+        "engine.io": "1.8.3",
+        "has-binary": "0.1.7",
+        "object-assign": "4.1.0",
+        "socket.io-adapter": "0.5.0",
+        "socket.io-client": "1.7.3",
+        "socket.io-parser": "2.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
+          "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz",
+      "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=",
+      "dev": true,
+      "requires": {
+        "debug": "2.3.3",
+        "socket.io-parser": "2.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-client": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz",
+      "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=",
+      "dev": true,
+      "requires": {
+        "backo2": "1.0.2",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "2.3.3",
+        "engine.io-client": "1.8.3",
+        "has-binary": "0.1.7",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "2.3.1",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz",
+      "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.1.2",
+        "debug": "2.2.0",
+        "isarray": "0.0.1",
+        "json3": "3.3.2"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
+          "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.1"
+          }
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "ms": {
+          "version": "0.7.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
+          "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
+          "dev": true
+        }
+      }
+    },
+    "sockjs": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
+      "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+      "dev": true,
+      "requires": {
+        "faye-websocket": "^0.10.0",
+        "uuid": "^3.0.1"
+      }
+    },
+    "sockjs-client": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz",
+      "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.6",
+        "eventsource": "0.1.6",
+        "faye-websocket": "~0.11.0",
+        "inherits": "^2.0.1",
+        "json3": "^3.3.2",
+        "url-parse": "^1.1.8"
+      },
+      "dependencies": {
+        "faye-websocket": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz",
+          "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
+          "dev": true,
+          "requires": {
+            "websocket-driver": ">=0.5.1"
+          }
+        }
+      }
+    },
+    "source-list-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
+      "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-loader": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz",
+      "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==",
+      "dev": true,
+      "requires": {
+        "async": "^2.5.0",
+        "loader-utils": "^1.1.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        }
+      }
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "dev": true,
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz",
+      "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "dev": true
+    },
+    "spdx-correct": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
+      "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
+      "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
+      "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==",
+      "dev": true
+    },
+    "spdy": {
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz",
+      "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.8",
+        "handle-thing": "^1.2.5",
+        "http-deceiver": "^1.2.7",
+        "safe-buffer": "^5.0.1",
+        "select-hose": "^2.0.0",
+        "spdy-transport": "^2.0.18"
+      }
+    },
+    "spdy-transport": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz",
+      "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.8",
+        "detect-node": "^2.0.3",
+        "hpack.js": "^2.1.6",
+        "obuf": "^1.1.1",
+        "readable-stream": "^2.2.9",
+        "safe-buffer": "^5.0.1",
+        "wbuf": "^1.7.2"
+      }
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+      "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+      "dev": true,
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "ssri": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
+      "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "dev": true,
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "stats-webpack-plugin": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz",
+      "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.4"
+      }
+    },
+    "statuses": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+      "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+      "dev": true
+    },
+    "stdout-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+      "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "dev": true,
+      "requires": {
+        "inherits": "~2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "stream-each": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+      "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "stream-http": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+      "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.6",
+        "to-arraybuffer": "^1.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "stream-shift": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+      "dev": true
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "strip-ansi": "^3.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+      "dev": true
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "get-stdin": "^4.0.1"
+      }
+    },
+    "style-loader": {
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz",
+      "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.4.5"
+      }
+    },
+    "stylus": {
+      "version": "0.54.5",
+      "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz",
+      "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=",
+      "dev": true,
+      "requires": {
+        "css-parse": "1.7.x",
+        "debug": "*",
+        "glob": "7.0.x",
+        "mkdirp": "0.5.x",
+        "sax": "0.5.x",
+        "source-map": "0.1.x"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.0.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+          "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.2",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "source-map": {
+          "version": "0.1.43",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+          "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+          "dev": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "stylus-loader": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz",
+      "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "lodash.clonedeep": "^4.5.0",
+        "when": "~3.6.x"
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+      "dev": true
+    },
+    "tapable": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz",
+      "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==",
+      "dev": true
+    },
+    "tar": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "block-stream": "*",
+        "fstream": "^1.0.2",
+        "inherits": "2"
+      }
+    },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "through2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "^2.1.5",
+        "xtend": "~4.0.1"
+      }
+    },
+    "thunky": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz",
+      "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=",
+      "dev": true
+    },
+    "timers-browserify": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
+      "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
+      "dev": true,
+      "requires": {
+        "setimmediate": "^1.0.4"
+      }
+    },
+    "tmp": {
+      "version": "0.0.31",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
+      "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.1"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
+      "dev": true
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+      "dev": true
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
+    "tokenizr": {
+      "version": "1.3.10",
+      "resolved": "https://registry.npmjs.org/tokenizr/-/tokenizr-1.3.10.tgz",
+      "integrity": "sha512-XlYlczHEQrbmj/JInA9vcsBJlukyTJWvjmQodjlbkul5fZ4o1JDNYAvLlrHZs03CSR8nFjNmTEqN3NrjTjmN+A==",
+      "dev": true
+    },
+    "toposort": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
+      "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
+      "dev": true
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "tree-kill": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz",
+      "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true,
+      "optional": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+      "dev": true
+    },
+    "true-case-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+      "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "^7.1.2"
+      }
+    },
+    "ts-node": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz",
+      "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.0",
+        "chalk": "^2.3.0",
+        "diff": "^3.1.0",
+        "make-error": "^1.1.1",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.5.3",
+        "yn": "^2.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "tsickle": {
+      "version": "0.32.1",
+      "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.32.1.tgz",
+      "integrity": "sha512-JW9j+W0SaMSZGejIFZBk0AiPfnhljK3oLx5SaqxrJhjlvzFyPml5zqG1/PuScUj6yTe1muEqwk5CnDK0cOZmKw==",
+      "dev": true,
+      "requires": {
+        "jasmine-diff": "^0.1.3",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map": "^0.6.0",
+        "source-map-support": "^0.5.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+    },
+    "tslint": {
+      "version": "5.9.1",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz",
+      "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.22.0",
+        "builtin-modules": "^1.1.1",
+        "chalk": "^2.3.0",
+        "commander": "^2.12.1",
+        "diff": "^3.2.0",
+        "glob": "^7.1.1",
+        "js-yaml": "^3.7.0",
+        "minimatch": "^3.0.4",
+        "resolve": "^1.3.2",
+        "semver": "^5.3.0",
+        "tslib": "^1.8.0",
+        "tsutils": "^2.12.1"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+          "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.5"
+          }
+        }
+      }
+    },
+    "tsutils": {
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.8.1"
+      }
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true,
+      "optional": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-is": {
+      "version": "1.6.16",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.18"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "typescript": {
+      "version": "2.7.2",
+      "resolved": "http://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz",
+      "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==",
+      "dev": true
+    },
+    "uglify-js": {
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+      "dev": true,
+      "requires": {
+        "commander": "~2.17.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "uglifyjs-webpack-plugin": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
+      "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "schema-utils": "^0.4.5",
+        "serialize-javascript": "^1.4.0",
+        "source-map": "^0.6.1",
+        "uglify-es": "^3.3.4",
+        "webpack-sources": "^1.1.0",
+        "worker-farm": "^1.5.2"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.13.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+          "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "uglify-es": {
+          "version": "3.3.9",
+          "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+          "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
+          "dev": true,
+          "requires": {
+            "commander": "~2.13.0",
+            "source-map": "~0.6.1"
+          }
+        }
+      }
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
+      "dev": true
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "unique-filename": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz",
+      "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=",
+      "dev": true,
+      "requires": {
+        "unique-slug": "^2.0.0"
+      }
+    },
+    "unique-slug": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz",
+      "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=",
+      "dev": true,
+      "requires": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "dev": true,
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "dev": true,
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "dev": true,
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+          "dev": true
+        }
+      }
+    },
+    "upath": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
+      "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+      "dev": true
+    },
+    "upper-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+      "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz",
+      "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "url-join": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz",
+      "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=",
+      "dev": true
+    },
+    "url-loader": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.1.tgz",
+      "integrity": "sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "mime": "^2.0.3",
+        "schema-utils": "^1.0.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
+          "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
+    "url-parse": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
+      "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
+      "dev": true,
+      "requires": {
+        "querystringify": "^2.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true
+    },
+    "useragent": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+      "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.x",
+        "tmp": "0.0.x"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "util.promisify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
+      "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "object.getownpropertydescriptors": "^2.0.3"
+      }
+    },
+    "utila": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+      "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=",
+      "dev": true
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "validate-npm-package-name": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+      "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
+      "dev": true,
+      "requires": {
+        "builtins": "^1.0.3"
+      }
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "dev": true
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+      "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
+      "dev": true
+    },
+    "watchpack": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+      "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "neo-async": "^2.5.0"
+      }
+    },
+    "wbuf": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+      "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+      "dev": true,
+      "requires": {
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "webassemblyjs": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/webassemblyjs/-/webassemblyjs-1.4.3.tgz",
+      "integrity": "sha512-4lOV1Lv6olz0PJkDGQEp82HempAn147e6BXijWDzz9g7/2nSebVP9GVg62Fz5ZAs55mxq13GA0XLyvY8XkyDjg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/validation": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "long": "^3.2.0"
+      }
+    },
+    "webdriver-js-extender": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.0.0.tgz",
+      "integrity": "sha512-fbyKiVu3azzIc5d4+26YfuPQcFTlgFQV5yQ/0OQj4Ybkl4g1YQuIPskf5v5wqwRJhHJnPHthB6tqCjWHOKLWag==",
+      "dev": true,
+      "requires": {
+        "@types/selenium-webdriver": "^3.0.0",
+        "selenium-webdriver": "^3.0.1"
+      }
+    },
+    "webpack": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.9.2.tgz",
+      "integrity": "sha512-jlWrCrJDU3sdWFprel6jHH8esN2C++Q8ehedRo74u7MWLTUJn9SD7RSgsCTEZCSRpVpMascDylAqPoldauOMfA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/wasm-edit": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "acorn": "^5.0.0",
+        "acorn-dynamic-import": "^3.0.0",
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0",
+        "chrome-trace-event": "^0.1.1",
+        "enhanced-resolve": "^4.0.0",
+        "eslint-scope": "^3.7.1",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^2.3.0",
+        "loader-utils": "^1.1.0",
+        "memory-fs": "~0.4.1",
+        "micromatch": "^3.1.8",
+        "mkdirp": "~0.5.0",
+        "neo-async": "^2.5.0",
+        "node-libs-browser": "^2.0.0",
+        "schema-utils": "^0.4.4",
+        "tapable": "^1.0.0",
+        "uglifyjs-webpack-plugin": "^1.2.4",
+        "watchpack": "^1.5.0",
+        "webpack-sources": "^1.0.1"
+      }
+    },
+    "webpack-core": {
+      "version": "0.6.9",
+      "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
+      "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
+      "dev": true,
+      "requires": {
+        "source-list-map": "~0.1.7",
+        "source-map": "~0.4.1"
+      },
+      "dependencies": {
+        "source-list-map": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz",
+          "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "webpack-dev-middleware": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.2.0.tgz",
+      "integrity": "sha512-YJLMF/96TpKXaEQwaLEo+Z4NDK8aV133ROF6xp9pe3gQoS7sxfpXh4Rv9eC+8vCvWfmDjRQaMSlRPbO+9G6jgA==",
+      "dev": true,
+      "requires": {
+        "loud-rejection": "^1.6.0",
+        "memory-fs": "~0.4.1",
+        "mime": "^2.3.1",
+        "path-is-absolute": "^1.0.0",
+        "range-parser": "^1.0.3",
+        "url-join": "^4.0.0",
+        "webpack-log": "^2.0.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
+          "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-dev-server": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.7.tgz",
+      "integrity": "sha512-KagFrNHf3QKndS61cXqzkQ4gpdXo0d1LZTTplAJzNK1Ev2ZyJiu+BzerW/2dixYYfpnGzp0AcvCXpmYXIOkFOA==",
+      "dev": true,
+      "requires": {
+        "ansi-html": "0.0.7",
+        "bonjour": "^3.5.0",
+        "chokidar": "^2.0.0",
+        "compression": "^1.5.2",
+        "connect-history-api-fallback": "^1.3.0",
+        "debug": "^3.1.0",
+        "del": "^3.0.0",
+        "express": "^4.16.2",
+        "html-entities": "^1.2.0",
+        "http-proxy-middleware": "~0.18.0",
+        "import-local": "^1.0.0",
+        "internal-ip": "^3.0.1",
+        "ip": "^1.1.5",
+        "killable": "^1.0.0",
+        "loglevel": "^1.4.1",
+        "opn": "^5.1.0",
+        "portfinder": "^1.0.9",
+        "schema-utils": "^1.0.0",
+        "selfsigned": "^1.9.1",
+        "serve-index": "^1.7.2",
+        "sockjs": "0.3.19",
+        "sockjs-client": "1.1.5",
+        "spdy": "^3.4.1",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^5.1.0",
+        "webpack-dev-middleware": "3.2.0",
+        "webpack-log": "^2.0.0",
+        "yargs": "12.0.1"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          },
+          "dependencies": {
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "cross-spawn": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+          "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^4.0.1",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "decamelize": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz",
+          "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==",
+          "dev": true,
+          "requires": {
+            "xregexp": "4.0.0"
+          }
+        },
+        "execa": {
+          "version": "0.7.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+          "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^5.0.1",
+            "get-stream": "^3.0.0",
+            "is-stream": "^1.1.0",
+            "npm-run-path": "^2.0.0",
+            "p-finally": "^1.0.0",
+            "signal-exit": "^3.0.0",
+            "strip-eof": "^1.0.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+          "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
+          "dev": true,
+          "requires": {
+            "execa": "^0.7.0",
+            "lcid": "^1.0.0",
+            "mem": "^1.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
+          "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          },
+          "dependencies": {
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.1",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz",
+          "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^2.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^2.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^10.1.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "webpack-log": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
+      "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^3.0.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "webpack-merge": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz",
+      "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.5"
+      }
+    },
+    "webpack-sources": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
+      "integrity": "sha512-9BZwxR85dNsjWz3blyxdOhTgtnQvv3OEs5xofI0wPYTwu5kaWxS08UuD1oI7WLBLpRO+ylf0ofnXLXWmGb2WMw==",
+      "dev": true,
+      "requires": {
+        "source-list-map": "^2.0.0",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-subresource-integrity": {
+      "version": "1.1.0-rc.4",
+      "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz",
+      "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=",
+      "dev": true,
+      "requires": {
+        "webpack-core": "^0.6.8"
+      }
+    },
+    "websocket-driver": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+      "dev": true,
+      "requires": {
+        "http-parser-js": ">=0.4.0",
+        "websocket-extensions": ">=0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+      "dev": true
+    },
+    "when": {
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
+      "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=",
+      "dev": true
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true,
+      "optional": true
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "worker-farm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
+      "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
+      "dev": true,
+      "requires": {
+        "errno": "~0.1.7"
+      }
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "ws": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz",
+      "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=",
+      "dev": true,
+      "requires": {
+        "options": ">=0.0.5",
+        "ultron": "1.0.x"
+      }
+    },
+    "wtf-8": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz",
+      "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=",
+      "dev": true
+    },
+    "xml2js": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+      "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+      "dev": true,
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~9.0.1"
+      },
+      "dependencies": {
+        "sax": {
+          "version": "1.2.4",
+          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+          "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+          "dev": true
+        }
+      }
+    },
+    "xmlbuilder": {
+      "version": "9.0.7",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+      "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
+      "dev": true
+    },
+    "xmldom": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
+      "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=",
+      "dev": true
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
+      "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=",
+      "dev": true
+    },
+    "xregexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz",
+      "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "xxhashjs": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
+      "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
+      "dev": true,
+      "requires": {
+        "cuint": "^0.2.2"
+      }
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+      "dev": true
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+      "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^3.0.0",
+        "cliui": "^3.2.0",
+        "decamelize": "^1.1.1",
+        "get-caller-file": "^1.0.1",
+        "os-locale": "^1.4.0",
+        "read-pkg-up": "^1.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^1.0.1",
+        "set-blocking": "^2.0.0",
+        "string-width": "^1.0.2",
+        "which-module": "^1.0.0",
+        "y18n": "^3.2.1",
+        "yargs-parser": "^5.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true,
+          "optional": true
+        },
+        "y18n": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+          "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+      "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^3.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "yauzl": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+      "dev": true,
+      "requires": {
+        "fd-slicer": "~1.0.1"
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
+      "dev": true
+    },
+    "yn": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+      "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+      "dev": true
+    },
+    "zone.js": {
+      "version": "0.8.26",
+      "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz",
+      "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA=="
+    }
+  }
+}
diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json
new file mode 100644 (file)
index 0000000..9174d1b
--- /dev/null
@@ -0,0 +1,84 @@
+{
+  "name": "eg",
+  "version": "0.0.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve",
+    "build": "ng build",
+    "test": "npm run create-mock-idl; ng test",
+    "lint": "ng lint",
+    "e2e": "ng e2e",
+    "create-mock-idl": "cd src/test_data && perl idl2js.pl",
+    "export-strings": "ng xi18n --output-path locale",
+    "merge-strings": "xliffmerge",
+    "build-fr-CA": "ng build --configuration=production-fr-CA --output-path ../../web/eg2/fr-CA --deploy-url /eg2/fr-CA/ --base-href /eg2/fr-CA; sed -i s/IDL2js\\\"/IDL2js?locale=fr-CA\\\"/g ../../web/eg2/fr-CA/index.html; sed -i s/lang=\\\"en\\\"/lang=\\\"fr\\\"/g ../../web/eg2/fr-CA/index.html"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "^6.1.0",
+    "@angular/common": "^6.1.0",
+    "@angular/compiler": "^6.1.0",
+    "@angular/core": "^6.1.0",
+    "@angular/forms": "^6.1.0",
+    "@angular/http": "^6.1.0",
+    "@angular/platform-browser": "^6.1.0",
+    "@angular/platform-browser-dynamic": "^6.1.0",
+    "@angular/router": "^6.1.0",
+    "@ng-bootstrap/ng-bootstrap": "^3.2.0",
+    "bootstrap-css-only": "^4.1.1",
+    "core-js": "^2.5.4",
+    "ngx-cookie": "^4.0.2",
+    "rxjs": "^6.0.0",
+    "rxjs-compat": "^6.3.2",
+    "zone.js": "~0.8.26"
+  },
+  "devDependencies": {
+    "@angular-devkit/build-angular": "~0.7.0",
+    "@angular/cli": "~6.1.5",
+    "@angular/compiler-cli": "^6.1.0",
+    "@angular/language-service": "^6.1.0",
+    "@types/jasmine": "~2.8.6",
+    "@types/jasminewd2": "~2.0.3",
+    "@types/node": "~8.9.4",
+    "codelyzer": "~4.2.1",
+    "jasmine-core": "~2.99.1",
+    "jasmine-spec-reporter": "~4.2.1",
+    "karma": "~1.7.1",
+    "karma-chrome-launcher": "~2.2.0",
+    "karma-coverage-istanbul-reporter": "~2.0.0",
+    "karma-jasmine": "~1.1.1",
+    "karma-jasmine-html-reporter": "^0.2.2",
+    "karma-phantomjs-launcher": "^1.0.4",
+    "ngx-i18nsupport": "^0.17.0",
+    "protractor": "~5.4.0",
+    "ts-node": "~5.0.1",
+    "tslint": "~5.9.1",
+    "typescript": "~2.7.2"
+  },
+  "xliffmergeOptions": {
+    "srcDir": "src/locale",
+    "genDir": "src/locale",
+    "i18nFile": "messages.xlf",
+    "i18nBaseFile": "messages",
+    "i18nFormat": "xlf",
+    "encoding": "UTF-8",
+    "defaultLanguage": "en",
+    "languages": [
+      "en",
+      "fr-CA"
+    ],
+    "removeUnusedIds": true,
+    "supportNgxTranslate": false,
+    "ngxTranslateExtractionPattern": "@@|ngx-translate",
+    "useSourceAsTarget": true,
+    "targetPraefix": "",
+    "targetSuffix": "",
+    "beautifyOutput": false,
+    "allowIdChange": false,
+    "autotranslate": false,
+    "apikey": "",
+    "apikeyfile": "",
+    "verbose": false,
+    "quiet": false
+  }
+}
diff --git a/Open-ILS/src/eg2/protractor.conf.js b/Open-ILS/src/eg2/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/src/eg2/src/app/app.component.ts b/Open-ILS/src/eg2/src/app/app.component.ts
new file mode 100644 (file)
index 0000000..3f95092
--- /dev/null
@@ -0,0 +1,11 @@
+import {Component} from '@angular/core';
+
+@Component({
+  selector: 'eg-root',
+  template: '<router-outlet></router-outlet><eg-print></eg-print>'
+})
+
+export class BaseComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/app.module.ts b/Open-ILS/src/eg2/src/app/app.module.ts
new file mode 100644 (file)
index 0000000..20de8ab
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * BaseModule is the shared starting point for all apps.  It provides
+ * the root route and a simple welcome page for users that end up here
+ * accidentally.
+ */
+import {BrowserModule} from '@angular/platform-browser';
+import {NgModule} from '@angular/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; // ng-bootstrap
+import {CookieModule} from 'ngx-cookie'; // import CookieMonster
+
+import {EgCommonModule} from './common.module';
+import {BaseComponent} from './app.component';
+import {BaseRoutingModule} from './routing.module';
+import {WelcomeComponent} from './welcome.component';
+
+@NgModule({
+  declarations: [
+    BaseComponent,
+    WelcomeComponent
+  ],
+  imports: [
+    EgCommonModule.forRoot(),
+    BaseRoutingModule,
+    BrowserModule,
+    NgbModule.forRoot(),
+    CookieModule.forRoot()
+  ],
+  exports: [],
+  bootstrap: [BaseComponent]
+})
+
+export class BaseModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
new file mode 100644 (file)
index 0000000..c83ad39
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Modules, services, and components used by all apps.
+ */
+import {CommonModule, DatePipe, CurrencyPipe} from '@angular/common';
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {FormsModule} from '@angular/forms';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+
+/*
+Note core services are injected into 'root'.
+They do not have to be added to the providers list.
+*/
+
+// consider moving these to core...
+import {FormatService} from '@eg/core/format.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+// Globally available components
+import {PrintComponent} from '@eg/share/print/print.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+
+@NgModule({
+  declarations: [
+    PrintComponent,
+    DialogComponent,
+    ConfirmDialogComponent,
+    PromptDialogComponent,
+    ProgressInlineComponent,
+    ProgressDialogComponent
+  ],
+  imports: [
+    CommonModule,
+    FormsModule,
+    RouterModule,
+    NgbModule
+  ],
+  exports: [
+    CommonModule,
+    RouterModule,
+    NgbModule,
+    FormsModule,
+    PrintComponent,
+    DialogComponent,
+    ConfirmDialogComponent,
+    PromptDialogComponent,
+    ProgressInlineComponent,
+    ProgressDialogComponent
+  ]
+})
+
+export class EgCommonModule {
+    /** forRoot() lets us define services that should only be
+     * instantiated once for all loaded routes */
+    static forRoot(): ModuleWithProviders {
+        return {
+            ngModule: EgCommonModule,
+            providers: [
+                DatePipe,
+                CurrencyPipe,
+                PrintService,
+                FormatService
+            ]
+        };
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/README b/Open-ILS/src/eg2/src/app/core/README
new file mode 100644 (file)
index 0000000..3cf0ec4
--- /dev/null
@@ -0,0 +1,9 @@
+Core Angular services and assocated types/classes.
+
+Core services are imported and exported by the base module and 
+automatically added as dependencies to ALL applications.
+
+1. Only add services here that are universally required.
+2. Avoid URL path navigation in the core services as paths will vary 
+   by application.
+
diff --git a/Open-ILS/src/eg2/src/app/core/auth.service.ts b/Open-ILS/src/eg2/src/app/core/auth.service.ts
new file mode 100644 (file)
index 0000000..dad2acd
--- /dev/null
@@ -0,0 +1,341 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {NetService} from './net.service';
+import {EventService, EgEvent} from './event.service';
+import {IdlService, IdlObject} from './idl.service';
+import {StoreService} from './store.service';
+
+// Not universally available.
+declare var BroadcastChannel;
+
+// Models a login instance.
+class AuthUser {
+    user:        IdlObject; // actor.usr (au) object
+    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 AuthLoginArgs {
+    username: string;
+    password: string;
+    type: string;
+    workstation?: string;
+}
+
+export enum AuthWsState {
+    PENDING,
+    NOT_USED,
+    NOT_FOUND_SERVER,
+    NOT_FOUND_LOCAL,
+    VALID
+}
+
+@Injectable({providedIn: 'root'})
+export class AuthService {
+
+    private authChannel: any;
+
+    private activeUser: AuthUser = null;
+
+    workstationState: AuthWsState = AuthWsState.PENDING;
+
+    // Used by auth-checking resolvers
+    redirectUrl: string;
+
+    // reference to active auth validity setTimeout handler.
+    pollTimeout: any;
+
+    constructor(
+        private egEvt: EventService,
+        private net: NetService,
+        private store: StoreService
+    ) {
+
+        // BroadcastChannel is not yet defined in PhantomJS and elsewhere
+        this.authChannel = (typeof BroadcastChannel === 'undefined') ?
+            {} : new BroadcastChannel('eg.auth');
+    }
+
+    // Returns true if we are currently in op-change mode.
+    opChangeIsActive(): boolean {
+        return Boolean(this.store.getLoginSessionItem('eg.auth.time.oc'));
+    }
+
+    // - Accessor functions always refer to the active user.
+
+    user(): IdlObject {
+        return this.activeUser ? this.activeUser.user : null;
+    }
+
+    // Workstation name.
+    workstation(): string {
+        return this.activeUser ? this.activeUser.workstation : null;
+    }
+
+    token(): string {
+        return this.activeUser ? this.activeUser.token : null;
+    }
+
+    authtime(): number {
+        return this.activeUser ? this.activeUser.authtime : 0;
+    }
+
+    // NOTE: NetService emits an event if the auth session has expired.
+    // This only rejects when no authtoken is found.
+    testAuthToken(): Promise<any> {
+
+        if (!this.activeUser) {
+            // Only necessary on new page loads.  During op-change,
+            // for example, we already have an activeUser.
+            this.activeUser = new AuthUser(
+                this.store.getLoginSessionItem('eg.auth.token'),
+                this.store.getLoginSessionItem('eg.auth.time')
+            );
+        }
+
+        if (!this.token()) {
+            return Promise.reject('no authtoken');
+        }
+
+        return this.net.request(
+            'open-ils.auth',
+            'open-ils.auth.session.retrieve', this.token()).toPromise()
+        .then(user => {
+            // NetService interceps NO_SESSION events.
+            // We can only get here if the session is valid.
+            this.activeUser.user = user;
+            this.listenForLogout();
+            this.sessionPoll();
+        });
+    }
+
+    loginApi(args: AuthLoginArgs, service: string,
+        method: string, isOpChange?: boolean): Promise<void> {
+
+        return this.net.request(service, method, args)
+        .toPromise().then(res => {
+            return this.handleLoginResponse(
+                args, this.egEvt.parse(res), isOpChange);
+        });
+    }
+
+    login(args: AuthLoginArgs, isOpChange?: boolean): Promise<void> {
+        let service = 'open-ils.auth';
+        let method = 'open-ils.auth.login';
+
+        return this.net.request(
+            'open-ils.auth_proxy',
+            'open-ils.auth_proxy.enabled')
+        .toPromise().then(
+            enabled => {
+                if (Number(enabled) === 1) {
+                    service = 'open-ils.auth_proxy';
+                    method = 'open-ils.auth_proxy.login';
+                }
+                return this.loginApi(args, service, method, isOpChange);
+            },
+            error => {
+                // auth_proxy check resulted in a low-level error.
+                // Likely the service is not running.  Fall back to
+                // standard auth login.
+                return this.loginApi(args, service, method, isOpChange);
+            }
+        );
+    }
+
+    handleLoginResponse(
+        args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
+
+        switch (evt.textcode) {
+            case 'SUCCESS':
+                return this.handleLoginOk(args, evt, isOpChange);
+
+            case 'WORKSTATION_NOT_FOUND':
+                console.error(`No such workstation "${args.workstation}"`);
+                this.workstationState = AuthWsState.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: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
+
+        if (isOpChange) {
+            this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
+            this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
+        }
+
+        this.activeUser = new AuthUser(
+            evt.payload.authtoken,
+            evt.payload.authtime,
+            args.workstation
+        );
+
+        this.store.setLoginSessionItem('eg.auth.token', this.token());
+        this.store.setLoginSessionItem('eg.auth.time', this.authtime());
+
+        return Promise.resolve();
+    }
+
+    undoOpChange(): Promise<any> {
+        if (this.opChangeIsActive()) {
+            this.deleteSession();
+            this.activeUser = new AuthUser(
+                this.store.getLoginSessionItem('eg.auth.token.oc'),
+                this.store.getLoginSessionItem('eg.auth.time.oc'),
+                this.activeUser.workstation
+            );
+            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());
+        }
+        // Re-fetch the user.
+        return this.testAuthToken();
+    }
+
+    /**
+     * Listen for logout events initiated by other browser tabs.
+     */
+    listenForLogout(): void {
+        if (this.authChannel.onmessage) {
+            return;
+        }
+
+        this.authChannel.onmessage = (e) => {
+            console.debug(
+                `received eg.auth broadcast ${JSON.stringify(e.data)}`);
+
+            if (e.data.action === 'logout') {
+                // Logout will be handled by the originating tab.
+                // We just need to clear tab-local memory.
+                this.cleanup();
+                this.net.authExpired$.emit({viaExternal: true});
+            }
+        };
+    }
+
+    /**
+     * Force-check the validity of the authtoken on occasion.
+     * This allows us to redirect an idle staff client back to the login
+     * page after the session times out.  Otherwise, the UI would stay
+     * open with potentially sensitive data visible.
+     * TODO: What is the practical difference (for a browser) between
+     * checking auth validity and the ui.general.idle_timeout setting?
+     * Does that setting serve a purpose in a browser environment?
+     */
+    sessionPoll(): void {
+
+        // add a 5 second delay to give the token plenty of time
+        // to expire on the server.
+        let pollTime = this.authtime() * 1000 + 5000;
+
+        if (pollTime < 60000) {
+            // Never poll more often than once per minute.
+            pollTime = 60000;
+        } else if (pollTime > 2147483647) {
+            // Avoid integer overflow resulting in $timeout() effectively
+            // running with timeout=0 in a loop.
+            pollTime = 2147483647;
+        }
+
+        this.pollTimeout = setTimeout(() => {
+            this.net.request(
+                'open-ils.auth',
+                'open-ils.auth.session.retrieve',
+                this.token(),
+                0, // return extra auth details, unneeded here.
+                1  // avoid extending the auth timeout
+
+            // NetService intercepts NO_SESSION events.
+            // If the promise resolves, the session is valid.
+            ).subscribe(
+                user => this.sessionPoll(),
+                err  => console.warn('auth poll error: ' + err)
+            );
+
+        }, pollTime);
+    }
+
+
+    // Resolves if login workstation matches a workstation known to this
+    // browser instance.  No attempt is made to see if the workstation
+    // is present on the server.  That happens at login time.
+    verifyWorkstation(): Promise<void> {
+
+        if (!this.user()) {
+            this.workstationState = AuthWsState.PENDING;
+            return Promise.reject('Cannot verify workstation without user');
+        }
+
+        if (!this.user().wsid()) {
+            this.workstationState = AuthWsState.NOT_USED;
+            return Promise.reject('User has no workstation ID to verify');
+        }
+
+        return new Promise((resolve, reject) => {
+            const workstations =
+                this.store.getLocalItem('eg.workstation.all');
+
+            if (workstations) {
+                const ws = workstations.filter(
+                    w => Number(w.id) === Number(this.user().wsid()))[0];
+
+                if (ws) {
+                    this.activeUser.workstation = ws.name;
+                    this.workstationState = AuthWsState.VALID;
+                    return resolve();
+                }
+            }
+
+            this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
+            reject();
+        });
+    }
+
+    deleteSession(): void {
+        if (this.token()) {
+            // note we have to subscribe to the net.request or it will
+            // not fire -- observables only run when subscribed to.
+            this.net.request(
+                'open-ils.auth',
+                'open-ils.auth.session.delete', this.token())
+            .subscribe(x => {});
+        }
+    }
+
+    // Tell all listening browser tabs that it's time to logout.
+    // This should only be invoked by one tab.
+    broadcastLogout(): void {
+        console.debug('Notifying tabs of imminent auth token removal');
+        this.authChannel.postMessage({action : 'logout'});
+    }
+
+    // Remove/reset session data
+    cleanup(): void {
+        this.activeUser = null;
+        if (this.pollTimeout) {
+            clearTimeout(this.pollTimeout);
+            this.pollTimeout = null;
+        }
+    }
+
+    // Invalidate server auth token and clean up.
+    logout(): void {
+        this.deleteSession();
+        this.store.clearLoginSessionItems();
+        this.cleanup();
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/event.service.ts b/Open-ILS/src/eg2/src/app/core/event.service.ts
new file mode 100644 (file)
index 0000000..0bbf60b
--- /dev/null
@@ -0,0 +1,55 @@
+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({providedIn: 'root'})
+export class EventService {
+
+    /**
+     * Returns an Event if 'thing' is an event, null otherwise.
+     */
+    parse(thing: any): EgEvent {
+
+        // All events have a textcode
+        if (thing && typeof thing === 'object' && 'textcode' in thing) {
+
+            const evt = new EgEvent();
+
+            ['textcode', 'payload', 'desc', 'note', 'servertime', 'ilsperm']
+                .forEach(field => { evt[field] = thing[field]; });
+
+            evt.debug = thing.stacktrace;
+            evt.code = +(thing.ilsevent || -1);
+            evt.ilspermloc = +(thing.ilspermloc || -1);
+            evt.success = thing.textcode === 'SUCCESS';
+
+            return evt;
+        }
+
+        return null;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/event.spec.ts b/Open-ILS/src/eg2/src/app/core/event.spec.ts
new file mode 100644 (file)
index 0000000..3dfdab2
--- /dev/null
@@ -0,0 +1,47 @@
+import {EventService} from './event.service';
+
+describe('EventService', () => {
+    let service: EventService;
+    beforeEach(() => {
+        service = new EventService();
+    });
+
+    const evt = {
+        ilsevent: '12345',
+        pid: '12345',
+        desc: 'Test Event Description',
+        payload: {test : 'xyz'},
+        textcode: 'TEST_EVENT',
+        servertime: 'Wed Nov 6 16:05:50 2013'
+    };
+
+    it('should parse an event object', () => {
+        expect(service.parse(evt)).not.toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse({})).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse({abc : '123'})).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse([])).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse('STRING')).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse(true)).toBe(null);
+    });
+
+    it('should stringify an event', () => {
+        expect(service.parse(evt).toString()).toBe(
+            'Event: 12345:TEST_EVENT -> Test Event Description');
+    });
+
+});
diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts
new file mode 100644 (file)
index 0000000..2c7e388
--- /dev/null
@@ -0,0 +1,103 @@
+import {Injectable} from '@angular/core';
+import {DatePipe, CurrencyPipe} from '@angular/common';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+
+/**
+ * Format IDL vield values for display.
+ */
+
+declare var OpenSRF;
+
+export interface FormatParams {
+    value: any;
+    idlClass?: string;
+    idlField?: string;
+    datatype?: string;
+    orgField?: string; // 'shortname' || 'name'
+    datePlusTime?: boolean;
+}
+
+@Injectable({providedIn: 'root'})
+export class FormatService {
+
+    dateFormat = 'shortDate';
+    dateTimeFormat = 'short';
+    wsOrgTimezone: string = OpenSRF.tz;
+
+    constructor(
+        private datePipe: DatePipe,
+        private currencyPipe: CurrencyPipe,
+        private idl: IdlService,
+        private org: OrgService
+    ) {
+
+        // Create an inilne polyfill for Number.isNaN, which is
+        // not available in PhantomJS for unit testing.
+        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
+        if (!Number.isNaN) {
+            // "The following works because NaN is the only value
+            // in javascript which is not equal to itself."
+            Number.isNaN = (value: any) => {
+                return value !== value;
+            };
+        }
+    }
+
+    /**
+     * Create a human-friendly display version of any field type.
+     */
+    transform(params: FormatParams): string {
+        const value = params.value;
+
+        if (   value === undefined
+            || value === null
+            || value === ''
+            || Number.isNaN(value)) {
+            return '';
+        }
+
+        let datatype = params.datatype;
+
+        if (!datatype) {
+            if (params.idlClass && params.idlField) {
+                datatype = this.idl.classes[params.idlClass]
+                    .field_map[params.idlField].datatype;
+            } else {
+                // Assume it's a primitive value
+                return value + '';
+            }
+        }
+
+        switch (datatype) {
+
+            case 'org_unit':
+                const orgField = params.orgField || 'shortname';
+                const org = this.org.get(value);
+                return org ? org[orgField]() : '';
+
+            case 'timestamp':
+                const date = new Date(value);
+                let fmt = this.dateFormat || 'shortDate';
+                if (params.datePlusTime) {
+                    fmt = this.dateTimeFormat || 'short';
+                }
+                return this.datePipe.transform(date, fmt);
+
+            case 'money':
+                return this.currencyPipe.transform(value);
+
+            case 'bool':
+                // Slightly better than a bare 't' or 'f'.
+                // Should probably add a global true/false string.
+                return Boolean(
+                    value === 't' || value === 1 ||
+                    value === '1' || value === true
+                ).toString();
+
+            default:
+                return value + '';
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/format.spec.ts b/Open-ILS/src/eg2/src/app/core/format.spec.ts
new file mode 100644 (file)
index 0000000..05991df
--- /dev/null
@@ -0,0 +1,90 @@
+import {DatePipe, CurrencyPipe} from '@angular/common';
+import {IdlService} from './idl.service';
+import {EventService} from './event.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+import {StoreService} from './store.service';
+import {OrgService} from './org.service';
+import {FormatService} from './format.service';
+
+
+describe('FormatService', () => {
+
+    let currencyPipe: CurrencyPipe;
+    let datePipe: DatePipe;
+    let idlService: IdlService;
+    let netService: NetService;
+    let authService: AuthService;
+    let pcrudService: PcrudService;
+    let orgService: OrgService;
+    let evtService: EventService;
+    let storeService: StoreService;
+    let service: FormatService;
+
+    beforeEach(() => {
+        currencyPipe = new CurrencyPipe('en');
+        datePipe = new DatePipe('en');
+        idlService = new IdlService();
+        evtService = new EventService();
+        storeService = new StoreService(null /* CookieService */);
+        netService = new NetService(evtService);
+        authService = new AuthService(evtService, netService, storeService);
+        pcrudService = new PcrudService(idlService, netService, authService);
+        orgService = new OrgService(netService, authService, pcrudService);
+        service = new FormatService(
+            datePipe,
+            currencyPipe,
+            idlService,
+            orgService
+        );
+    });
+
+    const initTestData = () => {
+        idlService.parseIdl();
+        const win: any = window; // trick TS
+        win._eg_mock_data.generateOrgTree(idlService, orgService);
+    };
+
+    it('should format an org unit name', () => {
+        initTestData();
+        const str = service.transform({
+            value: orgService.root(),
+            datatype: 'org_unit',
+            orgField: 'shortname' // currently the default
+        });
+        expect(str).toBe('ROOT');  // from eg_mock.js
+    });
+
+    it('should format a date', () => {
+        initTestData();
+        const str = service.transform({
+            value: new Date(2018, 6, 5),
+            datatype: 'timestamp',
+        });
+        expect(str).toBe('7/5/18');
+    });
+
+    it('should format a date plus time', () => {
+        initTestData();
+        const str = service.transform({
+            value: new Date(2018, 6, 5, 12, 30, 1),
+            datatype: 'timestamp',
+            datePlusTime: true
+        });
+        expect(str).toBe('7/5/18, 12:30 PM');
+    });
+
+
+
+    it('should format money', () => {
+        initTestData();
+        const str = service.transform({
+            value: '12.1',
+            datatype: 'money'
+        });
+        expect(str).toBe('$12.10');
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/core/idl.service.ts b/Open-ILS/src/eg2/src/app/core/idl.service.ts
new file mode 100644 (file)
index 0000000..89f8411
--- /dev/null
@@ -0,0 +1,137 @@
+import {Injectable} from '@angular/core';
+
+// Added globally by /IDL2js
+declare var _preload_fieldmapper_IDL: Object;
+
+/**
+ * Every IDL object class implements this interface.
+ */
+export interface IdlObject {
+    a: any[];
+    classname: string;
+    _isfieldmapper: boolean;
+    // Dynamically appended functions from the IDL.
+    [fields: string]: any;
+}
+
+@Injectable({providedIn: 'root'})
+export class IdlService {
+
+    classes: any = {}; // IDL class metadata
+    constructors = {}; // IDL instance generators
+
+    /**
+     * Create a new IDL object instance.
+     */
+    create(cls: string, seed?: any[]): IdlObject {
+        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.
+         */
+        const mkclass = (cls, fields) => {
+            this.classes[cls].classname = cls;
+
+            // This dance lets us encode each IDL object with the
+            // IdlObject interface.  Useful for adding type restrictions
+            // where desired for functions, etc.
+            const generator: any = ((): IdlObject => {
+
+                const 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];
+                    };
+
+                    if (!field.label) {
+                        field.label = field.name;
+                    }
+
+                    // Coerce 'aou' links to datatype org_unit for consistency.
+                    if (field.datatype === 'link' && field.class === 'aou') {
+                        field.datatype = 'org_unit';
+                    }
+                });
+
+                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];
+        };
+
+        Object.keys(this.classes).forEach(class_ => {
+            mkclass(class_, this.classes[class_].fields);
+        });
+    }
+
+    // Makes a deep copy of an IdlObject's / structures containing
+    // IdlObject's.  Note we don't use JSON cross-walk because our
+    // JSON lib does not handle circular references.
+    // @depth specifies the maximum number of steps through IdlObject'
+    // we will traverse.
+    clone(source: any, depth?: number): any {
+        if (depth === undefined) {
+            depth = 100;
+        }
+
+        let result;
+        if (typeof source === 'undefined' || source === null) {
+            return source;
+
+        } else if (source._isfieldmapper) {
+            // same depth because we're still cloning this same object
+            result = this.create(source.classname, this.clone(source.a, depth));
+
+        } else {
+            if (Array.isArray(source)) {
+                result = [];
+            } else if (typeof source === 'object') { // source is not null
+                result = {};
+            } else {
+                return source; // primitive
+            }
+
+            for (const j in source) {
+                if (source[j] === null || typeof source[j] === 'undefined') {
+                    result[j] = source[j];
+                } else if (source[j]._isfieldmapper) {
+                    if (depth) {
+                        result[j] = this.clone(source[j], depth - 1);
+                    }
+                } else {
+                    result[j] = this.clone(source[j], depth);
+                }
+            }
+        }
+
+        return result;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
new file mode 100644 (file)
index 0000000..8138bf4
--- /dev/null
@@ -0,0 +1,28 @@
+import {IdlService} from './idl.service';
+
+describe('IdlService', () => {
+    let service: IdlService;
+    beforeEach(() => {
+        service = new IdlService();
+    });
+
+    it('should parse the IDL', () => {
+        service.parseIdl();
+        expect(service.classes['aou'].fields.length).toBeGreaterThan(0);
+    });
+
+    it('should create an aou object', () => {
+        service.parseIdl();
+        const org = service.create('aou');
+        expect(typeof org.id).toBe('function');
+    });
+
+    it('should create an aou object with accessor/mutators', () => {
+        service.parseIdl();
+        const org = service.create('aou');
+        org.name('AN ORG');
+        expect(org.name()).toBe('AN ORG');
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/core/locale.service.ts b/Open-ILS/src/eg2/src/app/core/locale.service.ts
new file mode 100644 (file)
index 0000000..0ffbfd4
--- /dev/null
@@ -0,0 +1,69 @@
+import {Injectable} from '@angular/core';
+import {Location} from '@angular/common';
+import {environment} from '../../environments/environment';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs';
+import {CookieService} from 'ngx-cookie';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Injectable({providedIn: 'root'})
+export class LocaleService {
+
+    constructor(
+        private ngLocation: Location,
+        private cookieService: CookieService,
+        private pcrud: PcrudService) {
+    }
+
+    setLocale(code: string) {
+        let url = this.ngLocation.prepareExternalUrl('/');
+
+        // The last part of the base path will be the locale
+        // Replace it with the selected locale
+        url = url.replace(/\/[a-z]{2}-[A-Z]{2}\/$/, `/${code}`);
+
+        // Finally tack the path of the current page back onto the URL
+        // which is more friendly than forcing them back to the splash page.
+        url += this.ngLocation.path();
+
+        // Set a 10-year locale cookie to maintain compatibility
+        // with the AngularJS client.
+        // Cookie takes the form aa_bb instead of aa-BB
+        const cookie = code.replace(/-/, '_').toLowerCase();
+        this.cookieService.put('eg_locale',
+            cookie, {path : '/', secure: true, expires: '+10y'});
+
+        window.location.href = url;
+    }
+
+    // Returns codes supported for the current environment.
+    supportedLocaleCodes(): string[] {
+        return environment.locales || [];
+    }
+
+    // Returns i18n_l objects matching the locales supported
+    // in the current environment.
+    supportedLocales(): Observable<IdlObject> {
+        const locales = this.supportedLocaleCodes();
+
+        if (locales.length === 0) {
+            return of();
+        }
+
+        return this.pcrud.search('i18n_l', {code: locales});
+    }
+
+    // Extract the local from the URL.
+    // It's the last component of the base path.
+    // Note we don't extract it from the cookie since using cookies
+    // to store the locale will not be necessary when AngularJS
+    // is deprecated.
+    currentLocaleCode(): string {
+        const base = this.ngLocation.prepareExternalUrl('/');
+        const code = base.match(/\/([a-z]{2}-[A-Z]{2})\/$/);
+        return code ? code[1] : '';
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/net.service.ts b/Open-ILS/src/eg2/src/app/core/net.service.ts
new file mode 100644 (file)
index 0000000..3c3435b
--- /dev/null
@@ -0,0 +1,187 @@
+/**
+ *
+ * constructor(private net : NetService) {
+ *   ...
+ *   this.net.request(service, method, param1 [, param2, ...])
+ *     .subscribe(
+ *       (res) => console.log('received one resopnse: ' + res),
+ *       (err) => console.error('recived request error: ' + err),
+ *       ()    => console.log('request complete')
+ *     )
+ *   );
+ *   ...
+ *
+ *  // Example translating a net request into a promise.
+ *  this.net.request(service, method, param1)
+ *  .toPromise().then(result => console.log(result));
+ *
+ * }
+ *
+ * Each response is relayed via Observable.next().  The interface is
+ * the same for streaming and atomic requests.
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {EventService, EgEvent} from './event.service';
+
+// Global vars from opensrf.js
+// These are availavble at runtime, but are not exported.
+declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
+
+export class NetRequest {
+    service: string;
+    method: string;
+    params: any[];
+    observer: Observer<any>;
+    superseded = false;
+    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
+    session?: any;
+    // True if we're using a single-use local session
+    localSession = true;
+
+    // Last Event encountered by this request.
+    // Most callers will not need to import Event 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;
+            this.localSession = false;
+        } else {
+            this.session = new OpenSRF.ClientSession(service);
+        }
+    }
+}
+
+export interface AuthExpiredEvent {
+    // request is set when the auth expiration was determined as a
+    // by-product of making an API call.
+    request?: NetRequest;
+
+    // True if this environment (e.g. browser tab) was notified of the
+    // expired auth token from an external source (e.g. another browser tab).
+    viaExternal?: boolean;
+}
+
+@Injectable({providedIn: 'root'})
+export class NetService {
+
+    permFailed$: EventEmitter<NetRequest>;
+    authExpired$: EventEmitter<AuthExpiredEvent>;
+
+    // If true, permission failures are emitted via permFailed$
+    // and the active request is marked as superseded.
+    permFailedHasHandler: Boolean = false;
+
+    constructor(
+        private egEvt: EventService
+    ) {
+        this.permFailed$ = new EventEmitter<NetRequest>();
+        this.authExpired$ = new EventEmitter<AuthExpiredEvent>();
+    }
+
+    // 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 NetRequest(service, method, params));
+    }
+
+    // Request with pre-compiled NetRequest
+    requestCompiled(request: NetRequest): Observable<any> {
+        return Observable.create(
+            observer => {
+                request.observer = observer;
+                this.sendCompiledRequest(request);
+            }
+        );
+    }
+
+    // Send the compiled request to the server via WebSockets
+    sendCompiledRequest(request: NetRequest): void {
+        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
+        console.debug(`Net: request ${request.method}`);
+
+        request.session.request({
+            async  : true, // WS only operates in async mode
+            method : request.method,
+            params : request.params,
+            oncomplete : () => {
+
+                // TODO: teach opensrf.js to call cleanup() inside
+                // disconnect() and teach Pcrud to call cleanup()
+                // as needed to avoid long-lived session data bloat.
+                if (request.localSession) {
+                    request.session.cleanup();
+                }
+
+                // A superseded request will be complete()'ed by the
+                // superseder at a later time.
+                if (!request.superseded) {
+                    request.observer.complete();
+                }
+            },
+            onresponse : r => {
+                this.dispatchResponse(request, r.recv().content());
+            },
+            onerror : errmsg => {
+                const msg = `${request.method} failed! See server logs. ${errmsg}`;
+                console.error(msg);
+                request.observer.error(msg);
+            },
+            onmethoderror : (req, statCode, statMsg) => {
+                const msg =
+                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
+                console.error(msg);
+
+                if (request.service === 'open-ils.pcrud'
+                    && Number(statCode) === 401) {
+                    // 401 is the PCRUD equivalent of a NO_SESSION event
+                    this.authExpired$.emit({request: 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(request, response): void {
+        request.evt = this.egEvt.parse(response);
+
+        if (request.evt) {
+            switch (request.evt.textcode) {
+
+                case 'NO_SESSION':
+                    console.debug(`Net emitting event: ${request.evt}`);
+                    request.observer.error(request.evt.toString());
+                    this.authExpired$.emit({request: request});
+                    return;
+
+                case 'PERM_FAILURE':
+                    if (this.permFailedHasHandler) {
+                        console.debug(`Net 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/src/eg2/src/app/core/org.service.ts b/Open-ILS/src/eg2/src/app/core/org.service.ts
new file mode 100644 (file)
index 0000000..38faaff
--- /dev/null
@@ -0,0 +1,278 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {IdlObject, IdlService} from './idl.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+
+type OrgNodeOrId = number | IdlObject;
+
+interface OrgFilter {
+    canHaveUsers?: boolean;
+    canHaveVolumes?: boolean;
+    opacVisible?: boolean;
+    inList?: number[];
+    notInList?: number[];
+}
+
+interface OrgSettingsBatch {
+    [key: string]: any;
+}
+
+@Injectable({providedIn: 'root'})
+export class OrgService {
+
+    private orgList: IdlObject[] = [];
+    private orgTree: IdlObject; // root node + children
+    private orgMap: {[id: number]: IdlObject} = {};
+    private settingsCache: OrgSettingsBatch = {};
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService
+    ) {}
+
+    get(nodeOrId: OrgNodeOrId): IdlObject {
+        if (typeof nodeOrId === 'object') {
+            return nodeOrId;
+        }
+        return this.orgMap[nodeOrId];
+    }
+
+    list(): IdlObject[] {
+        return this.orgList;
+    }
+
+    /**
+     * Returns a list of org units that match the selected criteria.
+     * All filters must match for an org to be included in the result set.
+     * Unset filter options are ignored.
+     */
+    filterList(filter: OrgFilter, asId?: boolean): any[] {
+        const list = [];
+        this.list().forEach(org => {
+
+            const chu = filter.canHaveUsers;
+            if (chu && !this.canHaveUsers(org)) { return; }
+            if (chu === false && this.canHaveUsers(org)) { return; }
+
+            const chv = filter.canHaveVolumes;
+            if (chv && !this.canHaveVolumes(org)) { return; }
+            if (chv === false && this.canHaveVolumes(org)) { return; }
+
+            const ov = filter.opacVisible;
+            if (ov && !this.opacVisible(org)) { return; }
+            if (ov === false && this.opacVisible(org)) { return; }
+
+            if (filter.inList && !filter.inList.includes(org.id())) {
+                return;
+            }
+
+            if (filter.notInList && filter.notInList.includes(org.id())) {
+                return;
+            }
+
+            // All filter tests passed.  Add it to the list
+            list.push(asId ? org.id() : org);
+        });
+
+        return list;
+    }
+
+    tree(): IdlObject {
+        return this.orgTree;
+    }
+
+    // get the root OU
+    root(): IdlObject {
+        return this.orgList[0];
+    }
+
+    // list of org_unit objects or IDs for ancestors + me
+    ancestors(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
+        let node = this.get(nodeOrId);
+        if (!node) { return []; }
+        const 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: OrgNodeOrId, asId?: boolean): any[] {
+        const node = this.get(nodeOrId);
+        if (!node) { return []; }
+        const nodes = [];
+        const descend = (n) => {
+            nodes.push(n);
+            n.children().forEach(descend);
+        };
+        descend(node);
+        if (asId) {
+            return nodes.map(n => n.id());
+        }
+        return nodes;
+    }
+
+    // list of org_unit objects or IDs for ancestors + me + descendants
+    fullPath(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
+        const list = this.ancestors(nodeOrId, false).concat(
+          this.descendants(nodeOrId, false).slice(1));
+        if (asId) {
+            return list.map(n => n.id());
+        }
+        return list;
+    }
+
+    sortTree(sortField?: string, node?: IdlObject): 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?: IdlObject): 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();
+        });
+    }
+
+    /**
+     * Populate 'target' with settings from cache where available.
+     * Return the list of settings /not/ pulled from cache.
+     */
+    private settingsFromCache(names: string[], target: any) {
+        const cacheKeys = Object.keys(this.settingsCache);
+
+        cacheKeys.forEach(key => {
+            const matchIdx = names.indexOf(key);
+            if (matchIdx > -1) {
+                target[key] = this.settingsCache[key];
+                names.splice(matchIdx, 1);
+            }
+        });
+
+        return names;
+    }
+
+    /**
+     * Fetch org settings from the network.
+     * 'auth' is null for anonymous lookup.
+     */
+    private settingsFromNet(orgId: number,
+        names: string[], auth?: string): Promise<any> {
+
+        const settings = {};
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.ou_setting.ancestor_default.batch',
+                orgId, names, auth
+            ).subscribe(
+                blob => {
+                    Object.keys(blob).forEach(key => {
+                        const val = blob[key]; // null or hash
+                        settings[key] = val ? val.value : null;
+                    });
+                    resolve(settings);
+                },
+                err => reject(err)
+            );
+        });
+    }
+
+
+    /**
+     *
+     */
+    settings(names: string[],
+        orgId?: number, anonymous?: boolean): Promise<OrgSettingsBatch> {
+
+        const settings = {};
+        let auth: string = null;
+        let useCache = false;
+
+        if (this.auth.user()) {
+            if (orgId) {
+                useCache = Number(orgId) === Number(this.auth.user().ws_ou());
+            } else {
+                orgId = this.auth.user().ws_ou();
+                useCache = true;
+            }
+
+            // avoid passing auth token when anonymous is requested.
+            if (!anonymous) {
+                auth = this.auth.token();
+            }
+
+        } else if (!anonymous) {
+            return Promise.reject(
+                'Use "anonymous" To retrieve org settings without an authtoken');
+        }
+
+        if (useCache) {
+            names = this.settingsFromCache(names, settings);
+        }
+
+        // All requested settings found in cache (or name list is empty)
+        if (names.length === 0) {
+            return Promise.resolve(settings);
+        }
+
+        return this.settingsFromNet(orgId, names, auth)
+        .then(sets => {
+            if (useCache) {
+                Object.keys(sets).forEach(key => {
+                    this.settingsCache[key] = sets[key];
+                });
+            }
+            return sets;
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/org.spec.ts b/Open-ILS/src/eg2/src/app/core/org.spec.ts
new file mode 100644 (file)
index 0000000..78c2f26
--- /dev/null
@@ -0,0 +1,66 @@
+import {IdlService} from './idl.service';
+import {EventService} from './event.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+import {StoreService} from './store.service';
+import {OrgService} from './org.service';
+
+describe('OrgService', () => {
+    let idlService: IdlService;
+    let netService: NetService;
+    let authService: AuthService;
+    let pcrudService: PcrudService;
+    let orgService: OrgService;
+    let evtService: EventService;
+    let storeService: StoreService;
+
+    beforeEach(() => {
+        idlService = new IdlService();
+        evtService = new EventService();
+        storeService = new StoreService(null /* CookieService */);
+        netService = new NetService(evtService);
+        authService = new AuthService(evtService, netService, storeService);
+        pcrudService = new PcrudService(idlService, netService, authService);
+        orgService = new OrgService(netService, authService, pcrudService);
+    });
+
+    const initTestData = () => {
+        idlService.parseIdl();
+        const win: any = window; // trick TS
+        win._eg_mock_data.generateOrgTree(idlService, orgService);
+    };
+
+    it('should provide get by ID', () => {
+        initTestData();
+        expect(orgService.get(orgService.tree().id())).toBe(orgService.root());
+    });
+
+    it('should provide get by node', () => {
+        initTestData();
+        expect(orgService.get(orgService.tree())).toBe(orgService.root());
+    });
+
+    it('should provide ancestors', () => {
+        initTestData();
+        expect(orgService.ancestors(2, true)).toEqual([2, 1]);
+    });
+
+    it('should provide descendants', () => {
+        initTestData();
+        expect(orgService.descendants(2, true)).toEqual([2, 4]);
+    });
+
+    it('should provide full path', () => {
+        initTestData();
+        expect(orgService.fullPath(4, true)).toEqual([4, 2, 1]);
+    });
+
+    it('should provide root', () => {
+        initTestData();
+        expect(orgService.root().id()).toEqual(1);
+    });
+
+});
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/pcrud.service.ts b/Open-ILS/src/eg2/src/app/core/pcrud.service.ts
new file mode 100644 (file)
index 0000000..76ee341
--- /dev/null
@@ -0,0 +1,305 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {IdlService, IdlObject} from './idl.service';
+import {NetService, NetRequest} from './net.service';
+import {AuthService} from './auth.service';
+
+// Externally defined.  Used here for debugging.
+declare var js2JSON: (jsThing: any) => string;
+declare var OpenSRF: any; // creating sessions
+
+interface PcrudReqOps {
+    authoritative?: boolean;
+    anonymous?: boolean;
+    idlist?: boolean;
+    atomic?: boolean;
+}
+
+// For for documentation purposes.
+type PcrudResponse = any;
+
+export class PcrudContext {
+
+    static verboseLogging = true; //
+    static identGenerator = 0; // for debug logging
+
+    private ident: number;
+    private authoritative: boolean;
+    private xactCloseMode: string;
+    private cudIdx: number;
+    private cudAction: string;
+    private cudLast: PcrudResponse;
+    private cudList: IdlObject[];
+
+    private idl: IdlService;
+    private net: NetService;
+    private auth: AuthService;
+
+    // Tracks nested CUD actions
+    cudObserver: Observer<PcrudResponse>;
+
+    session: any; // OpenSRF.ClientSession
+
+    constructor( // passed in by parent service -- not injected
+        egIdl: IdlService,
+        egNet: NetService,
+        egAuth: AuthService
+    ) {
+        this.idl = egIdl;
+        this.net = egNet;
+        this.auth = egAuth;
+        this.xactCloseMode = 'rollback';
+        this.ident = PcrudContext.identGenerator++;
+        this.session = new OpenSRF.ClientSession('open-ils.pcrud');
+    }
+
+    toString(): string {
+        return '[PCRUDContext ' + this.ident + ']';
+    }
+
+    log(msg: string): void {
+        if (PcrudContext.verboseLogging) {
+            console.debug(this + ': ' + msg);
+        }
+    }
+
+    err(msg: string): void {
+        console.error(this + ': ' + msg);
+    }
+
+    token(reqOps?: PcrudReqOps): string {
+        return (reqOps && reqOps.anonymous) ?
+            'ANONYMOUS' : this.auth.token();
+    }
+
+    connect(): Promise<PcrudContext> {
+        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?: PcrudReqOps): Observable<PcrudResponse> {
+        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?: PcrudReqOps): Observable<PcrudResponse> {
+        const search = {};
+        search[this.idl.classes[fmClass].pkey] = {'!=' : null};
+        return this.search(fmClass, search, pcrudOps, reqOps);
+    }
+
+    search(fmClass: string, search: any,
+            pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        reqOps = reqOps || {};
+        this.authoritative = reqOps.authoritative || false;
+
+        const 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: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('create', list);
+    }
+    update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('update', list);
+    }
+    remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('delete', list);
+    }
+    autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> { // RENAMED
+        return this.cud('auto',   list);
+    }
+
+    xactClose(): Observable<PcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.' + this.xactCloseMode,
+            [this.token()]
+        );
+    }
+
+    xactBegin(): Observable<PcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.begin', [this.token()]
+        );
+    }
+
+    private dispatch(method: string, params: any[]): Observable<PcrudResponse> {
+        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<PcrudResponse>): Observable<PcrudResponse> {
+        return Observable.create(observer => {
+
+            // 1. connect
+            this.connect()
+
+            // 2. start the transaction
+            .then(() => 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<PcrudResponse> {
+
+        // this.log(`sendRequest(${method})`);
+
+        return this.net.requestCompiled(
+            new NetRequest(
+                'open-ils.pcrud', method, params, this.session)
+        );
+    }
+
+    private cud(action: string,
+        list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        this.cudList = [].concat(list); // value or array
+
+        this.log(`CUD(): ${action}`);
+
+        this.cudIdx = 0;
+        this.cudAction = action;
+        this.xactCloseMode = 'commit';
+
+        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 {
+        if (this.cudIdx >= this.cudList.length) {
+            this.cudObserver.complete();
+            return;
+        }
+
+        let action = this.cudAction;
+        const 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({providedIn: 'root'})
+export class PcrudService {
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    // Pass-thru functions for one-off PCRUD calls
+
+    connect(): Promise<PcrudContext> {
+        return this.newContext().connect();
+    }
+
+    newContext(): PcrudContext {
+        return new PcrudContext(this.idl, this.net, this.auth);
+    }
+
+    retrieve(fmClass: string, pkey: Number | string,
+        pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps);
+    }
+
+    retrieveAll(fmClass: string, pcrudOps?: any,
+        reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps);
+    }
+
+    search(fmClass: string, search: any,
+        pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().search(fmClass, search, pcrudOps, reqOps);
+    }
+
+    create(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().create(list);
+    }
+
+    update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().update(list);
+    }
+
+    remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().remove(list);
+    }
+
+    autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().autoApply(list);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/perm.service.ts b/Open-ILS/src/eg2/src/app/core/perm.service.ts
new file mode 100644 (file)
index 0000000..44d3c63
--- /dev/null
@@ -0,0 +1,59 @@
+import {Injectable} from '@angular/core';
+import {NetService} from './net.service';
+import {OrgService} from './org.service';
+import {AuthService} from './auth.service';
+
+interface HasPermAtResult {
+    [permName: string]: any[]; // org IDs or org unit objects
+}
+
+interface HasPermHereResult {
+    [permName: string]: boolean;
+}
+
+@Injectable({providedIn: 'root'})
+export class PermService {
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private auth: AuthService,
+    ) {}
+
+    // workstation not required.
+    hasWorkPermAt(permNames: string[], asId?: boolean): Promise<HasPermAtResult> {
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.has_work_perm_at.batch',
+            this.auth.token(), permNames
+        ).toPromise().then(resp => {
+            const answer: HasPermAtResult = {};
+            permNames.forEach(perm => {
+                let orgs = [];
+                resp[perm].forEach(oneOrg => {
+                    orgs = orgs.concat(this.org.descendants(oneOrg, asId));
+                });
+                answer[perm] = orgs;
+            });
+
+            return answer;
+        });
+    }
+
+    // workstation required
+    hasWorkPermHere(permNames: string[]): Promise<HasPermHereResult> {
+        const wsId: number = +this.auth.user().wsid();
+
+        if (!wsId) {
+            return Promise.reject('hasWorkPermHere requires a workstation');
+        }
+
+        return this.hasWorkPermAt(permNames, true).then(resp => {
+            const answer: HasPermHereResult = {};
+            Object.keys(resp).forEach(perm => {
+                answer[perm] = resp[perm].indexOf(wsId) > -1;
+            });
+            return answer;
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/server-store.service.ts b/Open-ILS/src/eg2/src/app/core/server-store.service.ts
new file mode 100644 (file)
index 0000000..43415c1
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * Set and get server-stored settings.
+ */
+import {Injectable} from '@angular/core';
+import {AuthService} from './auth.service';
+import {NetService} from './net.service';
+
+// Settings summary objects returned by the API
+interface ServerSettingSummary {
+    name: string;
+    value: string;
+    has_org_setting: boolean;
+    has_user_setting: boolean;
+    has_workstation_setting: boolean;
+}
+
+@Injectable({providedIn: 'root'})
+export class ServerStoreService {
+
+    cache: {[key: string]: ServerSettingSummary};
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService) {
+        this.cache = {};
+    }
+
+    setItem(key: string, value: any): Promise<any> {
+
+        if (!this.auth.token()) {
+            return Promise.reject('Auth required to apply settings');
+        }
+
+        const setting: any = {};
+        setting[key] = value;
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.apply.user_or_ws',
+            this.auth.token(), setting)
+
+        .toPromise().then(appliedCount => {
+
+            if (Number(appliedCount) > 0) { // value applied
+                return this.cache[key] = value;
+            }
+
+            return Promise.reject(
+                `No user or workstation setting type exists for: "${key}".\n` +
+                'Create a ws/user setting type or use setLocalItem() to ' +
+                'store the value locally.'
+            );
+        });
+    }
+
+    // Returns a single setting value
+    getItem(key: string): Promise<any> {
+        return this.getItemBatch([key]).then(
+            settings => settings[key]
+        );
+    }
+
+    // Returns a set of key/value pairs for the requested settings
+    getItemBatch(keys: string[]): Promise<any> {
+
+        const values: any = {};
+        keys.forEach(key => {
+            if (this.cache[key]) {
+                values[key] = this.cache[key];
+            }
+        });
+
+        if (keys.length === Object.keys(values).length) {
+            // All values are cached already
+            return Promise.resolve(values);
+        }
+
+        if (!this.auth.token()) {
+            // Authtokens require for fetching server settings, but
+            // calls to retrieve settings could potentially occur
+            // before auth completes -- Ideally not, but just to be safe.
+            return Promise.resolve({});
+        }
+
+        // Server call required.  Limit the settings to lookup to those
+        // we don't already have cached.
+        const serverKeys = [];
+        keys.forEach(key => {
+            if (!Object.keys(values).includes(key)) {
+                serverKeys.push(key);
+            }
+        });
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.settings.retrieve',
+                serverKeys, this.auth.token()
+            ).subscribe(
+                summary => {
+                    this.cache[summary.name] =
+                        values[summary.name] = summary.value;
+                },
+                err => reject,
+                () => resolve(values)
+            );
+        });
+    }
+
+    removeItem(key: string): Promise<any> {
+        return this.setItem(key, null);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/store.service.ts b/Open-ILS/src/eg2/src/app/core/store.service.ts
new file mode 100644 (file)
index 0000000..46dd621
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * Store and retrieve data from various sources.
+ *
+ * Data Types:
+ * 1. LocalItem: Stored in window.localStorage and persist indefinitely.
+ * 2. SessionItem: Stored in window.sessionStorage and persist until
+ *    the end of the current browser tab/window.  Data is only available
+ *    to the tab/window where the data was set.
+ * 3. LoginItem: Stored as session cookies and persist until the browser
+ *    is closed.  These values are avalable to all browser windows/tabs.
+ */
+import {Injectable} from '@angular/core';
+import {CookieService} from 'ngx-cookie';
+
+@Injectable({providedIn: 'root'})
+export class StoreService {
+
+    // Base path for cookie-based storage.
+    // Useful for limiting cookies to subsections of the application.
+    // Store cookies globally by default.
+    // Note cookies shared with /eg/staff must be stored at "/"
+    loginSessionBasePath = '/';
+
+    // 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 === undefined || 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);
+    }
+
+    setLocalItem(key: string, val: any, isJson?: boolean): void {
+        if (!isJson) {
+            val = JSON.stringify(val);
+        }
+        window.localStorage.setItem(key, val);
+    }
+
+    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, secure: true});
+    }
+
+    getLocalItem(key: string): any {
+        return this.parseJson(window.localStorage.getItem(key));
+    }
+
+    getSessionItem(key: string): any {
+        return this.parseJson(window.sessionStorage.getItem(key));
+    }
+
+    getLoginSessionItem(key: string): any {
+        return this.parseJson(this.cookieService.get(key));
+    }
+
+    removeLocalItem(key: string): void {
+        window.localStorage.removeItem(key);
+    }
+
+    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/src/eg2/src/app/core/store.spec.ts b/Open-ILS/src/eg2/src/app/core/store.spec.ts
new file mode 100644 (file)
index 0000000..ae6c27f
--- /dev/null
@@ -0,0 +1,22 @@
+import {StoreService} from './store.service';
+
+describe('StoreService', () => {
+    let service: StoreService;
+    beforeEach(() => {
+        service = new StoreService(null /* CookieService */);
+    });
+
+    it('should set/get a localStorage value', () => {
+        const str = 'hello, world';
+        service.setLocalItem('testKey', str);
+        expect(service.getLocalItem('testKey')).toBe(str);
+    });
+
+    it('should set/get a sessionStorage value', () => {
+        const str = 'hello, world again';
+        service.setLocalItem('testKey', str);
+        expect(service.getLocalItem('testKey')).toBe(str);
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/resolver.service.ts b/Open-ILS/src/eg2/src/app/resolver.service.ts
new file mode 100644 (file)
index 0000000..faa6038
--- /dev/null
@@ -0,0 +1,36 @@
+import {Injectable} from '@angular/core';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRouteSnapshot} from '@angular/router';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {LocaleService} from '@eg/core/locale.service';
+
+// For locale application
+declare var OpenSRF;
+
+@Injectable()
+export class BaseResolver implements Resolve<Promise<void>> {
+
+    constructor(
+        private router: Router,
+        private idl: IdlService,
+        private org: OrgService,
+        private locale: LocaleService
+    ) {}
+
+    /**
+     * Loads pre-auth data common to all applications.
+     * No auth token is available at this level.  When needed, auth is
+     * enforced by application/group-specific resolvers at lower levels.
+     */
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Promise<void> {
+
+        OpenSRF.locale = this.locale.currentLocaleCode();
+
+        this.idl.parseIdl();
+
+        return this.org.fetchOrgs(); // anonymous PCRUD.
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/routing.module.ts b/Open-ILS/src/eg2/src/app/routing.module.ts
new file mode 100644 (file)
index 0000000..db3ee19
--- /dev/null
@@ -0,0 +1,29 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {BaseResolver} from './resolver.service';
+import {WelcomeComponent} from './welcome.component';
+
+/**
+ * Avoid loading all application JS up front 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.
+ * These modules are encoded as separate JS chunks that are fetched
+ * from the server only when needed.
+ */
+const routes: Routes = [
+  { path: '',
+    component: WelcomeComponent
+  }, {
+    path: 'staff',
+    resolve : {startup : BaseResolver},
+    loadChildren: './staff/staff.module#StaffModule'
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule],
+  providers: [BaseResolver]
+})
+
+export class BaseRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/share/README b/Open-ILS/src/eg2/src/app/share/README
new file mode 100644 (file)
index 0000000..f428f79
--- /dev/null
@@ -0,0 +1,6 @@
+Shared Angular services, components, directives, and associated classes.  
+
+These items are NOT automatically imported to the base module, though some
+may already be imported by intermediate modules (e.g. StaffCommonModule).   
+Import as needed.
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html
new file mode 100644 (file)
index 0000000..82ed72a
--- /dev/null
@@ -0,0 +1,26 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Access Key Assignments</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row border-bottom">
+      <div class="col-lg-3 p-1 border-right text-center" i18n>Command</div>
+      <div class="col-lg-6 p-1 border-right" i18n>Action</div>
+      <div class="col-lg-3 p-1" i18n>Context</div>
+    </div>
+    <div class="row border-bottom" *ngFor="let a of assignments()">
+      <div class="col-lg-3 p-1 border-right text-center">{{a.key}}</div>
+      <div class="col-lg-6 p-1 border-right">{{a.desc}}</div>
+      <div class="col-lg-3 p-1">{{a.ctx}}</div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close()" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts
new file mode 100644 (file)
index 0000000..d713ee6
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'eg-accesskey-info',
+  templateUrl: './accesskey-info.component.html'
+})
+export class AccessKeyInfoComponent extends DialogComponent {
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private keyService: AccessKeyService) {
+        super(modal);
+    }
+
+    assignments(): any[] {
+        return this.keyService.infoIze();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts
new file mode 100644 (file)
index 0000000..dfc835d
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Assign access keys to <a> tags.
+ *
+ * Access key action is peformed via .click(). hrefs, routerLinks,
+ * and (click) actions are all supported.
+ *
+ *   <a
+ *     routerLink="/staff/splash"
+ *     egAccessKey
+ *     keySpec="alt+h" i18n-keySpec
+ *     keyDesc="My Description" 18n-keyDesc
+ *   >
+ */
+import {Directive, ElementRef, Input, OnInit} from '@angular/core';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+
+@Directive({
+  selector: '[egAccessKey]'
+})
+export class AccessKeyDirective implements OnInit {
+
+    // Space-separated list of key combinations
+    // E.g. "ctrl+h", "alt+h ctrl+y"
+    @Input() keySpec: string;
+
+    // Description to display in the accesskey info dialog
+    @Input() keyDesc: string;
+
+    // Context info to display in the accesskey info dialog
+    // E.g. "navbar"
+    @Input() keyCtx: string;
+
+    constructor(
+        private elm: ElementRef,
+        private keyService: AccessKeyService
+    ) { }
+
+    ngOnInit() {
+
+        if (!this.keySpec) {
+            console.warn('AccessKey no keySpec provided');
+            return;
+        }
+
+        this.keySpec.split(/ /).forEach(keySpec => {
+            this.keyService.assign({
+                key: keySpec,
+                desc: this.keyDesc,
+                ctx: this.keyCtx,
+                action: () => this.elm.nativeElement.click()
+            });
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
new file mode 100644 (file)
index 0000000..51dda57
--- /dev/null
@@ -0,0 +1,67 @@
+import {Injectable, EventEmitter, HostListener} from '@angular/core';
+
+export interface AccessKeyAssignment {
+    key: string;      // keyboard command
+    desc: string;     // human-friendly description
+    ctx: string;      // template context
+    action: Function; // handler function
+}
+
+@Injectable()
+export class AccessKeyService {
+
+    // Assignments stored as an array with most recently assigned
+    // items toward the front.  Most recent items have precedence.
+    assignments: AccessKeyAssignment[] = [];
+
+    constructor() {}
+
+    assign(assn: AccessKeyAssignment): void {
+        this.assignments.unshift(assn);
+    }
+
+    /**
+     * Compress a set of single-fire keyboard events into single
+     * string.  For example:  Control and 't' becomes 'ctrl+t'.
+     */
+    compressKeys(evt: KeyboardEvent): string {
+
+        let s = '';
+        if (evt.ctrlKey || evt.metaKey) { s += 'ctrl+'; }
+        if (evt.altKey) { s += 'alt+'; }
+        s += evt.key.toLowerCase();
+
+        return s;
+    }
+
+    /**
+     * Checks for a key assignment and fires the assigned action.
+     */
+    fire(evt: KeyboardEvent): void {
+        const keySpec = this.compressKeys(evt);
+        for (const i in this.assignments) { // for-loop to exit early
+            if (keySpec === this.assignments[i].key) {
+                const assign = this.assignments[i];
+                console.debug(`AccessKey assignment found for ${assign.key}`);
+                // Allow the current digest cycle to complete before
+                // firing the access key action.
+                setTimeout(assign.action, 0);
+                evt.preventDefault();
+                return;
+            }
+        }
+    }
+
+    /**
+     * Returns a simplified key assignment list containing just
+     * the key spec and the description.  Useful for inspecting
+     * without exposing the actions.
+     */
+    infoIze(): any[] {
+        return this.assignments.map(a => {
+            return {key: a.key, desc: a.desc, ctx: a.ctx};
+        });
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
new file mode 100644 (file)
index 0000000..19924d9
--- /dev/null
@@ -0,0 +1,249 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {from} from 'rxjs/observable/from';
+import {map} from 'rxjs/operators/map';
+import {OrgService} from '@eg/core/org.service';
+import {UnapiService} from '@eg/share/catalog/unapi.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+export 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'
+};
+
+export const HOLDINGS_XPATH =
+    '/holdings:holdings/holdings:counts/holdings:count';
+
+
+export class BibRecordSummary {
+    id: number; // == record.id() for convenience
+    orgId: number;
+    orgDepth: number;
+    record: IdlObject;
+    display: any;
+    attributes: any;
+    holdingsSummary: any;
+    holdCount: number;
+    bibCallNumber: string;
+    net: NetService;
+
+    constructor(record: IdlObject, orgId: number, orgDepth: number) {
+        this.id = record.id();
+        this.record = record;
+        this.orgId = orgId;
+        this.orgDepth = orgDepth;
+        this.display = {};
+        this.attributes = {};
+        this.bibCallNumber = null;
+    }
+
+    ingest() {
+        this.compileDisplayFields();
+        this.compileRecordAttrs();
+
+        // Normalize some data for JS consistency
+        this.record.creator(Number(this.record.creator()));
+        this.record.editor(Number(this.record.editor()));
+    }
+
+    compileDisplayFields() {
+        this.record.flat_display_entries().forEach(entry => {
+            if (entry.multi() === 't') {
+                if (this.display[entry.name()]) {
+                    this.display[entry.name()].push(entry.value());
+                } else {
+                    this.display[entry.name()] = [entry.value()];
+                }
+            } else {
+                this.display[entry.name()] = entry.value();
+            }
+        });
+    }
+
+    compileRecordAttrs() {
+        // Any attr can be multi-valued.
+        this.record.mattrs().forEach(attr => {
+            if (this.attributes[attr.attr()]) {
+                this.attributes[attr.attr()].push(attr.value());
+            } else {
+                this.attributes[attr.attr()] = [attr.value()];
+            }
+        });
+    }
+
+    // Get -> Set -> Return bib hold count
+    getHoldCount(): Promise<number> {
+
+        if (Number.isInteger(this.holdCount)) {
+            return Promise.resolve(this.holdCount);
+        }
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.bre.holds.count', this.id
+        ).toPromise().then(count => this.holdCount = count);
+    }
+
+    // Get -> Set -> Return bib-level call number
+    getBibCallNumber(): Promise<string> {
+
+        if (this.bibCallNumber !== null) {
+            return Promise.resolve(this.bibCallNumber);
+        }
+
+        // TODO labelClass = cat.default_classification_scheme YAOUS
+        const labelClass = 1;
+
+        return this.net.request(
+            'open-ils.cat',
+            'open-ils.cat.biblio.record.marc_cn.retrieve',
+            this.id, labelClass
+        ).toPromise().then(cnArray => {
+            if (cnArray && cnArray.length > 0) {
+                const key1 = Object.keys(cnArray[0])[0];
+                this.bibCallNumber = cnArray[0][key1];
+            } else {
+                this.bibCallNumber = '';
+            }
+            return this.bibCallNumber;
+        });
+    }
+}
+
+@Injectable()
+export class BibRecordService {
+
+    // Cache of bib editor / creator objects
+    // Assumption is this list will be limited in size.
+    userCache: {[id: number]: IdlObject};
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private unapi: UnapiService,
+        private pcrud: PcrudService
+    ) {
+        this.userCache = {};
+    }
+
+    // Avoid fetching the MARC blob by specifying which fields on the
+    // bre to select.  Note that fleshed fields are explicitly selected.
+    fetchableBreFields(): string[] {
+        return this.idl.classes.bre.fields
+            .filter(f => !f.virtual && f.name !== 'marc')
+            .map(f => f.name);
+    }
+
+    // Note when multiple IDs are provided, responses are emitted in order
+    // of receipt, not necessarily in the requested ID order.
+    getBibSummary(bibIds: number | number[],
+        orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
+
+        const ids = [].concat(bibIds);
+
+        if (ids.length === 0) {
+            return from([]);
+        }
+
+        return this.pcrud.search('bre', {id: ids},
+            {   flesh: 1,
+                flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
+                select: {bre : this.fetchableBreFields()}
+            },
+            {anonymous: true} // skip unneccesary auth
+        ).pipe(mergeMap(bib => {
+            const summary = new BibRecordSummary(bib, orgId, orgDepth);
+            summary.net = this.net; // inject
+            summary.ingest();
+            return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
+            .then(holdingsSummary => {
+                summary.holdingsSummary = holdingsSummary;
+                return summary;
+            });
+        }));
+    }
+
+    // Flesh the creator and editor fields.
+    // Handling this separately lets us pull from the cache and
+    // avoids the requirement that the main bib query use a staff
+    // (VIEW_USER) auth token.
+    fleshBibUsers(records: IdlObject[]): Promise<void> {
+
+        const search = [];
+
+        records.forEach(rec => {
+            ['creator', 'editor'].forEach(field => {
+                const id = rec[field]();
+                if (Number.isInteger(id)) {
+                    if (this.userCache[id]) {
+                        rec[field](this.userCache[id]);
+                    } else if (!search.includes(id)) {
+                        search.push(id);
+                    }
+                }
+            });
+        });
+
+        if (search.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.pcrud.search('au', {id: search})
+        .pipe(map(user => {
+            this.userCache[user.id()] = user;
+            records.forEach(rec => {
+                if (user.id() === rec.creator()) {
+                    rec.creator(user);
+                }
+                if (user.id() === rec.editor()) {
+                    rec.editor(user);
+                }
+            });
+        })).toPromise();
+    }
+
+    getHoldingsSummary(recordId: number,
+        orgId: number, orgDepth: number): Promise<any> {
+
+        const holdingsSummary = [];
+
+        return this.unapi.getAsXmlDocument({
+            target: 'bre',
+            id: recordId,
+            extras: '{holdings_xml}',
+            format: 'holdings_xml',
+            orgId: orgId,
+            depth: orgDepth
+        }).then(xmlDoc => {
+
+            // namespace resolver
+            const resolver: any = (prefix: string): string => {
+                return NAMESPACE_MAPS[prefix] || null;
+            };
+
+            // Extract the holdings data from the unapi xml doc
+            const result = xmlDoc.evaluate(HOLDINGS_XPATH,
+                xmlDoc, resolver, XPathResult.ANY_TYPE, null);
+
+            let node;
+            while (node = result.iterateNext()) {
+                const counts = {type : node.getAttribute('type')};
+                ['depth', 'org_unit', 'transcendant',
+                    'available', 'count', 'unshadow'].forEach(field => {
+                    counts[field] = Number(node.getAttribute(field));
+                });
+                holdingsSummary.push(counts);
+            }
+
+            return holdingsSummary;
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts
new file mode 100644 (file)
index 0000000..c370b30
--- /dev/null
@@ -0,0 +1,28 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {CatalogService} from './catalog.service';
+import {CatalogUrlService} from './catalog-url.service';
+import {BibRecordService} from './bib-record.service';
+import {UnapiService} from './unapi.service';
+import {MarcHtmlComponent} from './marc-html.component';
+
+
+@NgModule({
+    declarations: [
+        MarcHtmlComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        MarcHtmlComponent
+    ],
+    providers: [
+        CatalogService,
+        CatalogUrlService,
+        UnapiService,
+        BibRecordService
+    ]
+})
+
+export class CatalogCommonModule {}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts
new file mode 100644 (file)
index 0000000..253e3aa
--- /dev/null
@@ -0,0 +1,143 @@
+import {Injectable} from '@angular/core';
+import {ParamMap} from '@angular/router';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogSearchContext, FacetFilter} from './search-context';
+import {CATALOG_CCVM_FILTERS} from './catalog.service';
+
+@Injectable()
+export class CatalogUrlService {
+
+    // consider supporting a param name prefix/namespace
+
+    constructor(private org: OrgService) { }
+
+    /**
+     * Returns a URL query structure suitable for using with
+     * router.navigate(..., {queryParams:...}).
+     * No navigation is performed within.
+     */
+    toUrlParams(context: CatalogSearchContext):
+            {[key: string]: string | string[]} {
+
+        const params = {
+            query: [],
+            fieldClass: [],
+            joinOp: [],
+            matchOp: [],
+            facets: [],
+            identQuery: null,
+            identQueryType: null,
+            org: null,
+            limit: null,
+            offset: null
+        };
+
+        params.org = context.searchOrg.id();
+
+        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', 'identQuery', 'identQueryType']
+        .forEach(field => {
+            if (context[field]) {
+                // Only propagate applied values to the URL.
+                params[field] = context[field];
+            }
+        });
+
+        if (params.identQuery) {
+            // Ident queries (e.g. tcn search) discards all remaining filters
+            return params;
+        }
+
+        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
+            }));
+        });
+
+        return params;
+    }
+
+    /**
+     * Creates a new search context from the active route params.
+     */
+    fromUrlParams(params: ParamMap): CatalogSearchContext {
+        const 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', 'identQuery', 'identQueryType']
+        .forEach(field => {
+            const 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 => {
+            const arr = params.getAll(field);
+            if (arr && arr.length) {
+                context[field] = arr;
+            }
+        });
+
+        CATALOG_CCVM_FILTERS.forEach(code => {
+            const val = params.get(code);
+            if (val) {
+                context.ccvmFilters[code] = val.split(/,/);
+            } else {
+                context.ccvmFilters[code] = [''];
+            }
+        });
+
+        params.getAll('facets').forEach(blob => {
+            const facet = JSON.parse(blob);
+            context.addFacet(new FacetFilter(facet.c, facet.n, facet.v));
+        });
+
+        if (params.get('org')) {
+            context.searchOrg = this.org.get(+params.get('org'));
+        }
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
new file mode 100644 (file)
index 0000000..95967cb
--- /dev/null
@@ -0,0 +1,210 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {map} from 'rxjs/operators/map';
+import {OrgService} from '@eg/core/org.service';
+import {UnapiService} from '@eg/share/catalog/unapi.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogSearchContext, CatalogSearchState} from './search-context';
+import {BibRecordService, BibRecordSummary} from './bib-record.service';
+
+// CCVM's we care about in a catalog context
+// Don't fetch them all because there are a lot.
+export const CATALOG_CCVM_FILTERS = [
+    'item_type',
+    'item_form',
+    'item_lang',
+    'audience',
+    'audience_group',
+    'vr_format',
+    'bib_level',
+    'lit_form',
+    'search_format',
+    'icon_format'
+];
+
+@Injectable()
+export class CatalogService {
+
+    ccvmMap: {[ccvm: string]: IdlObject[]} = {};
+    cmfMap: {[cmf: string]: IdlObject} = {};
+
+    // 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 idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private unapi: UnapiService,
+        private pcrud: PcrudService,
+        private bibService: BibRecordService
+    ) {}
+
+    search(ctx: CatalogSearchContext): Promise<void> {
+        ctx.searchState = CatalogSearchState.SEARCHING;
+
+        const 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));
+    }
+
+    // Appends records to the search result set as they arrive.
+    // Returns a void promise once all records have been retrieved
+    fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
+
+        const depth = ctx.global ?
+            ctx.org.root().ou_type().depth() :
+            ctx.searchOrg.ou_type().depth();
+
+        return this.bibService.getBibSummary(
+            ctx.currentResultIds(), ctx.searchOrg.id(), depth)
+        .pipe(map(summary => {
+            // Responses are not necessarily returned in request-ID order.
+            const idx = ctx.currentResultIds().indexOf(summary.record.id());
+            if (ctx.result.records) {
+                // May be reset when quickly navigating results.
+                ctx.result.records[idx] = summary;
+            }
+        })).toPromise();
+    }
+
+    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 => {
+                const facetData = {};
+                Object.keys(facets).forEach(cmfId => {
+                    const facetHash = facets[cmfId];
+                    const cmf = this.cmfMap[cmfId];
+
+                    const cmfData = [];
+                    Object.keys(facetHash).forEach(value => {
+                        const 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, anonymous: true}
+            ).subscribe(list => {
+                this.compileCcvms(list);
+                resolve();
+            });
+        });
+    }
+
+    compileCcvms(ccvms: IdlObject[]): 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, anonymous: true}
+            ).subscribe(
+                cmfs => {
+                    cmfs.forEach(c => this.cmfMap[c.id()] = c);
+                    resolve();
+                }
+            );
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts b/Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts
new file mode 100644 (file)
index 0000000..38b1da7
--- /dev/null
@@ -0,0 +1,90 @@
+import {Component, OnInit, Input, ElementRef} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+
+@Component({
+  selector: 'eg-marc-html',
+  // view is generated from MARC HTML
+  template: '<ng-template></ng-template>'
+})
+export class MarcHtmlComponent implements OnInit {
+
+    recId: number;
+    initDone = 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();
+        }
+    }
+
+    recType: string;
+    @Input() set recordType(rtype: string) {
+        this.recType = rtype;
+    }
+
+    constructor(
+        private elm: ElementRef,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        this.collectData();
+    }
+
+    collectData() {
+        if (!this.recId) { return; }
+
+        let service = 'open-ils.search';
+        let method = 'open-ils.search.biblio.record.html';
+        const params: any[] = [this.recId];
+
+        switch (this.recType) {
+
+            case 'authority':
+                method = 'open-ils.search.authority.to_html';
+                break;
+
+            case 'vandelay-authority':
+                params.unshift(this.auth.token());
+                service = 'open-ils.vandelay';
+                method = 'open-ils.vandelay.queued_authority_record.html';
+                break;
+
+            case 'vandelay-bib':
+                params.unshift(this.auth.token());
+                service = 'open-ils.vandelay';
+                method = 'open-ils.vandelay.queued_bib_record.html';
+                break;
+        }
+
+        this.net.requestWithParamList(service, method, params)
+        .toPromise().then(html => this.injectHtml(html));
+    }
+
+    injectHtml(html: string) {
+
+        // Remove embedded labels and actions.
+        html = html.replace(
+            /<button onclick="window.print(.*?)<\/button>/, '');
+
+        html = html.replace(/<title>(.*?)<\/title>/, '');
+
+        // remove reference to nonexistant CSS file
+        html = html.replace(/<link(.*?)\/>/, '');
+
+        // there shouldn't be any, but while we're at it,
+        // kill any embedded script tags
+        html = html.replace(/<script(.*?)<\/script>/, '');
+
+        this.elm.nativeElement.innerHTML = html;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
new file mode 100644 (file)
index 0000000..e4e64b2
--- /dev/null
@@ -0,0 +1,266 @@
+import {OrgService} from '@eg/core/org.service';
+import {IdlObject} from '@eg/core/idl.service';
+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 = false;
+    global = false;
+    sort: string;
+    fieldClass: string[];
+    query: string[];
+    identQuery: string;
+    identQueryType: string; // isbn, issn, etc.
+    joinOp: string[];
+    matchOp: string[];
+    format: string;
+    searchOrg: IdlObject;
+    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: OrgService;
+
+    constructor() {
+        this.pager = new Pager();
+        this.reset();
+    }
+
+    // List of result IDs for the current page of data.
+    currentResultIds(): number[] {
+        const ids = [];
+        const max = Math.min(
+            this.pager.offset + this.pager.limit,
+            this.pager.resultCount
+        );
+        for (let idx = this.pager.offset; idx < max; 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
+     * search-global, or search-org.
+     */
+    reset(): void {
+        this.pager.offset = 0;
+        this.format = '';
+        this.sort = '';
+        this.query = [''];
+        this.identQuery = null;
+        this.identQueryType = 'identifier|isbn';
+        this.fieldClass  = ['keyword'];
+        this.matchOp = ['contains'];
+        this.joinOp = [''];
+        this.ccvmFilters = {};
+        this.facetFilters = [];
+        this.result = {};
+        this.resultIds = [];
+        this.searchState = CatalogSearchState.PENDING;
+    }
+
+    isSearchable(): boolean {
+
+        if (this.identQuery && this.identQueryType) {
+            return true;
+        }
+
+        return this.query.length
+            && this.query[0] !== ''
+            && this.searchOrg !== null;
+    }
+
+    compileSearch(): string {
+        let str = '';
+
+        if (this.available) {
+            str += '#available';
+        }
+
+        if (this.sort) {
+            // e.g. title, title.descending
+            const parts = this.sort.split(/\./);
+            if (parts[1]) { str += ' #descending'; }
+            str += ' sort(' + parts[0] + ')';
+        }
+
+        if (this.identQuery && this.identQueryType) {
+            if (str) { str += ' '; }
+            str += this.identQueryType + ':' + this.identQuery;
+
+        } else {
+
+            // -------
+            // Compile boolean sub-query components
+            if (str.length) { str += ' '; }
+            const 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];
+        const joinOp = this.joinOp[idx];
+        const matchOp = this.matchOp[idx];
+        const 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 => f.equals(facet))[0]
+        );
+    }
+
+    removeFacet(facet: FacetFilter): void {
+        this.facetFilters = this.facetFilters.filter(f => !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/src/eg2/src/app/share/catalog/unapi.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts
new file mode 100644 (file)
index 0000000..e285a89
--- /dev/null
@@ -0,0 +1,54 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {OrgService} from '@eg/core/org.service';
+
+/*
+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 UnapiParams {
+    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 UnapiService {
+
+    constructor(private org: OrgService) {}
+
+    createUrl(params: UnapiParams): string {
+        const depth = params.depth || 0;
+        const 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: UnapiParams): Promise<XMLDocument> {
+        // XReq creates an XML document for us.  Seems like the right
+        // tool for the job.
+        const url = this.createUrl(params);
+        return new Promise((resolve, reject) => {
+            const xhttp = new XMLHttpRequest();
+            xhttp.onreadystatechange = function() { // no () => {} !
+                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/src/eg2/src/app/share/combobox/combobox-entry.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts
new file mode 100644 (file)
index 0000000..26015b7
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, Input, Host, OnInit} from '@angular/core';
+import {ComboboxComponent} from './combobox.component';
+
+@Component({
+  selector: 'eg-combobox-entry',
+  template: '<ng-template></ng-template>'
+})
+export class ComboboxEntryComponent implements OnInit {
+
+    @Input() entryId: any;
+    @Input() entryLabel: string;
+    @Input() selected: boolean;
+
+    constructor(@Host() private combobox: ComboboxComponent) {}
+
+    ngOnInit() {
+        if (this.selected) {
+            this.combobox.startId = this.entryId;
+        }
+        this.combobox.addEntry(
+            {id: this.entryId, label: this.entryLabel});
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
new file mode 100644 (file)
index 0000000..47237e9
--- /dev/null
@@ -0,0 +1,27 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<div class="d-flex">
+  <input type="text" 
+    class="form-control"
+    [ngClass]="{'text-success font-italic font-weight-bold': selected && selected.freetext}"
+    [placeholder]="placeholder"
+    [name]="name"
+    [disabled]="isDisabled"
+    [required]="isRequired"
+    [(ngModel)]="selected" 
+    [ngbTypeahead]="filter"
+    [resultTemplate]="displayTemplate"
+    [inputFormatter]="formatDisplayString"
+    (click)="click$.next($event.target.value)"
+    (blur)="onBlur()"
+    (selectItem)="selectorChanged($event)"
+    #instance="ngbTypeahead"/>
+  <div class="d-flex flex-column icons" (click)="openMe($event)">
+    <span class="material-icons">keyboard_arrow_up</span>
+    <span class="material-icons">keyboard_arrow_down</span>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
new file mode 100644 (file)
index 0000000..40fc1c0
--- /dev/null
@@ -0,0 +1,241 @@
+/**
+ * <eg-combobox [allowFreeText]="true" [entries]="comboboxEntryList"/>
+ *  <!-- see also <eg-combobox-entry> -->
+ * </eg-combobox>
+ */
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter, ElementRef} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {tap} from 'rxjs/operators/tap';
+import {reduce} from 'rxjs/operators/reduce';
+import {of} from 'rxjs';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {mapTo} from 'rxjs/operators/mapTo';
+import {debounceTime} from 'rxjs/operators/debounceTime';
+import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
+import {merge} from 'rxjs/operators/merge';
+import {filter} from 'rxjs/operators/filter';
+import {Subject} from 'rxjs/Subject';
+import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
+import {StoreService} from '@eg/core/store.service';
+
+export interface ComboboxEntry {
+  id: any;
+  label: string;
+  freetext?: boolean;
+}
+
+@Component({
+  selector: 'eg-combobox',
+  templateUrl: './combobox.component.html',
+  styles: [`
+    .icons {margin-left:-18px}
+    .material-icons {font-size: 16px;font-weight:bold}
+  `]
+})
+export class ComboboxComponent implements OnInit {
+
+    selected: ComboboxEntry;
+    click$: Subject<string>;
+    entrylist: ComboboxEntry[];
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Applies a name attribute to the input.
+    // Useful in forms.
+    @Input() name: string;
+
+    // Placeholder text for selector input
+    @Input() placeholder = '';
+
+    @Input() persistKey: string; // TODO
+
+    @Input() allowFreeText = false;
+
+    // Add a 'required' attribute to the input
+    isRequired: boolean;
+    @Input() set required(r: boolean) {
+        this.isRequired = r;
+    }
+
+    // Disable the input
+    isDisabled: boolean;
+    @Input() set disabled(d: boolean) {
+        this.isDisabled = d;
+    }
+
+    // Entry ID of the default entry to select (optional)
+    // onChange() is NOT fired when applying the default value,
+    // unless startIdFiresOnChange is set to true.
+    @Input() startId: any;
+    @Input() startIdFiresOnChange: boolean;
+
+    @Input() asyncDataSource: (term: string) => Observable<ComboboxEntry>;
+
+    // Useful for efficiently preventing duplicate async entries
+    asyncIds: {[idx: string]: boolean};
+
+    // True if a default selection has been made.
+    defaultSelectionApplied: boolean;
+
+    @Input() set entries(el: ComboboxEntry[]) {
+        this.entrylist = el;
+        this.applySelection();
+    }
+
+    // Emitted when the value is changed via UI.
+    // When the UI value is cleared, null is emitted.
+    @Output() onChange: EventEmitter<ComboboxEntry>;
+
+    // Useful for massaging the match string prior to comparison
+    // and display.  Default version trims leading/trailing spaces.
+    formatDisplayString: (ComboboxEntry) => string;
+
+    constructor(
+      private elm: ElementRef,
+      private store: StoreService,
+    ) {
+        this.entrylist = [];
+        this.asyncIds = {};
+        this.click$ = new Subject<string>();
+        this.onChange = new EventEmitter<ComboboxEntry>();
+        this.defaultSelectionApplied = false;
+
+        this.formatDisplayString = (result: ComboboxEntry) => {
+            return result.label.trim();
+        };
+    }
+
+    ngOnInit() {
+    }
+
+    openMe($event) {
+        // Give the input a chance to focus then fire the click
+        // handler to force open the typeahead
+        this.elm.nativeElement.getElementsByTagName('input')[0].focus();
+        setTimeout(() => this.click$.next(''));
+    }
+
+    // Apply a default selection where needed
+    applySelection() {
+
+        if (this.startId &&
+            this.entrylist && !this.defaultSelectionApplied) {
+
+            const entry =
+                this.entrylist.filter(e => e.id === this.startId)[0];
+
+            if (entry) {
+                this.selected = entry;
+                this.defaultSelectionApplied = true;
+                if (this.startIdFiresOnChange) {
+                    this.selectorChanged(
+                        {item: this.selected, preventDefault: () => true});
+                }
+            }
+        }
+    }
+
+    // Called by combobox-entry.component
+    addEntry(entry: ComboboxEntry) {
+        this.entrylist.push(entry);
+        this.applySelection();
+    }
+
+    onBlur() {
+        // When the selected value is a string it means we have either
+        // no value (user cleared the input) or a free-text value.
+
+        if (typeof this.selected === 'string') {
+
+            if (this.allowFreeText && this.selected !== '') {
+                // Free text entered which does not match a known entry
+                // translate it into a dummy ComboboxEntry
+                this.selected = {
+                    id: null,
+                    label: this.selected,
+                    freetext: true
+                };
+
+            } else {
+
+                this.selected = null;
+            }
+
+            // Manually fire the onchange since NgbTypeahead fails
+            // to fire the onchange when the value is cleared.
+            this.selectorChanged(
+                {item: this.selected, preventDefault: () => true});
+        }
+    }
+
+    // Fired by the typeahead to inform us of a change.
+    selectorChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        this.onChange.emit(selEvent.item);
+    }
+
+    // Adds matching async entries to the entry list
+    // and propagates the search term for pipelining.
+    addAsyncEntries(term: string): Observable<string> {
+
+        if (!term || !this.asyncDataSource) {
+            return of(term);
+        }
+
+        return new Observable(observer => {
+            this.asyncDataSource(term).subscribe(
+                (entry: ComboboxEntry) => {
+                    if (!this.asyncIds['' + entry.id]) {
+                        this.asyncIds['' + entry.id] = true;
+                        this.addEntry(entry);
+                    }
+                },
+                err => {},
+                ()  => {
+                    observer.next(term);
+                    observer.complete();
+                }
+            );
+        });
+    }
+
+    filter = (text$: Observable<string>): Observable<ComboboxEntry[]> => {
+        return text$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+
+            // Merge click actions in with the stream of text entry
+            merge(
+                // Inject a specifier indicating the source of the
+                // action is a user click instead of a text entry.
+                // This tells the filter to show all values in sync mode.
+                this.click$.pipe(filter(() =>
+                    !this.instance.isPopupOpen() && !this.asyncDataSource
+                )).pipe(mapTo('_CLICK_'))
+            ),
+
+            // mergeMap coalesces an observable into our stream.
+            mergeMap(term => this.addAsyncEntries(term)),
+            map((term: string) => {
+
+                if (term === '' || term === '_CLICK_') {
+                    if (this.asyncDataSource) {
+                        return [];
+                    } else {
+                        // In sync mode, a post-focus empty search or
+                        // click event displays the whole list.
+                        return this.entrylist;
+                    }
+                }
+
+                // Filter entrylist whose labels substring-match the
+                // text entered.
+                return this.entrylist.filter(entry =>
+                    entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1
+                );
+            })
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
new file mode 100644 (file)
index 0000000..c686be4
--- /dev/null
@@ -0,0 +1,21 @@
+
+<div class="input-group">
+  <input 
+    class="form-control" 
+    ngbDatepicker
+    #datePicker="ngbDatepicker"
+    placeholder="yyyy-mm-dd"
+    class="form-control"
+    name="{{fieldName}}"
+    [required]="required"
+    [(ngModel)]="current"
+    (dateSelect)="onDateSelect($event)">
+  <div class="input-group-append">
+    <button class="btn btn-outline-secondary" 
+      (click)="datePicker.toggle()" type="button">
+      <span title="Select Date" i18n-title                       
+        class="material-icons mat-icon-in-button">calendar_today</span>
+    </button>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts
new file mode 100644 (file)
index 0000000..ae3a729
--- /dev/null
@@ -0,0 +1,70 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * RE: displaying locale dates in the input field:
+ * https://github.com/ng-bootstrap/ng-bootstrap/issues/754
+ * https://stackoverflow.com/questions/40664523/angular2-ngbdatepicker-how-to-format-date-in-inputfield
+ */
+
+@Component({
+  selector: 'eg-date-select',
+  templateUrl: './date-select.component.html'
+})
+export class DateSelectComponent implements OnInit {
+
+    @Input() initialIso: string; // ISO string
+    @Input() initialYmd: string; // YYYY-MM-DD (uses local time zone)
+    @Input() initialDate: Date;  // Date object
+    @Input() required: boolean;
+    @Input() fieldName: string;
+
+    current: NgbDateStruct;
+
+    @Output() onChangeAsDate: EventEmitter<Date>;
+    @Output() onChangeAsIso: EventEmitter<string>;
+    @Output() onChangeAsYmd: EventEmitter<string>;
+
+    constructor() {
+        this.onChangeAsDate = new EventEmitter<Date>();
+        this.onChangeAsIso = new EventEmitter<string>();
+        this.onChangeAsYmd = new EventEmitter<string>();
+    }
+
+    ngOnInit() {
+
+        if (this.initialYmd) {
+            this.initialDate = this.localDateFromYmd(this.initialYmd);
+
+        } else if (this.initialIso) {
+            this.initialDate = new Date(this.initialIso);
+        }
+
+        if (this.initialDate) {
+            this.current = {
+                year: this.initialDate.getFullYear(),
+                month: this.initialDate.getMonth() + 1,
+                day: this.initialDate.getDate()
+            };
+        }
+    }
+
+    onDateSelect(evt) {
+        const ymd = `${evt.year}-${evt.month}-${evt.day}`;
+        const date = this.localDateFromYmd(ymd);
+        const iso = date.toISOString();
+        this.onChangeAsDate.emit(date);
+        this.onChangeAsYmd.emit(ymd);
+        this.onChangeAsIso.emit(iso);
+    }
+
+    // Create a date in the local time zone with selected YMD values.
+    // TODO: Consider moving this to a date service...
+    localDateFromYmd(ymd: string): Date {
+        const parts = ymd.split('-');
+        return new Date(
+            Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html
new file mode 100644 (file)
index 0000000..21766ca
--- /dev/null
@@ -0,0 +1,17 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body"><p>{{dialogBody}}</p></div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close('confirmed')" i18n>Confirm</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="dismiss('canceled')" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts
new file mode 100644 (file)
index 0000000..efcbdeb
--- /dev/null
@@ -0,0 +1,17 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@Component({
+  selector: 'eg-confirm-dialog',
+  templateUrl: './confirm.component.html'
+})
+
+/**
+ * Confirmation dialog that asks a yes/no question.
+ */
+export class ConfirmDialogComponent extends DialogComponent {
+    // What question are we asking?
+    @Input() public dialogBody: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
new file mode 100644 (file)
index 0000000..3ffd5db
--- /dev/null
@@ -0,0 +1,80 @@
+import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter} from '@angular/core';
+import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog base class.  Handles the ngbModal logic.
+ * Sub-classed component templates must have a #dialogContent selector
+ * at the root of the template (see ConfirmDialogComponent).
+ */
+
+@Component({
+    selector: 'eg-dialog',
+    template: '<ng-template></ng-template>'
+})
+export class DialogComponent implements OnInit {
+
+    // Assume all dialogs support a title attribute.
+    @Input() public dialogTitle: string;
+
+    // Pointer to the dialog content template.
+    @ViewChild('dialogContent')
+    private dialogContent: TemplateRef<any>;
+
+    // Emitted after open() is called on the ngbModal.
+    // Note when overriding open(), this will not fire unless also
+    // called in the overridding method.
+    onOpen$ = new EventEmitter<any>();
+
+    // The modalRef allows direct control of the modal instance.
+    private modalRef: NgbModalRef = null;
+
+    constructor(private modalService: NgbModal) {}
+
+    ngOnInit() {
+        this.onOpen$ = new EventEmitter<any>();
+    }
+
+    open(options?: NgbModalOptions): Promise<any> {
+
+        if (this.modalRef !== null) {
+            console.warn('Dismissing existing dialog');
+            this.dismiss();
+        }
+
+        this.modalRef = this.modalService.open(this.dialogContent, options);
+
+        if (this.onOpen$) {
+            // Let the digest cycle complete
+            setTimeout(() => this.onOpen$.emit(true));
+        }
+
+        return new Promise( (resolve, reject) => {
+
+            this.modalRef.result.then(
+                (result) => {
+                    resolve(result);
+                    this.modalRef = null;
+                },
+                (result) => {
+                    console.debug('dialog closed with ' + result);
+                    reject(result);
+                    this.modalRef = null;
+                }
+            );
+        });
+    }
+
+    close(reason?: any): void {
+        if (this.modalRef) {
+            this.modalRef.close(reason);
+        }
+    }
+
+    dismiss(reason?: any): void {
+        if (this.modalRef) {
+            this.modalRef.dismiss(reason);
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css
new file mode 100644 (file)
index 0000000..fa08a1f
--- /dev/null
@@ -0,0 +1,5 @@
+
+.eg-progress-inline progress {
+  width: 100%;
+  height: 25px;
+}
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html
new file mode 100644 (file)
index 0000000..615e867
--- /dev/null
@@ -0,0 +1,28 @@
+<div class="eg-progress-inline">
+
+  <div *ngIf="hasValue() && hasMax()">
+    <!-- determinate progress bar.  shows max/value progress -->
+    <div class="row">
+      <div class="col-lg-10">
+        <progress max="{{max}}" value="{{value}}"></progress>
+      </div>
+      <div class="col-lg-2">{{percent()}}%</div>
+    </div>
+  </div>
+
+  <div *ngIf="hasValue() && !hasMax()">
+    <div class="row">
+      <!-- semi-determinate progress bar.  shows value -->
+      <div class="col-lg-10"><progress max="1"></progress></div>
+      <div class="col-lg-2">{{value}}...</div>
+    </div>
+  </div>
+
+  <div *ngIf="!hasValue()">
+    <div class="row">
+      <!-- indeterminate -->
+      <div class="col-lg-12"><progress max="1"></progress></div>
+    </div>
+  </div>
+
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts
new file mode 100644 (file)
index 0000000..9b131d1
--- /dev/null
@@ -0,0 +1,92 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+
+/**
+ * Inline Progress Bar
+ *
+ * // assuming a template reference...
+ * @ViewChild('progress')
+ * private progress: progressInlineComponent;
+ *
+ * progress.update({value : 0, max : 123});
+ * progress.increment();
+ * progress.increment();
+ *
+ * Each progress has 2 numbers, 'max' and 'value'.
+ * The content of these values determines how the progress displays.
+ *
+ * There are 3 flavors:
+ *
+ * -- value is set, max is set
+ * determinate: shows a progression with a percent complete.
+ *
+ * -- value is set, max is unset
+ * semi-determinate, with a value report.  Shows a value-less
+ * <progress/>, but shows the value as a number in the progress.
+ *
+ * This is useful in cases where the total number of items to retrieve
+ * from the server is unknown, but we know how many items we've
+ * retrieved thus far.  It helps to reinforce that something specific
+ * is happening, but we don't know when it will end.
+ *
+ * -- value is unset
+ * indeterminate: shows a generic value-less <progress/> with no
+ * clear indication of progress.
+ */
+@Component({
+  selector: 'eg-progress-inline',
+  templateUrl: './progress-inline.component.html',
+  styleUrls: ['progress-inline.component.css']
+})
+export class ProgressInlineComponent {
+
+    @Input() max: number;
+    @Input() value: number;
+
+    reset() {
+        delete this.max;
+        delete this.value;
+    }
+
+    hasValue(): boolean {
+        return Number.isInteger(this.value);
+    }
+
+    hasMax(): boolean {
+        return Number.isInteger(this.max);
+    }
+
+    percent(): number {
+        if (this.hasValue()  &&
+            this.hasMax()    &&
+            this.max > 0     &&
+            this.value <= this.max) {
+            return Math.floor((this.value / this.max) * 100);
+        }
+        return 100;
+    }
+
+    // Set the current state of the progress bar.
+    update(args: {[key: string]: number}) {
+        if (args.max !== undefined) {
+            this.max = args.max;
+        }
+        if (args.value !== undefined) {
+            this.value = args.value;
+        }
+    }
+
+    // Increment the current value.  If no amount is specified,
+    // it increments by 1.  Calling increment() on an indetermite
+    // progress bar will force it to be a (semi-)determinate bar.
+    increment(amt?: number) {
+        if (!Number.isInteger(amt)) { amt = 1; }
+
+        if (!this.hasValue()) {
+            this.value = 0;
+        }
+
+        this.value += amt;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.css b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.css
new file mode 100644 (file)
index 0000000..a79609e
--- /dev/null
@@ -0,0 +1,5 @@
+
+.eg-progress-dialog progress {
+  width: 100%;
+  height: 25px;
+}
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html
new file mode 100644 (file)
index 0000000..78ca3d0
--- /dev/null
@@ -0,0 +1,33 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 *ngIf="dialogTitle" class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+
+  <div class="modal-body eg-progress-dialog">
+
+    <div *ngIf="hasValue() && hasMax()">
+      <!-- determinate progress bar.  shows max/value progress -->
+      <div class="col-lg-10">
+        <progress max="{{max}}" value="{{value}}"></progress>
+      </div>
+      <div class="col-lg-2">{{percent()}}%</div>
+    </div>
+
+    <div *ngIf="hasValue() && !hasMax()">
+      <!-- semi-determinate progress bar.  shows value -->
+      <div class="col-lg-10"><progress max="1"></progress></div>
+      <div class="col-lg-2">{{value}}...</div>
+    </div>
+
+    <div *ngIf="!hasValue()">
+      <!-- indeterminate -->
+      <div class="col-lg-12"><progress max="1"></progress></div>
+    </div>
+
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts
new file mode 100644 (file)
index 0000000..6bf4edb
--- /dev/null
@@ -0,0 +1,108 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@Component({
+  selector: 'eg-progress-dialog',
+  templateUrl: './progress.component.html',
+  styleUrls: ['progress.component.css']
+})
+
+/**
+ * TODO: This duplicates the code from ProgressInlineComponent.
+ * This component should insert to <eg-progress-inline/> into
+ * its template instead of duplicating the code.  However, until
+ * Angular bug https://github.com/angular/angular/issues/14842
+ * is fixed, it's not possible to get a reference to the embedded
+ * inline progress, which is needed for access the update/increment
+ * API.
+ * Also consider moving the progress traking logic to a service
+ * to reduce code duplication.
+ */
+
+/**
+ * Progress Dialog.
+ *
+ * // assuming a template reference...
+ * @ViewChild('progressDialog')
+ * private dialog: ProgressDialogComponent;
+ *
+ * dialog.open();
+ * dialog.update({value : 0, max : 123});
+ * dialog.increment();
+ * dialog.increment();
+ * dialog.close();
+ *
+ * Each dialog has 2 numbers, 'max' and 'value'.
+ * The content of these values determines how the dialog displays.
+ *
+ * There are 3 flavors:
+ *
+ * -- value is set, max is set
+ * determinate: shows a progression with a percent complete.
+ *
+ * -- value is set, max is unset
+ * semi-determinate, with a value report.  Shows a value-less
+ * <progress/>, but shows the value as a number in the dialog.
+ *
+ * This is useful in cases where the total number of items to retrieve
+ * from the server is unknown, but we know how many items we've
+ * retrieved thus far.  It helps to reinforce that something specific
+ * is happening, but we don't know when it will end.
+ *
+ * -- value is unset
+ * indeterminate: shows a generic value-less <progress/> with no
+ * clear indication of progress.
+ */
+export class ProgressDialogComponent extends DialogComponent {
+
+    max: number;
+    value: number;
+
+    reset() {
+        delete this.max;
+        delete this.value;
+    }
+
+    hasValue(): boolean {
+        return Number.isInteger(this.value);
+    }
+
+    hasMax(): boolean {
+        return Number.isInteger(this.max);
+    }
+
+    percent(): number {
+        if (this.hasValue()  &&
+            this.hasMax()    &&
+            this.max > 0     &&
+            this.value <= this.max) {
+            return Math.floor((this.value / this.max) * 100);
+        }
+        return 100;
+    }
+
+    // Set the current state of the progress bar.
+    update(args: {[key: string]: number}) {
+        if (args.max !== undefined) {
+            this.max = args.max;
+        }
+        if (args.value !== undefined) {
+            this.value = args.value;
+        }
+    }
+
+    // Increment the current value.  If no amount is specified,
+    // it increments by 1.  Calling increment() on an indetermite
+    // progress bar will force it to be a (semi-)determinate bar.
+    increment(amt?: number) {
+        if (!Number.isInteger(amt)) { amt = 1; }
+
+        if (!this.hasValue()) {
+            this.value = 0;
+        }
+
+        this.value += amt;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html
new file mode 100644 (file)
index 0000000..1d7936b
--- /dev/null
@@ -0,0 +1,22 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <p>{{dialogBody}}</p>
+    <div class="text-center">
+        <input class="form-control" [(ngModel)]="promptValue"/>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close(promptValue)" i18n>Confirm</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="dismiss('canceled')" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts
new file mode 100644 (file)
index 0000000..ab7f77e
--- /dev/null
@@ -0,0 +1,19 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@Component({
+  selector: 'eg-prompt-dialog',
+  templateUrl: './prompt.component.html'
+})
+
+/**
+ * Promptation dialog that requests user input.
+ */
+export class PromptDialogComponent extends DialogComponent {
+    // What question are we asking?
+    @Input() public dialogBody: string;
+    // Value to return to the caller
+    @Input() public promptValue: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
new file mode 100644 (file)
index 0000000..721423c
--- /dev/null
@@ -0,0 +1,146 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Record Editor: {{recordLabel}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <form #fmEditForm="ngForm" role="form" class="form-validated common-form striped-odd">
+      <div class="form-group row" *ngFor="let field of fields">
+        <div class="col-lg-3 offset-lg-1">
+          <label for="rec-{{field.name}}">{{field.label}}</label>
+        </div>
+        <div class="col-lg-7">
+
+          <span *ngIf="field.template">
+            <ng-container
+              *ngTemplateOutlet="field.template; context:customTemplateFieldContext(field)">
+            </ng-container> 
+          </span>
+
+          <span *ngIf="!field.template">
+
+            <span *ngIf="field.datatype == 'id' && !pkeyIsEditable">
+              {{record[field.name]()}}
+            </span>
+  
+            <input *ngIf="field.datatype == 'id' && pkeyIsEditable"
+              class="form-control"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <input *ngIf="field.datatype == 'text' || field.datatype == 'interval'"
+              class="form-control"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+
+            <span *ngIf="field.datatype == 'timestamp'">
+              <eg-date-select
+                (onChangeAsIso)="record[field.name]($event)"
+                initialIso="{{record[field.name]()}}">
+              </eg-date-select>
+            </span>
+
+            <input *ngIf="field.datatype == 'int'"
+              class="form-control"
+              type="number"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <input *ngIf="field.datatype == 'float'"
+              class="form-control"
+              type="number" step="0.1"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <span *ngIf="field.datatype == 'money'">
+              <!-- in read-only mode display the local-aware currency -->
+              <input *ngIf="field.readOnly"
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                [readonly]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]() | currency"/>
+  
+              <input *ngIf="!field.readOnly"
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                placeholder="{{field.label}}..."
+                i18n-placeholder
+                [readonly]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
+            </span>
+  
+            <input *ngIf="field.datatype == 'bool'"
+              class="form-check-input"
+              type="checkbox"
+              name="{{field.name}}"
+              [readonly]="field.readOnly"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <span *ngIf="field.datatype == 'link'"
+              [ngClass]="{nullable : !field.isRequired()}">
+              <select
+                class="form-control"
+                name="{{field.name}}"
+                [disabled]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)">
+                <option *ngFor="let item of field.linkedValues" 
+                  [value]="item.id">{{item.name}}</option>
+              </select>
+            </span>
+  
+            <eg-org-select *ngIf="field.datatype == 'org_unit'"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [limitPerms]="modePerms[mode]"
+              [applyDefault]="field.orgDefaultAllowed"
+              [initialOrgId]="record[field.name]()"
+              (onChange)="record[field.name]($event)">
+            </eg-org-select>
+
+          </span>
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" *ngIf="mode == 'view'"
+      (click)="close()" i18n>Close</button>
+    <button type="button" class="btn btn-info" 
+      [disabled]="fmEditForm.invalid" *ngIf="mode != 'view'"
+      (click)="save()" i18n>Save</button>
+    <button type="button" class="btn btn-warning ml-2" *ngIf="mode != 'view'"
+      (click)="cancel()" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
new file mode 100644 (file)
index 0000000..308218a
--- /dev/null
@@ -0,0 +1,302 @@
+import {Component, OnInit, Input,
+    Output, EventEmitter, TemplateRef} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+interface CustomFieldTemplate {
+    template: TemplateRef<any>;
+
+    // Allow the caller to pass in a free-form context blob to
+    // be addedto the caller's custom template context, along
+    // with our stock context.
+    context?: {[fields: string]: any};
+}
+
+interface CustomFieldContext {
+    // Current create/edit/view record
+    record: IdlObject;
+
+    // IDL field definition blob
+    field: any;
+
+    // additional context values passed via CustomFieldTemplate
+    [fields: string]: any;
+}
+
+@Component({
+  selector: 'eg-fm-record-editor',
+  templateUrl: './fm-editor.component.html'
+})
+export class FmRecordEditorComponent
+    extends DialogComponent implements OnInit {
+
+    // IDL class hint (e.g. "aou")
+    @Input() idlClass: string;
+
+    // mode: 'create' for creating a new record,
+    //       'update' for editing an existing record
+    //       'view' for viewing an existing record without editing
+    mode: 'create' | 'update' | 'view' = 'create';
+    recId: any;
+    // IDL record we are editing
+    // TODO: allow this to be update in real time by the caller?
+    record: IdlObject;
+
+    // Permissions extracted from the permacrud defs in the IDL
+    // for the current IDL class
+    modePerms: {[mode: string]: string};
+
+    @Input() customFieldTemplates:
+        {[fieldName: string]: CustomFieldTemplate} = {};
+
+    // list of fields that should not be displayed
+    @Input() hiddenFieldsList: string[] = [];
+    @Input() hiddenFields: string; // comma-separated string version
+
+    // list of fields that should always be read-only
+    @Input() readonlyFieldsList: string[] = [];
+    @Input() readonlyFields: string; // comma-separated string version
+
+    // list of required fields; this supplements what the IDL considers
+    // required
+    @Input() requiredFieldsList: string[] = [];
+    @Input() requiredFields: string; // comma-separated string version
+
+    // list of org_unit fields where a default value may be applied by
+    // the org-select if no value is present.
+    @Input() orgDefaultAllowedList: string[] = [];
+    @Input() orgDefaultAllowed: string; // comma-separated string version
+
+    // hash, keyed by field name, of functions to invoke to check
+    // whether a field is required.  Each callback is passed the field
+    // name and the record and should return a boolean value. This
+    // supports cases where whether a field is required or not depends
+    // on the current value of another field.
+    @Input() isRequiredOverride:
+        {[field: string]: (field: string, record: IdlObject) => boolean};
+
+    // IDL record display label.  Defaults to the IDL label.
+    @Input() recordLabel: string;
+
+    // Emit the modified object when the save action completes.
+    @Output() onSave$ = new EventEmitter<IdlObject>();
+
+    // Emit the original object when the save action is canceled.
+    @Output() onCancel$ = new EventEmitter<IdlObject>();
+
+    // Emit an error message when the save action fails.
+    @Output() onError$ = new EventEmitter<string>();
+
+    // IDL info for the the selected IDL class
+    idlDef: any;
+
+    // Can we edit the primary key?
+    pkeyIsEditable = false;
+
+    // List of IDL field definitions.  This is a subset of the full
+    // list of fields on the IDL, since some are hidden, virtual, etc.
+    fields: any[];
+
+    @Input() editMode(mode: 'create' | 'update' | 'view') {
+        this.mode = mode;
+    }
+
+    // Record ID to view/update.  Value is dynamic.  Records are not
+    // fetched until .open() is called.
+    @Input() set recordId(id: any) {
+        if (id) { this.recId = id; }
+    }
+
+    constructor(
+      private modal: NgbModal, // required for passing to parent
+      private idl: IdlService,
+      private auth: AuthService,
+      private pcrud: PcrudService) {
+      super(modal);
+    }
+
+    // Avoid fetching data on init since that may lead to unnecessary
+    // data retrieval.
+    ngOnInit() {
+        this.listifyInputs();
+        this.idlDef = this.idl.classes[this.idlClass];
+        this.recordLabel = this.idlDef.label;
+    }
+
+    // Opening dialog, fetch data.
+    open(options?: NgbModalOptions): Promise<any> {
+        return this.initRecord().then(
+            ok => super.open(options),
+            err => console.warn(`Error fetching FM data: ${err}`)
+        );
+    }
+
+    // Translate comma-separated string versions of various inputs
+    // to arrays.
+    private listifyInputs() {
+        if (this.hiddenFields) {
+            this.hiddenFieldsList = this.hiddenFields.split(/,/);
+        }
+        if (this.readonlyFields) {
+            this.readonlyFieldsList = this.readonlyFields.split(/,/);
+        }
+        if (this.requiredFields) {
+            this.requiredFieldsList = this.requiredFields.split(/,/);
+        }
+        if (this.orgDefaultAllowed) {
+            this.orgDefaultAllowedList = this.orgDefaultAllowed.split(/,/);
+        }
+    }
+
+    private initRecord(): Promise<any> {
+
+        const pc = this.idlDef.permacrud || {};
+        this.modePerms = {
+            view:   pc.retrieve ? pc.retrieve.perms : [],
+            create: pc.create ? pc.create.perms : [],
+            update: pc.update ? pc.update.perms : [],
+        };
+
+        if (this.mode === 'update' || this.mode === 'view') {
+            return this.pcrud.retrieve(this.idlClass, this.recId)
+            .toPromise().then(rec => {
+
+                if (!rec) {
+                    return Promise.reject(`No '${this.idlClass}'
+                        record found with id ${this.recId}`);
+                }
+
+                this.record = rec;
+                this.convertDatatypesToJs();
+                return this.getFieldList();
+            });
+        }
+
+        // create a new record from scratch
+        this.pkeyIsEditable = !('pkey_sequence' in this.idlDef);
+        this.record = this.idl.create(this.idlClass);
+        return this.getFieldList();
+    }
+
+    // Modifies the FM record in place, replacing IDL-compatible values
+    // with native JS values.
+    private convertDatatypesToJs() {
+        this.idlDef.fields.forEach(field => {
+            if (field.datatype === 'bool') {
+                if (this.record[field.name]() === 't') {
+                    this.record[field.name](true);
+                } else if (this.record[field.name]() === 'f') {
+                    this.record[field.name](false);
+                }
+            }
+        });
+    }
+
+    // Modifies the provided FM record in place, replacing JS values
+    // with IDL-compatible values.
+    convertDatatypesToIdl(rec: IdlObject) {
+        const fields = this.idlDef.fields;
+        fields.forEach(field => {
+            if (field.datatype === 'bool') {
+                if (rec[field.name]() === true) {
+                    rec[field.name]('t');
+                // } else if (rec[field.name]() === false) {
+                } else { // TODO: some bools can be NULL
+                    rec[field.name]('f');
+                }
+            } else if (field.datatype === 'org_unit') {
+                const org = rec[field.name]();
+                if (org && typeof org === 'object') {
+                    rec[field.name](org.id());
+                }
+            }
+        });
+    }
+
+
+    private flattenLinkedValues(cls: string, list: IdlObject[]): any[] {
+        const idField = this.idl.classes[cls].pkey;
+        const selector =
+            this.idl.classes[cls].field_map[idField].selector || idField;
+
+        return list.map(item => {
+            return {id: item[idField](), name: item[selector]()};
+        });
+    }
+
+    private getFieldList(): Promise<any> {
+
+        this.fields = this.idlDef.fields.filter(f =>
+            !f.virtual && !this.hiddenFieldsList.includes(f.name)
+        );
+
+        const promises = [];
+
+        this.fields.forEach(field => {
+            field.readOnly = this.mode === 'view'
+                || this.readonlyFieldsList.includes(field.name);
+
+            if (this.isRequiredOverride &&
+                field.name in this.isRequiredOverride) {
+                field.isRequired = () => {
+                    return this.isRequiredOverride[field.name](field.name, this.record);
+                };
+            } else {
+                field.isRequired = () => {
+                    return field.required ||
+                        this.requiredFieldsList.includes(field.name);
+                };
+            }
+
+            if (field.datatype === 'link') {
+                promises.push(
+                    this.pcrud.retrieveAll(field.class, {}, {atomic : true})
+                    .toPromise().then(list => {
+                        field.linkedValues =
+                            this.flattenLinkedValues(field.class, list);
+                    })
+                );
+            } else if (field.datatype === 'org_unit') {
+                field.orgDefaultAllowed =
+                    this.orgDefaultAllowedList.includes(field.name);
+            }
+
+            if (this.customFieldTemplates[field.name]) {
+                field.template = this.customFieldTemplates[field.name].template;
+                field.context = this.customFieldTemplates[field.name].context;
+            }
+
+        });
+
+        // Wait for all network calls to complete
+        return Promise.all(promises);
+    }
+
+    // Returns a context object to be inserted into a custom
+    // field template.
+    customTemplateFieldContext(fieldDef: any): CustomFieldContext {
+        return Object.assign(
+            {   record : this.record,
+                field: fieldDef // from this.fields
+            },  fieldDef.context || {}
+        );
+    }
+
+    save() {
+        const recToSave = this.idl.clone(this.record);
+        this.convertDatatypesToIdl(recToSave);
+        this.pcrud[this.mode]([recToSave]).toPromise().then(
+            result => this.close(result),
+            error  => this.dismiss(error)
+        );
+    }
+
+    cancel() {
+        this.dismiss('canceled');
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html
new file mode 100644 (file)
index 0000000..3de90e4
--- /dev/null
@@ -0,0 +1,20 @@
+
+<span *ngIf="!column.cellTemplate"
+  [ngbTooltip]="tooltipContent"
+  placement="top-left"
+  class="{{context.cellClassCallback(row, column)}}"
+  triggers="mouseenter:mouseleave">
+  {{context.getRowColumnValue(row, column)}}
+</span>
+<span *ngIf="column.cellTemplate" 
+  class="{{context.cellClassCallback(row, column)}}"
+  [ngbTooltip]="tooltipContent"
+  placement="top-left"
+  #tooltip="ngbTooltip" 
+  (mouseenter)="tooltip.open(column.getCellContext(row))"
+  (mouseleave)="tooltip.close()" triggers="manual">
+  <ng-container #templateContainer
+    *ngTemplateOutlet="column.cellTemplate; context: column.getCellContext(row)">
+  </ng-container> 
+</span>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts
new file mode 100644 (file)
index 0000000..3d844f3
--- /dev/null
@@ -0,0 +1,57 @@
+import {Component, Input, OnInit, AfterViewInit,
+    TemplateRef, ElementRef, AfterContentChecked} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+
+@Component({
+  selector: 'eg-grid-body-cell',
+  templateUrl: './grid-body-cell.component.html'
+})
+
+export class GridBodyCellComponent implements OnInit, AfterContentChecked {
+
+    @Input() context: GridContext;
+    @Input() row: any;
+    @Input() column: GridColumn;
+
+    initDone: boolean;
+    tooltipContent: string | TemplateRef<any>;
+
+    constructor(
+        private elm: ElementRef
+    ) {}
+
+    ngOnInit() {}
+
+    ngAfterContentChecked() {
+        this.setTooltip();
+    }
+
+    // Returns true if the contents of this cell exceed the
+    // boundaries of its container.
+    cellOverflows(): boolean {
+        let node = this.elm.nativeElement;
+        if (node) {
+            node = node.parentNode;
+            return node && (
+                node.scrollHeight > node.clientHeight ||
+                node.scrollWidth > node.clientWidth
+            );
+        }
+        return false;
+    }
+
+    // Tooltips are only applied to cells whose contents exceed
+    // their container.
+    // Applying an empty string value prevents a tooltip from rendering.
+    setTooltip() {
+        if (this.cellOverflows()) {
+            this.tooltipContent = this.column.cellTemplate ||
+                this.context.getRowColumnValue(this.row, this.column);
+        } else {
+            // No tooltip
+            this.tooltipContent = '';
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html
new file mode 100644 (file)
index 0000000..b7284fe
--- /dev/null
@@ -0,0 +1,39 @@
+<!--
+  tabindex=1 so the grid body can capture keyboard events.
+-->
+<div class="eg-grid-body" tabindex="1" (keydown)="onGridKeyDown($event)">
+  <div class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}"
+    [ngClass]="{'selected': context.rowSelector.contains(context.getRowIndex(row))}"
+    *ngFor="let row of context.dataSource.getPageOfRows(context.pager); let idx = index">
+
+    <div class="eg-grid-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+      <input type='checkbox' [(ngModel)]="context.rowSelector.indexes[context.getRowIndex(row)]">
+    </div>
+    <div class="eg-grid-cell eg-grid-number-cell eg-grid-cell-skinny">
+      {{context.pager.rowNumber(idx)}}
+    </div>
+    <div *ngIf="context.rowFlairIsEnabled" class="eg-grid-cell eg-grid-flair-cell">
+      <!-- using *ngIf allows us to assign the flair callback to a value,
+            obviating the need for multiple calls of the same function -->
+      <ng-container *ngIf="context.rowFlairCallback(row); let flair">
+        <ng-container *ngIf="flair.icon">
+          <!-- tooltip is disabled when no title is set -->
+          <span class="material-icons"
+            ngbTooltip="{{flair.title || ''}}" triggers="mouseenter:mouseleave">
+            {{flair.icon}}
+          </span>
+        </ng-container>
+      </ng-container>
+    </div>
+    <div class="eg-grid-cell eg-grid-body-cell" [ngStyle]="{flex:col.flex}"
+      [ngClass]="{'eg-grid-cell-overflow': context.overflowCells}"
+      (dblclick)="onRowDblClick(row)"
+      (click)="onRowClick($event, row, idx)"
+      *ngFor="let col of context.columnSet.displayColumns()">
+
+      <eg-grid-body-cell [context]="context" [row]="row" [column]="col">
+      </eg-grid-body-cell>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts
new file mode 100644 (file)
index 0000000..e4829ce
--- /dev/null
@@ -0,0 +1,77 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+import {GridComponent} from './grid.component';
+
+@Component({
+  selector: 'eg-grid-body',
+  templateUrl: './grid-body.component.html'
+})
+
+export class GridBodyComponent implements OnInit {
+
+    @Input() context: GridContext;
+
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {}
+
+    // Not using @HostListener because it only works globally.
+    onGridKeyDown(evt: KeyboardEvent) {
+        switch (evt.key) {
+            case 'ArrowUp':
+                this.context.selectPreviousRow();
+                evt.stopPropagation();
+                break;
+            case 'ArrowDown':
+                this.context.selectNextRow();
+                evt.stopPropagation();
+                break;
+            case 'ArrowLeft':
+                this.context.toPrevPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
+                evt.stopPropagation();
+                break;
+            case 'ArrowRight':
+                this.context.toNextPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
+                evt.stopPropagation();
+                break;
+            case 'Enter':
+                if (this.context.lastSelectedIndex) {
+                    this.grid.onRowActivate.emit(
+                        this.context.getRowByIndex(
+                            this.context.lastSelectedIndex)
+                    );
+                }
+                evt.stopPropagation();
+                break;
+        }
+    }
+
+    onRowClick($event: any, row: any, idx: number) {
+        const index = this.context.getRowIndex(row);
+
+        if (this.context.disableMultiSelect) {
+            this.context.selectOneRow(index);
+        } else if ($event.ctrlKey || $event.metaKey /* mac command */) {
+            if (this.context.toggleSelectOneRow(index)) {
+                this.context.lastSelectedIndex = index;
+            }
+
+        } else if ($event.shiftKey) {
+            // TODO shift range click
+
+        } else {
+            this.context.selectOneRow(index);
+        }
+
+        this.grid.onRowClick.emit(row);
+    }
+
+    onRowDblClick(row: any) {
+        this.grid.onRowActivate.emit(row);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html
new file mode 100644 (file)
index 0000000..3af756c
--- /dev/null
@@ -0,0 +1,69 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Grid Columns Configuration</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body eg-grid-column-config-dialog">
+
+    <div class="row">
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Visible</div>
+      <div class="col-lg-3 eg-grid-header-cell" i18n>Column Name</div>
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Move Up</div>
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Move Down</div>
+      <div class="col-lg-2 eg-grid-header-cell" i18n>First Visible</div>
+      <div class="col-lg-2 eg-grid-header-cell" i18n>Last Visible</div>
+      <div class="col-lg-2 eg-grid-header-cell" 
+        *ngIf="columnSet.isMultiSortable" i18n>Sort Priority</div>
+    </div>
+    <div class="row pt-1" *ngFor="let col of columnSet.columns"
+      [ngClass]="{visible : col.visible}">
+      <div class="col-lg-1" (click)="col.visible=!col.visible">
+        <span *ngIf="col.visible" class="badge badge-success">&#x2713;</span>
+        <span *ngIf="!col.visible" class="badge badge-warning">&#x2717;</span>
+      </div>
+      <div class="col-lg-3" (click)="col.visible=!col.visible">{{col.label}}</div>
+      <div class="col-lg-1">
+        <a class="no-href" title="Move column up" i18n-title
+          (click)="columnSet.moveColumn(col, -1)">
+          <span class="material-icons">arrow_upward</span>
+        </a>
+      </div>
+      <div class="col-lg-1">
+        <a class="no-href" title="Move column down" i18n-title
+          (click)="columnSet.moveColumn(col, 1)">
+          <span class="material-icons">arrow_downward</span>
+        </a>
+      </div>
+      <div class="col-lg-2">
+        <a class="no-href" title="Make first visible" i18n-title
+          (click)="columnSet.moveColumn(col, -10000)">
+          <span class="material-icons">vertical_align_top</span>
+        </a>
+      </div>
+      <div class="col-lg-2">
+        <a class="no-href" title="Make last visible" i18n-title
+          (click)="columnSet.moveColumn(col, 10000)">
+          <span class="material-icons">vertical_align_bottom</span>
+        </a>
+      </div>
+      <div class="col-lg-2" *ngIf="columnSet.isMultiSortable">
+        <div *ngIf="col.isMultiSortable">
+          <input type='number' [(ngModel)]="col.sort"
+            title="Sort Priority / Direction" i18n-title style='width:2.8em'/>
+        </div>
+      </div>
+
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-info" (click)="columnSet.moveVisibleToFront()">
+      Move Visible Columns To Top
+    </button>
+    <button type="button" class="btn btn-success ml-2" 
+      (click)="close('confirmed')" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts
new file mode 100644 (file)
index 0000000..10ad606
--- /dev/null
@@ -0,0 +1,16 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {GridColumnSet} from './grid';
+
+@Component({
+  selector: 'eg-grid-column-config',
+  templateUrl: './grid-column-config.component.html'
+})
+
+/**
+ */
+export class GridColumnConfigComponent extends DialogComponent implements OnInit {
+    @Input() columnSet: GridColumnSet;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html
new file mode 100644 (file)
index 0000000..ca24c00
--- /dev/null
@@ -0,0 +1,20 @@
+<div *ngIf="isVisible" class="eg-grid-column-width-config">
+  <div class="eg-grid-row">
+    <div class="eg-grid-column-width-header" i18n>Expand</div>
+    <div *ngFor="let col of columnSet.displayColumns()" 
+      class="eg-grid-cell text-center" [ngStyle]="{flex:col.flex}">
+      <a (click)="expandColumn(col)" title="Expand Column" i18n-title>
+        <span class="material-icons eg-grid-column-width-icon">call_made</span>
+      </a>
+    </div>
+  </div>
+  <div class="eg-grid-row">
+    <div class="eg-grid-column-width-header" i18n>Shrink</div>
+    <div *ngFor="let col of columnSet.displayColumns()" 
+      class="eg-grid-cell text-center" [ngStyle]="{flex:col.flex}">
+      <a (click)="shrinkColumn(col)" title="Shrink Column" i18n-title>
+        <span class="material-icons eg-grid-column-width-icon">call_received</span>
+      </a>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
new file mode 100644 (file)
index 0000000..f9bacf4
--- /dev/null
@@ -0,0 +1,32 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {GridContext, GridColumn, GridColumnSet,
+    GridDataSource} from './grid';
+
+@Component({
+  selector: 'eg-grid-column-width',
+  templateUrl: './grid-column-width.component.html'
+})
+
+export class GridColumnWidthComponent implements OnInit {
+
+    @Input() gridContext: GridContext;
+    columnSet: GridColumnSet;
+    isVisible: boolean;
+
+    constructor() {}
+
+    ngOnInit() {
+        this.isVisible = false;
+        this.columnSet = this.gridContext.columnSet;
+    }
+
+    expandColumn(col: GridColumn) {
+        col.flex++;
+    }
+
+    shrinkColumn(col: GridColumn) {
+        if (col.flex > 1) { col.flex--; }
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
new file mode 100644 (file)
index 0000000..dffede1
--- /dev/null
@@ -0,0 +1,57 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridColumn, GridColumnSet} from './grid';
+import {GridComponent} from './grid.component';
+
+@Component({
+  selector: 'eg-grid-column',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridColumnComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() name: string;
+    @Input() path: string;
+    @Input() label: string;
+    @Input() flex: number;
+    // is this the index field?
+    @Input() index: boolean;
+
+    // Columns are assumed to be visible unless hidden=true.
+    @Input() hidden: boolean;
+
+    @Input() sortable: boolean;
+    @Input() datatype: string;
+    @Input() multiSortable: boolean;
+
+    // Used in conjunction with cellTemplate
+    @Input() cellContext: any;
+    @Input() cellTemplate: TemplateRef<any>;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridColumnComponent needs an <eg-grid>');
+            return;
+        }
+
+        const col = new GridColumn();
+        col.name = this.name;
+        col.path = this.path;
+        col.label = this.label;
+        col.flex = this.flex;
+        col.hidden = this.hidden === true;
+        col.isIndex = this.index === true;
+        col.cellTemplate = this.cellTemplate;
+        col.cellContext = this.cellContext;
+        col.isSortable = this.sortable;
+        col.isMultiSortable = this.multiSortable;
+        col.datatype = this.datatype;
+        col.isAuto = false;
+        this.grid.context.columnSet.add(col);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
new file mode 100644 (file)
index 0000000..58e0c66
--- /dev/null
@@ -0,0 +1,32 @@
+
+<div class="eg-grid-row eg-grid-header-row">
+  <div class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+    <input type='checkbox' (click)="handleBatchSelect($event)">
+  </div>
+  <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
+    <span i18n="number|Row Number Header">#</span>
+  </div>
+  <div *ngIf="context.rowFlairIsEnabled" 
+    class="eg-grid-cell eg-grid-header-cell eg-grid-flair-cell">
+    <span class="material-icons">notifications</span>
+  </div>
+  <div *ngFor="let col of context.columnSet.displayColumns()" 
+    draggable="true" 
+    (dragstart)="dragColumn = col"
+    (drop)="onColumnDrop(col)"
+    (dragover)="onColumnDragEnter($event, col)"
+    (dragleave)="onColumnDragLeave($event, col)"
+    [ngClass]="{'dragover' : col.isDragTarget}"
+    class="eg-grid-cell eg-grid-header-cell" [ngStyle]="{flex:col.flex}">
+    <a class="sortable label-with-material-icon" *ngIf="col.isSortable" 
+      (click)="sortOneColumn(col)">
+      <span class="eg-grid-header-cell-sort-label">{{col.label}}</span>
+      <span class="material-icons eg-grid-header-cell-sort-arrow"
+        *ngIf="isColumnSorting(col, 'ASC')">arrow_downwards</span>
+      <span class="material-icons eg-grid-header-cell-sort-arrow"
+        *ngIf="isColumnSorting(col, 'DESC')">arrow_upwards</span>
+    </a>
+    <span *ngIf="!col.isSortable">{{col.label}}</span>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
new file mode 100644 (file)
index 0000000..0010a45
--- /dev/null
@@ -0,0 +1,85 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+
+@Component({
+  selector: 'eg-grid-header',
+  templateUrl: './grid-header.component.html'
+})
+
+export class GridHeaderComponent implements OnInit {
+
+    @Input() context: GridContext;
+
+    dragColumn: GridColumn;
+
+    constructor() {}
+
+    ngOnInit() {}
+
+    onColumnDragEnter($event: any, col: any) {
+        if (this.dragColumn && this.dragColumn.name !== col.name) {
+            col.isDragTarget = true;
+        }
+        $event.preventDefault();
+    }
+
+    onColumnDragLeave($event: any, col: any) {
+        col.isDragTarget = false;
+        $event.preventDefault();
+    }
+
+    onColumnDrop(col: GridColumn) {
+        this.context.columnSet.insertBefore(this.dragColumn, col);
+        this.context.columnSet.columns.forEach(c => c.isDragTarget = false);
+    }
+
+    sortOneColumn(col: GridColumn) {
+        let dir = 'ASC';
+        const sort = this.context.dataSource.sort;
+
+        if (sort.length && sort[0].name === col.name && sort[0].dir === 'ASC') {
+            dir = 'DESC';
+        }
+
+        this.context.dataSource.sort = [{name: col.name, dir: dir}];
+
+        if (this.context.useLocalSort) {
+            this.context.sortLocal();
+        } else {
+            this.context.reload();
+        }
+    }
+
+    // Returns true if the provided column is sorting in the
+    // specified direction.
+    isColumnSorting(col: GridColumn, dir: string): boolean {
+        const sort = this.context.dataSource.sort.filter(c => c.name === col.name)[0];
+        return sort && sort.dir === dir;
+    }
+
+    handleBatchSelect($event) {
+        if ($event.target.checked) {
+            if (this.context.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
+                // clear selections from other pages to avoid confusion.
+                this.context.rowSelector.clear();
+                this.selectAll();
+            }
+        } else {
+            this.context.rowSelector.clear();
+        }
+    }
+
+    selectAll() {
+        const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+        const indexes = rows.map(r => this.context.getRowIndex(r));
+        this.context.rowSelector.select(indexes);
+    }
+
+    allRowsAreSelected(): boolean {
+        const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+        const indexes = rows.map(r => this.context.getRowIndex(r));
+        return this.context.rowSelector.contains(indexes);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html
new file mode 100644 (file)
index 0000000..a098792
--- /dev/null
@@ -0,0 +1,30 @@
+
+
+<ng-container>
+  <eg-progress-dialog #progressDialog></eg-progress-dialog>
+  <ng-template #printTemplate let-context>
+    <div>
+      <style>
+        .grid-print-table {
+          border-collapse: collapse;
+          margin: 1px;
+        }
+        .grid-print-table td {
+          padding: 2px;
+          border: 1px solid #aaa;
+        }
+      </style>
+      <table class="grid-print-table">
+        <thead>
+          <tr><th *ngFor="let col of context.columns">{{col.label}}</th></tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let row of context.rows; trackBy: index">
+            <!-- item values have already been filtered, etc. -->
+            <td *ngFor="let col of context.columns"><span>{{row[col.name]}}</span></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </ng-template>
+</ng-container>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts
new file mode 100644 (file)
index 0000000..f73e26b
--- /dev/null
@@ -0,0 +1,45 @@
+import {Component, Input, TemplateRef, ViewChild} from '@angular/core';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {GridContext} from '@eg/share/grid/grid';
+
+@Component({
+  selector: 'eg-grid-print',
+  templateUrl: './grid-print.component.html'
+})
+
+/**
+ */
+export class GridPrintComponent {
+
+    @Input() gridContext: GridContext;
+    @ViewChild('printTemplate') private printTemplate: TemplateRef<any>;
+    @ViewChild('progressDialog')
+        private progressDialog: ProgressDialogComponent;
+
+    constructor(private printer: PrintService) {}
+
+    printGrid() {
+        this.progressDialog.open();
+        const columns = this.gridContext.columnSet.displayColumns();
+        const textItems = {columns: columns, rows: []};
+
+        this.gridContext.getAllRowsAsText().subscribe(
+            row => {
+              this.progressDialog.increment();
+              textItems.rows.push(row);
+            },
+            err => this.progressDialog.close(),
+            ()  => {
+                this.progressDialog.close();
+                this.printer.print({
+                    template: this.printTemplate,
+                    contextData: textItems,
+                    printContext: 'default'
+                });
+            }
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
new file mode 100644 (file)
index 0000000..593530a
--- /dev/null
@@ -0,0 +1,33 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarAction} from './grid';
+import {GridComponent} from './grid.component';
+
+@Component({
+  selector: 'eg-grid-toolbar-action',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarActionComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+    @Input() action: (rows: any[]) => any;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarActionComponent needs a [grid]');
+            return;
+        }
+
+        const action = new GridToolbarAction();
+        action.label = this.label;
+        action.action = this.action;
+
+        this.grid.context.toolbarActions.push(action);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
new file mode 100644 (file)
index 0000000..8287483
--- /dev/null
@@ -0,0 +1,43 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarButton} from './grid';
+import {GridComponent} from './grid.component';
+
+@Component({
+  selector: 'eg-grid-toolbar-button',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarButtonComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+    @Input() action: () => any;
+
+    @Input() set disabled(d: boolean) {
+        // Support asynchronous disabled values by appling directly
+        // to our button object as values arrive.
+        if (this.button) {
+            this.button.disabled = d;
+        }
+    }
+
+    button: GridToolbarButton;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {
+        this.button = new GridToolbarButton();
+    }
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarButtonComponent needs a [grid]');
+            return;
+        }
+
+        this.button.label = this.label;
+        this.button.action = this.action;
+        this.grid.context.toolbarButtons.push(this.button);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
new file mode 100644 (file)
index 0000000..f078797
--- /dev/null
@@ -0,0 +1,37 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarCheckbox} from './grid';
+import {GridComponent} from './grid.component';
+
+@Component({
+  selector: 'eg-grid-toolbar-checkbox',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarCheckboxComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+
+    // This is an input instead of an Output because the handler is
+    // passed off to the grid context for maintenance -- events
+    // are not fired directly from this component.
+    @Input() onChange: (checked: boolean) => void;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarCheckboxComponent needs a [grid]');
+            return;
+        }
+
+        const cb = new GridToolbarCheckbox();
+        cb.label = this.label;
+        cb.onChange = this.onChange;
+
+        this.grid.context.toolbarCheckboxes.push(cb);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
new file mode 100644 (file)
index 0000000..ae24021
--- /dev/null
@@ -0,0 +1,152 @@
+
+<div class="eg-grid-toolbar mb-2">
+
+  <div class="btn-toolbar">
+
+    <!-- buttons -->
+    <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
+      <button *ngFor="let btn of gridContext.toolbarButtons" 
+        [disabled]="btn.disabled"
+        class="btn btn-outline-dark mr-1" (click)="btn.action()">
+        {{btn.label}}
+      </button>
+    </div>
+
+    <!-- checkboxes -->
+    <div class="form-check form-check-inline" 
+      *ngIf="gridContext.toolbarCheckboxes.length">
+      <ng-container *ngFor="let cb of gridContext.toolbarCheckboxes">
+        <label class="form-check-label">
+          <input class="form-check-input" type="checkbox" 
+            (click)="cb.onChange($event.target.checked)"/>
+            {{cb.label}}
+        </label>
+      </ng-container>
+    </div>
+  </div>
+
+  <!-- push everything else to the right -->
+  <div class="flex-1"></div>
+
+  <div ngbDropdown class="mr-1" placement="bottom-right">
+    <button ngbDropdownToggle [disabled]="!gridContext.toolbarActions.length"
+        class="btn btn-outline-dark no-dropdown-caret">
+      <span title="Actions For Selected Rows" i18n-title 
+        class="material-icons mat-icon-in-button">playlist_add_check</span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item" (click)="performAction(action)"
+        *ngFor="let action of gridContext.toolbarActions">
+        <span class="ml-2">{{action.label}}</span>
+      </a>
+    </div>
+  </div>
+
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toFirst()">
+    <span title="First Page" i18n-title 
+        class="material-icons mat-icon-in-button">first_page</span>
+  </button>
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.decrement()">
+    <span title="Previous Page" i18n-title 
+        class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
+  </button>
+  <button [disabled]="gridContext.pager.isLastPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.increment()">
+    <span title="Next Page" i18n-title 
+        class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
+  </button>
+
+  <!--
+  Hiding jump-to-last since there's no analog in the angularjs grid and
+  it has limited value since the size of the data set is often unknown.
+  <button [disabled]="!gridContext.pager.resultCount || gridContext.pager.isLastPage()" 
+    type="button" class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toLast()">
+    <span title="First Page" i18n-title 
+        class="material-icons mat-icon-in-button">last_page</span>
+  </button>
+  -->
+
+  <div ngbDropdown class="mr-1" placement="bottom-right">
+    <button ngbDropdownToggle class="btn btn-outline-dark text-button">
+      <span title="Select Row Count" i18n-title i18n>
+        Rows {{gridContext.pager.limit}}
+      </span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item" 
+        *ngFor="let count of [5, 10, 25, 50, 100]"
+        (click)="gridContext.pager.setLimit(count)">
+        <span class="ml-2">{{count}}</span>
+      </a>
+    </div>
+  </div>
+
+  <button type="button" 
+    class="btn btn-outline-dark mr-1" 
+    (click)="gridContext.overflowCells=!gridContext.overflowCells">
+    <span *ngIf="!gridContext.overflowCells"
+      title="Expand Cells Vertically" i18n-title 
+      class="material-icons mat-icon-in-button">expand_more</span>
+    <span *ngIf="gridContext.overflowCells"
+      title="Collaps Cells Vertically" i18n-title 
+      class="material-icons mat-icon-in-button">expand_less</span>
+  </button>
+
+  <eg-grid-column-config #columnConfDialog [columnSet]="gridContext.columnSet">
+  </eg-grid-column-config>
+  <div ngbDropdown placement="bottom-right">
+    <button ngbDropdownToggle class="btn btn-outline-dark no-dropdown-caret">
+      <span title="Show Grid Options" i18n-title 
+        class="material-icons mat-icon-in-button">settings</span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="columnConfDialog.open({size:'lg'})">
+        <span class="material-icons">build</span>
+        <span class="ml-2" i18n>Manage Columns</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="colWidthConfig.isVisible = !colWidthConfig.isVisible">
+        <span class="material-icons">compare_arrows</span>
+        <span class="ml-2" i18n>Manage Column Widths</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="saveGridConfig()">
+        <span class="material-icons">save</span>
+        <span class="ml-2" i18n>Save Grid Settings</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="gridContext.columnSet.reset()">
+        <span class="material-icons">restore</span>
+        <span class="ml-2" i18n>Reset Columns</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="generateCsvExportUrl($event)"
+        [download]="csvExportFileName"
+        [href]="csvExportUrl">
+        <span class="material-icons">cloud_download</span>
+        <span class="ml-2" i18n>Download Full CSV</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" (click)="printHtml()">
+        <span class="material-icons">print</span>
+        <span class="ml-2" i18n>Print Full Grid</span>
+      </a>
+
+      <div class="dropdown-divider"></div>
+
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="col.visible=!col.visible" *ngFor="let col of gridContext.columnSet.columns">
+        <span *ngIf="col.visible" class="badge badge-success">&#x2713;</span>
+        <span *ngIf="!col.visible" class="badge badge-warning">&#x2717;</span>
+        <span class="ml-2">{{col.label}}</span>
+      </a>
+
+    </div>
+  </div>
+
+<div>
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
new file mode 100644 (file)
index 0000000..5c8b523
--- /dev/null
@@ -0,0 +1,86 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
+import {Pager} from '@eg/share/util/pager';
+import {GridColumn, GridColumnSet, GridToolbarButton,
+    GridToolbarAction, GridContext, GridDataSource} from '@eg/share/grid/grid';
+import {GridColumnWidthComponent} from './grid-column-width.component';
+import {GridPrintComponent} from './grid-print.component';
+
+@Component({
+  selector: 'eg-grid-toolbar',
+  templateUrl: 'grid-toolbar.component.html'
+})
+
+export class GridToolbarComponent implements OnInit {
+
+    @Input() gridContext: GridContext;
+    @Input() colWidthConfig: GridColumnWidthComponent;
+    @Input() gridPrinter: GridPrintComponent;
+
+    csvExportInProgress: boolean;
+    csvExportUrl: SafeUrl;
+    csvExportFileName: string;
+
+    constructor(private sanitizer: DomSanitizer) {}
+
+    ngOnInit() {}
+
+    saveGridConfig() {
+        // TODO: when server-side settings are supported, this operation
+        // may offer to save to user/workstation OR org unit settings
+        // depending on perms.
+
+        this.gridContext.saveGridConfig().then(
+            // hide the with config after saving
+            ok => this.colWidthConfig.isVisible = false,
+            err => console.error(`Error saving columns: ${err}`)
+        );
+    }
+
+    performAction(action: GridToolbarAction) {
+        action.action(this.gridContext.getSelectedRows());
+    }
+
+    printHtml() {
+        this.gridPrinter.printGrid();
+    }
+
+    generateCsvExportUrl($event) {
+
+        if (this.csvExportInProgress) {
+            // This is secondary href click handler.  Give the
+            // browser a moment to start the download, then reset
+            // the CSV download attributes / state.
+            setTimeout(() => {
+                this.csvExportUrl = null;
+                this.csvExportFileName = '';
+                this.csvExportInProgress = false;
+               }, 500
+            );
+            return;
+        }
+
+        this.csvExportInProgress = true;
+
+        // let the file name describe the grid
+        this.csvExportFileName = (
+            this.gridContext.persistKey || 'eg_grid_data'
+        ).replace(/\s+/g, '_') + '.csv';
+
+        this.gridContext.gridToCsv().then(csv => {
+            const blob = new Blob([csv], {type : 'text/plain'});
+            const win: any = window; // avoid TS errors
+            this.csvExportUrl = this.sanitizer.bypassSecurityTrustUrl(
+                (win.URL || win.webkitURL).createObjectURL(blob)
+            );
+
+            // Fire the 2nd click event now that the browser has
+            // information on how to download the CSV file.
+            setTimeout(() => $event.target.click());
+        });
+
+        $event.preventDefault();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.css b/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
new file mode 100644 (file)
index 0000000..9748c0c
--- /dev/null
@@ -0,0 +1,142 @@
+
+.eg-grid {
+    width: 100%;
+    color: rgba(0,0,0,.87); 
+}
+    
+.eg-grid-row {
+    display: flex;
+    border-bottom: 1px solid rgba(0,0,0,.12);
+    padding-left: 10px;
+    padding-right: 10px;
+}
+
+.eg-grid-header-row {
+  /* matches bootstrap card-header css */
+  background-color: rgba(0,0,0,.03);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+.eg-grid-body {
+    outline: none; /* for keyboard events */
+}
+
+.eg-grid-body-row {
+}
+
+.eg-grid-body-row.selected, 
+.eg-grid-column-config-dialog .visible {
+  color: #004085;
+  background-color: #cce5ff;
+  border-color: #b8daff;
+}
+
+.eg-grid-header-cell {
+    font-weight: bold;
+}
+
+.eg-grid-header-cell.dragover {
+    background-color: #cce5ff;
+    border-color: #b8daff;
+}
+
+.eg-grid-header-cell-sort-label {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.eg-grid-header-cell-sort-arrow {
+  font-size: 14px;
+}
+
+.eg-grid-cell {
+    flex: 2; /* applied per column */
+    padding: 6px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+/* allow tooltips to be wider than the default 200px */
+.eg-grid-cell .tooltip-inner {
+  max-width: 400px; 
+}
+
+/* in overflow mode, allow white space to wrap so the 
+ * full contents of the cell can be seen inline.  leaving 
+ * text-overflow and overlow as-is means long strings with
+ * no space will still be truncated with ellipses to avoid
+ * inconsistent grid column widths
+ */
+.eg-grid-cell-overflow {
+    white-space: normal;
+}
+
+.eg-grid-body-cell {
+}
+
+.eg-grid-toolbar {
+  display: flex;
+}
+
+.eg-grid-toolbar .material-icons {
+  font-size: 20px;
+}
+
+.eg-grid-toolbar .form-check-label:nth-child(even) {
+  padding-left: 5px;
+  padding-right: 5px;
+  margin-left: 3px;
+  margin-right: 3px;
+  border-radius: 5px;
+  background-color: rgba(0,0,0,.03);
+  border: 1px solid rgba(0,0,0,.125);
+}
+
+/* Kind of hacky -- only way to get a toolbar button with no 
+ * mat icon to line up horizontally with mat icon buttons */
+.eg-grid-toolbar .text-button {
+  padding-top: 11px;
+  padding-bottom: 11px;
+}
+
+.eg-grid-cell-skinny {
+  width: 2.2em;
+  text-align: center;
+  flex: none;
+}
+
+.eg-grid-flair-cell {
+  /* mat icons currently 22px, unclear why it needs this much space */
+  width: 34px; 
+  text-align: center;
+  flex: none;
+}
+
+/* depends on width of .eg-grid-cell-skinny */
+.eg-grid-column-width-header {
+  width: 4.4em;
+  text-align: center;
+  flex: none;
+  display: inline-flex;
+  vertical-align: middle;
+  align-items: center;
+}
+
+.eg-grid-column-width-config .eg-grid-cell {
+    border-left: 2px dashed grey;
+}
+
+.eg-grid-column-width-icon {
+  cursor: pointer;
+  font-size: 18px;
+  color: #007bff;
+}
+
+.eg-grid-column-config-dialog {
+  height: auto;
+  max-height: 400px;
+  overflow: auto;
+  box-shadow: none;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
new file mode 100644 (file)
index 0000000..a98e17a
--- /dev/null
@@ -0,0 +1,27 @@
+
+<div class="eg-grid">
+
+  <eg-grid-toolbar
+    [gridContext]="context" 
+    [gridPrinter]="gridPrinter"
+    [colWidthConfig]="colWidthConfig">
+  </eg-grid-toolbar>
+
+  <eg-grid-header [context]="context"></eg-grid-header>
+
+  <eg-grid-column-width #colWidthConfig [gridContext]="context">
+  </eg-grid-column-width>
+  
+  <eg-grid-print #gridPrinter [gridContext]="context">
+  </eg-grid-print>
+
+  <!-- move me too -->
+  <div class="row" *ngIf="dataSource.data.length == 0">
+    <div class="col-lg-12 text-center alert alert-light font-italic" i18n>
+      Nothing to Display
+    </div>
+  </div>
+
+  <eg-grid-body [context]="context"></eg-grid-body>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
new file mode 100644 (file)
index 0000000..1fa4c2c
--- /dev/null
@@ -0,0 +1,149 @@
+import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
+    OnDestroy, HostListener, ViewEncapsulation} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {FormatService} from '@eg/core/format.service';
+import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
+
+/**
+ * Main grid entry point.
+ */
+
+@Component({
+  selector: 'eg-grid',
+  templateUrl: './grid.component.html',
+  styleUrls: ['grid.component.css'],
+  // share grid css globally once imported so all grid component CSS
+  // can live in grid.component.css and to avoid multiple copies of
+  // the CSS when multiple grids are displayed.
+  encapsulation: ViewEncapsulation.None
+})
+
+export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
+
+    // Source of row data.
+    @Input() dataSource: GridDataSource;
+
+    // IDL class for auto-generation of columns
+    @Input() idlClass: string;
+
+    // True if any columns are sortable
+    @Input() sortable: boolean;
+
+    // True if the grid supports sorting of multiple columns at once
+    @Input() multiSortable: boolean;
+
+    // If true, grid sort requests only operate on data that
+    // already exists in the grid data source -- no row fetching.
+    // The assumption is all data is already available.
+    @Input() useLocalSort: boolean;
+
+    // Storage persist key / per-grid-type unique identifier
+    // The value is prefixed with 'eg.grid.'
+    @Input() persistKey: string;
+
+    // Prevent selection of multiple rows
+    @Input() disableMultiSelect: boolean;
+
+    // Show an extra column in the grid where the caller can apply
+    // row-specific flair (material icons).
+    @Input() rowFlairIsEnabled: boolean;
+
+    // Returns a material icon name to display in the flar column
+    // (if enabled) for the given row.
+    @Input() rowFlairCallback: (row: any) => GridRowFlairEntry;
+
+    // Returns a space-separated list of CSS class names to apply to
+    // a given row
+    @Input() rowClassCallback: (row: any) => string;
+
+    // Returns a space-separated list of CSS class names to apply to
+    // a given cell or all cells in a column.
+    @Input() cellClassCallback: (row: any, col: GridColumn) => string;
+
+    // comma-separated list of fields to show by default.
+    // This field takes precedence over hideFields.
+    // When a value is applied, any field not in this list will
+    // be hidden.
+    @Input() showFields: string;
+
+    // comma-separated list of fields to hide.
+    // This does not imply all other fields should be visible, only that
+    // the selected fields will be hidden.
+    @Input() hideFields: string;
+
+    // Allow the caller to jump directly to a specific page of
+    // grid data.
+    @Input() pageOffset: number;
+
+    context: GridContext;
+
+    // These events are emitted from our grid-body component.
+    // They are defined here for ease of access to the caller.
+    @Output() onRowActivate: EventEmitter<any>;
+    @Output() onRowClick: EventEmitter<any>;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private store: ServerStoreService,
+        private format: FormatService
+    ) {
+        this.context =
+            new GridContext(this.idl, this.org, this.store, this.format);
+        this.onRowActivate = new EventEmitter<any>();
+        this.onRowClick = new EventEmitter<any>();
+    }
+
+    ngOnInit() {
+
+        if (!this.dataSource) {
+            throw new Error('<eg-grid/> requires a [dataSource]');
+        }
+
+        this.context.idlClass = this.idlClass;
+        this.context.dataSource = this.dataSource;
+        this.context.persistKey = this.persistKey;
+        this.context.isSortable = this.sortable === true;
+        this.context.isMultiSortable = this.multiSortable === true;
+        this.context.useLocalSort = this.useLocalSort === true;
+        this.context.disableMultiSelect = this.disableMultiSelect === true;
+        this.context.rowFlairIsEnabled = this.rowFlairIsEnabled  === true;
+        this.context.rowFlairCallback = this.rowFlairCallback;
+        if (this.showFields) {
+            this.context.defaultVisibleFields = this.showFields.split(',');
+        }
+        if (this.hideFields) {
+            this.context.defaultHiddenFields = this.hideFields.split(',');
+        }
+
+        if (this.pageOffset) {
+            this.context.pager.offset = this.pageOffset;
+        }
+
+        // TS doesn't seem to like: let foo = bar || () => '';
+        this.context.rowClassCallback =
+            this.rowClassCallback || function () { return ''; };
+        this.context.cellClassCallback =
+            this.cellClassCallback || function() { return ''; };
+
+        this.context.init();
+    }
+
+    ngAfterViewInit() {
+        this.context.initData();
+    }
+
+    ngOnDestroy() {
+        this.context.destroy();
+    }
+
+    reload() {
+        this.context.reload();
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
new file mode 100644 (file)
index 0000000..0773a7e
--- /dev/null
@@ -0,0 +1,50 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {GridComponent} from './grid.component';
+import {GridColumnComponent} from './grid-column.component';
+import {GridHeaderComponent} from './grid-header.component';
+import {GridBodyComponent} from './grid-body.component';
+import {GridBodyCellComponent} from './grid-body-cell.component';
+import {GridToolbarComponent} from './grid-toolbar.component';
+import {GridToolbarButtonComponent} from './grid-toolbar-button.component';
+import {GridToolbarCheckboxComponent} from './grid-toolbar-checkbox.component';
+import {GridToolbarActionComponent} from './grid-toolbar-action.component';
+import {GridColumnConfigComponent} from './grid-column-config.component';
+import {GridColumnWidthComponent} from './grid-column-width.component';
+import {GridPrintComponent} from './grid-print.component';
+
+
+@NgModule({
+    declarations: [
+        // public + internal components
+        GridComponent,
+        GridColumnComponent,
+        GridHeaderComponent,
+        GridBodyComponent,
+        GridBodyCellComponent,
+        GridToolbarComponent,
+        GridToolbarButtonComponent,
+        GridToolbarCheckboxComponent,
+        GridToolbarActionComponent,
+        GridColumnConfigComponent,
+        GridColumnWidthComponent,
+        GridPrintComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        // public components
+        GridComponent,
+        GridColumnComponent,
+        GridToolbarButtonComponent,
+        GridToolbarCheckboxComponent,
+        GridToolbarActionComponent
+    ],
+    providers: [
+    ]
+})
+
+export class GridModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
new file mode 100644 (file)
index 0000000..e04940d
--- /dev/null
@@ -0,0 +1,972 @@
+/**
+ * Collection of grid related classses and interfaces.
+ */
+import {TemplateRef} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {FormatService} from '@eg/core/format.service';
+import {Pager} from '@eg/share/util/pager';
+
+const MAX_ALL_ROW_COUNT = 10000;
+
+export class GridColumn {
+    name: string;
+    path: string;
+    label: string;
+    flex: number;
+    align: string;
+    hidden: boolean;
+    visible: boolean;
+    sort: number;
+    idlClass: string;
+    idlFieldDef: any;
+    datatype: string;
+    cellTemplate: TemplateRef<any>;
+    cellContext: any;
+    isIndex: boolean;
+    isDragTarget: boolean;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    comparator: (valueA: any, valueB: any) => number;
+
+    // True if the column was automatically generated.
+    isAuto: boolean;
+
+    flesher: (obj: any, col: GridColumn, item: any) => any;
+
+    getCellContext(row: any) {
+        return {
+          col: this,
+          row: row,
+          userContext: this.cellContext
+        };
+    }
+}
+
+export class GridColumnSet {
+    columns: GridColumn[];
+    idlClass: string;
+    indexColumn: GridColumn;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    stockVisible: string[];
+    idl: IdlService;
+    defaultHiddenFields: string[];
+    defaultVisibleFields: string[];
+
+    constructor(idl: IdlService, idlClass?: string) {
+        this.idl = idl;
+        this.columns = [];
+        this.stockVisible = [];
+        this.idlClass = idlClass;
+    }
+
+    add(col: GridColumn) {
+
+        this.applyColumnDefaults(col);
+
+        if (!this.insertColumn(col)) {
+            // Column was rejected as a duplicate.
+            return;
+        }
+
+        if (col.isIndex) { this.indexColumn = col; }
+
+        // track which fields are visible on page load.
+        if (col.visible) {
+            this.stockVisible.push(col.name);
+        }
+
+        this.applyColumnSortability(col);
+    }
+
+    // Returns true if the new column was inserted, false otherwise.
+    // Declared columns take precedence over auto-generated columns
+    // when collisions occur.
+    // Declared columns are inserted in front of auto columns.
+    insertColumn(col: GridColumn): boolean {
+
+        if (col.isAuto) {
+            if (this.getColByName(col.name)) {
+                // New auto-generated column conflicts with existing
+                // column.  Skip it.
+                return false;
+            } else {
+                // No collisions.  Add to the end of the list
+                this.columns.push(col);
+                return true;
+            }
+        }
+
+        // Adding a declared column.
+
+        // Check for dupes.
+        for (let idx = 0; idx < this.columns.length; idx++) {
+            const testCol = this.columns[idx];
+            if (testCol.name === col.name) { // match found
+                if (testCol.isAuto) {
+                    // new column takes precedence, remove the existing column.
+                    this.columns.splice(idx, 1);
+                    break;
+                } else {
+                    // New column does not take precedence.  Avoid
+                    // inserting it.
+                    return false;
+                }
+            }
+        }
+
+        // Delcared columns are inserted just before the first auto-column
+        for (let idx = 0; idx < this.columns.length; idx++) {
+            const testCol = this.columns[idx];
+            if (testCol.isAuto) {
+                if (idx === 0) {
+                    this.columns.unshift(col);
+                } else {
+                    this.columns.splice(idx - 1, 0, col);
+                }
+                return true;
+            }
+        }
+
+        // No insertion point found.  Toss the new column on the end.
+        this.columns.push(col);
+        return true;
+    }
+
+    getColByName(name: string): GridColumn {
+        return this.columns.filter(c => c.name === name)[0];
+    }
+
+    idlInfoFromDotpath(dotpath: string): any {
+        if (!dotpath || !this.idlClass) { return null; }
+
+        let idlParent;
+        let idlField;
+        let idlClass = this.idl.classes[this.idlClass];
+
+        const pathParts = dotpath.split(/\./);
+
+        for (let i = 0; i < pathParts.length; i++) {
+            const part = pathParts[i];
+            idlParent = idlField;
+            idlField = idlClass.field_map[part];
+
+            if (idlField) {
+                if (idlField['class'] && (
+                    idlField.datatype === 'link' ||
+                    idlField.datatype === 'org_unit')) {
+                    idlClass = this.idl.classes[idlField['class']];
+                }
+            } else {
+                return null;
+            }
+        }
+
+        return {
+            idlParent: idlParent,
+            idlField : idlField,
+            idlClass : idlClass
+        };
+    }
+
+
+    reset() {
+        this.columns.forEach(col => {
+            col.flex = 2;
+            col.sort = 0;
+            col.align = 'left';
+            col.visible = this.stockVisible.includes(col.name);
+        });
+    }
+
+    applyColumnDefaults(col: GridColumn) {
+
+        if (!col.idlFieldDef && col.path) {
+            const idlInfo = this.idlInfoFromDotpath(col.path);
+            if (idlInfo) {
+                col.idlFieldDef = idlInfo.idlField;
+                if (!col.label) {
+                    col.label = col.idlFieldDef.label || col.idlFieldDef.name;
+                    col.datatype = col.idlFieldDef.datatype;
+                }
+            }
+        }
+
+        if (!col.name) { col.name = col.path; }
+        if (!col.flex) { col.flex = 2; }
+        if (!col.align) { col.align = 'left'; }
+        if (!col.label) { col.label = col.name; }
+        if (!col.datatype) { col.datatype = 'text'; }
+
+        col.visible = !col.hidden;
+    }
+
+    applyColumnSortability(col: GridColumn) {
+        // column sortability defaults to the sortability of the column set.
+        if (col.isSortable === undefined && this.isSortable) {
+            col.isSortable = true;
+        }
+
+        if (col.isMultiSortable === undefined && this.isMultiSortable) {
+            col.isMultiSortable = true;
+        }
+
+        if (col.isMultiSortable) {
+            col.isSortable = true;
+        }
+    }
+
+    displayColumns(): GridColumn[] {
+        return this.columns.filter(c => c.visible);
+    }
+
+    insertBefore(source: GridColumn, target: GridColumn) {
+        let targetIdx = -1;
+        let sourceIdx = -1;
+        this.columns.forEach((col, idx) => {
+            if (col.name === target.name) { targetIdx = idx; }});
+
+        this.columns.forEach((col, idx) => {
+            if (col.name === source.name) { sourceIdx = idx; }});
+
+        if (sourceIdx >= 0) {
+            this.columns.splice(sourceIdx, 1);
+        }
+
+        this.columns.splice(targetIdx, 0, source);
+    }
+
+    // Move visible columns to the front of the list.
+    moveVisibleToFront() {
+        const newCols = this.displayColumns();
+        this.columns.forEach(col => {
+            if (!col.visible) { newCols.push(col); }});
+        this.columns = newCols;
+    }
+
+    moveColumn(col: GridColumn, diff: number) {
+        let srcIdx, targetIdx;
+
+        this.columns.forEach((c, i) => {
+          if (c.name === col.name) { srcIdx = i; }
+        });
+
+        targetIdx = srcIdx + diff;
+        if (targetIdx < 0) {
+            targetIdx = 0;
+        } else if (targetIdx >= this.columns.length) {
+            // Target index follows the last visible column.
+            let lastVisible = 0;
+            this.columns.forEach((c, idx) => {
+                if (c.visible) { lastVisible = idx; }
+            });
+
+            // When moving a column (down) causes one or more
+            // visible columns to shuffle forward, our column
+            // moves into the slot of the last visible column.
+            // Otherwise, put it into the slot directly following
+            // the last visible column.
+            targetIdx = srcIdx <= lastVisible ? lastVisible : lastVisible + 1;
+        }
+
+        // Splice column out of old position, insert at new position.
+        this.columns.splice(srcIdx, 1);
+        this.columns.splice(targetIdx, 0, col);
+    }
+
+    compileSaveObject(): GridColumnPersistConf[] {
+        // only store information about visible columns.
+        // scrunch the data down to just the needed info.
+        return this.displayColumns().map(col => {
+            const c: GridColumnPersistConf = {name : col.name};
+            if (col.align !== 'left') { c.align = col.align; }
+            if (col.flex !== 2) { c.flex = Number(col.flex); }
+            if (Number(col.sort)) { c.sort = Number(c.sort); }
+            return c;
+        });
+    }
+
+    applyColumnSettings(conf: GridColumnPersistConf[]) {
+
+        if (!conf || conf.length === 0) {
+            // No configuration is available, but we have a list of
+            // fields to show or hide by default
+
+            if (this.defaultVisibleFields) {
+                this.columns.forEach(col => {
+                    if (this.defaultVisibleFields.includes(col.name)) {
+                        col.visible = true;
+                    } else {
+                        col.visible = false;
+                    }
+                });
+
+            } else if (this.defaultHiddenFields) {
+                this.defaultHiddenFields.forEach(name => {
+                    const col = this.getColByName(name);
+                    if (col) {
+                        col.visible = false;
+                    }
+                });
+            }
+
+            return;
+        }
+
+        const newCols = [];
+
+        conf.forEach(colConf => {
+            const col = this.getColByName(colConf.name);
+            if (!col) { return; } // no such column in this grid.
+
+            col.visible = true;
+            if (colConf.align) { col.align = colConf.align; }
+            if (colConf.flex)  { col.flex = Number(colConf.flex); }
+            if (colConf.sort)  { col.sort = Number(colConf.sort); }
+
+            // Add to new columns array, avoid dupes.
+            if (newCols.filter(c => c.name === col.name).length === 0) {
+                newCols.push(col);
+            }
+        });
+
+        // columns which are not expressed within the saved
+        // configuration are marked as non-visible and
+        // appended to the end of the new list of columns.
+        this.columns.forEach(c => {
+            if (conf.filter(cf => cf.name === c.name).length === 0) {
+                c.visible = false;
+                newCols.push(c);
+            }
+        });
+
+        this.columns = newCols;
+    }
+}
+
+
+export class GridRowSelector {
+    indexes: {[string: string]: boolean};
+
+    constructor() {
+        this.clear();
+    }
+
+    // Returns true if all of the requested indexes exist in the selector.
+    contains(index: string | string[]): boolean {
+        const indexes = [].concat(index);
+        for (let i = 0; i < indexes.length; i++) { // early exit
+            if (!this.indexes[indexes[i]]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    select(index: string | string[]) {
+        const indexes = [].concat(index);
+        indexes.forEach(i => this.indexes[i] = true);
+    }
+
+    deselect(index: string | string[]) {
+        const indexes = [].concat(index);
+        indexes.forEach(i => delete this.indexes[i]);
+    }
+
+    // Returns the list of selected index values.
+    // in some contexts (template checkboxes) the value for an index is
+    // set to false to deselect instead of having it removed (via deselect()).
+    selected() {
+        return Object.keys(this.indexes).filter(
+            ind => Boolean(this.indexes[ind]));
+    }
+
+    isEmpty(): boolean {
+        return this.selected().length === 0;
+    }
+
+    clear() {
+        this.indexes = {};
+    }
+}
+
+export interface GridRowFlairEntry {
+    icon: string;   // name of material icon
+    title: string;  // tooltip string
+}
+
+export class GridColumnPersistConf {
+    name: string;
+    flex?: number;
+    sort?: number;
+    align?: string;
+}
+
+export class GridPersistConf {
+    version: number;
+    limit: number;
+    columns: GridColumnPersistConf[];
+}
+
+export class GridContext {
+
+    pager: Pager;
+    idlClass: string;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    useLocalSort: boolean;
+    persistKey: string;
+    disableMultiSelect: boolean;
+    dataSource: GridDataSource;
+    columnSet: GridColumnSet;
+    rowSelector: GridRowSelector;
+    toolbarButtons: GridToolbarButton[];
+    toolbarCheckboxes: GridToolbarCheckbox[];
+    toolbarActions: GridToolbarAction[];
+    lastSelectedIndex: any;
+    pageChanges: Subscription;
+    rowFlairIsEnabled: boolean;
+    rowFlairCallback: (row: any) => GridRowFlairEntry;
+    rowClassCallback: (row: any) => string;
+    cellClassCallback: (row: any, col: GridColumn) => string;
+    defaultVisibleFields: string[];
+    defaultHiddenFields: string[];
+    overflowCells: boolean;
+
+    // Services injected by our grid component
+    idl: IdlService;
+    org: OrgService;
+    store: ServerStoreService;
+    format: FormatService;
+
+    constructor(
+        idl: IdlService,
+        org: OrgService,
+        store: ServerStoreService,
+        format: FormatService) {
+
+        this.idl = idl;
+        this.org = org;
+        this.store = store;
+        this.format = format;
+        this.pager = new Pager();
+        this.pager.limit = 10;
+        this.rowSelector = new GridRowSelector();
+        this.toolbarButtons = [];
+        this.toolbarCheckboxes = [];
+        this.toolbarActions = [];
+    }
+
+    init() {
+        this.columnSet = new GridColumnSet(this.idl, this.idlClass);
+        this.columnSet.isSortable = this.isSortable === true;
+        this.columnSet.isMultiSortable = this.isMultiSortable === true;
+        this.columnSet.defaultHiddenFields = this.defaultHiddenFields;
+        this.columnSet.defaultVisibleFields = this.defaultVisibleFields;
+        this.generateColumns();
+    }
+
+    // Load initial settings and data.
+    initData() {
+        this.applyGridConfig()
+        .then(ok => this.dataSource.requestPage(this.pager))
+        .then(ok => this.listenToPager());
+    }
+
+    destroy() {
+        this.ignorePager();
+    }
+
+    applyGridConfig(): Promise<void> {
+        return this.getGridConfig(this.persistKey)
+        .then(conf => {
+            let columns = [];
+            if (conf) {
+                columns = conf.columns;
+                if (conf.limit) {
+                    this.pager.limit = conf.limit;
+                }
+            }
+
+            // This is called regardless of the presence of saved
+            // settings so defaults can be applied.
+            this.columnSet.applyColumnSettings(columns);
+        });
+    }
+
+    reload() {
+        // Give the UI time to settle before reloading grid data.
+        // This can help when data retrieval depends on a value
+        // getting modified by an angular digest cycle.
+        setTimeout(() => {
+            this.pager.reset();
+            this.dataSource.reset();
+            this.dataSource.requestPage(this.pager);
+        });
+    }
+
+    // Sort the existing data source instead of requesting sorted
+    // data from the client.  Reset pager to page 1.  As with reload(),
+    // give the client a chance to setting before redisplaying.
+    sortLocal() {
+        setTimeout(() => {
+            this.pager.reset();
+            this.sortLocalData();
+            this.dataSource.requestPage(this.pager);
+        });
+    }
+
+    // Subscribe or unsubscribe to page-change events from the pager.
+    listenToPager() {
+        if (this.pageChanges) { return; }
+        this.pageChanges = this.pager.onChange$.subscribe(
+            val => this.dataSource.requestPage(this.pager));
+    }
+
+    ignorePager() {
+        if (!this.pageChanges) { return; }
+        this.pageChanges.unsubscribe();
+        this.pageChanges = null;
+    }
+
+    // Sort data in the data source array
+    sortLocalData() {
+
+        const sortDefs = this.dataSource.sort.map(sort => {
+            const def = {
+                name: sort.name,
+                dir: sort.dir,
+                col: this.columnSet.getColByName(sort.name)
+            };
+
+            if (!def.col.comparator) {
+                def.col.comparator = (a, b) => {
+                    if (a < b) { return -1; }
+                    if (a > b) { return 1; }
+                    return 0;
+                };
+            }
+
+            return def;
+        });
+
+        this.dataSource.data.sort((rowA, rowB) => {
+
+            for (let idx = 0; idx < sortDefs.length; idx++) {
+                const sortDef = sortDefs[idx];
+
+                const valueA = this.getRowColumnValue(rowA, sortDef.col);
+                const valueB = this.getRowColumnValue(rowB, sortDef.col);
+
+                if (valueA === '' && valueB === '') { continue; }
+                if (valueA === '' && valueB !== '') { return 1; }
+                if (valueA !== '' && valueB === '') { return -1; }
+
+                const diff = sortDef.col.comparator(valueA, valueB);
+                if (diff === 0) { continue; }
+
+                console.log(valueA, valueB, diff);
+
+                return sortDef.dir === 'DESC' ? -diff : diff;
+            }
+
+            return 0; // No differences found.
+        });
+    }
+
+    getRowIndex(row: any): any {
+        const col = this.columnSet.indexColumn;
+        if (!col) {
+            throw new Error('grid index column required');
+        }
+        return this.getRowColumnValue(row, col);
+    }
+
+    // Returns position in the data source array of the row with
+    // the provided index.
+    getRowPosition(index: any): number {
+        // for-loop for early exit
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) {
+            const row = this.dataSource.data[idx];
+            if (row !== undefined && index === this.getRowIndex(row)) {
+                return idx;
+            }
+        }
+    }
+
+    // Return the row with the provided index.
+    getRowByIndex(index: any): any {
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) {
+            const row = this.dataSource.data[idx];
+            if (row !== undefined && index === this.getRowIndex(row)) {
+                return row;
+            }
+        }
+    }
+
+    // Returns all selected rows, regardless of whether they are
+    // currently visible in the grid display.
+    getSelectedRows(): any[] {
+        const selected = [];
+        this.rowSelector.selected().forEach(index => {
+            const row = this.getRowByIndex(index);
+            if (row) {
+                selected.push(row);
+            }
+        });
+        return selected;
+    }
+
+    getRowColumnValue(row: any, col: GridColumn): string {
+        let val;
+        if (col.name in row) {
+            val = this.getObjectFieldValue(row, col.name);
+        } else {
+            if (col.path) {
+                val = this.nestedItemFieldValue(row, col);
+            }
+        }
+        return this.format.transform({value: val, datatype: col.datatype});
+    }
+
+    getObjectFieldValue(obj: any, name: string): any {
+        if (typeof obj[name] === 'function') {
+            return obj[name]();
+        } else {
+            return obj[name];
+        }
+    }
+
+    nestedItemFieldValue(obj: any, col: GridColumn): string {
+
+        let idlField;
+        let idlClassDef;
+        const original = obj;
+        const steps = col.path.split('.');
+
+        for (let i = 0; i < steps.length; i++) {
+            const step = steps[i];
+
+            if (typeof obj !== 'object') {
+                // We have run out of data to step through before
+                // reaching the end of the path.  Conclude fleshing via
+                // callback if provided then exit.
+                if (col.flesher && obj !== undefined) {
+                    return col.flesher(obj, col, original);
+                }
+                return obj;
+            }
+
+            const class_ = obj.classname;
+            if (class_ && (idlClassDef = this.idl.classes[class_])) {
+                idlField = idlClassDef.field_map[step];
+            }
+
+            obj = this.getObjectFieldValue(obj, step);
+        }
+
+        // We found a nested IDL object which may or may not have
+        // been configured as a top-level column.  Flesh the column
+        // metadata with our newly found IDL info.
+        if (idlField) {
+            if (!col.datatype) {
+                col.datatype = idlField.datatype;
+            }
+            if (!col.label) {
+                col.label = idlField.label || idlField.name;
+            }
+        }
+
+        return obj;
+    }
+
+
+    getColumnTextContent(row: any, col: GridColumn): string {
+        if (col.cellTemplate) {
+            // TODO
+            // Extract the text content from the rendered template.
+        } else {
+            return this.getRowColumnValue(row, col);
+        }
+    }
+
+    selectOneRow(index: any) {
+        this.rowSelector.clear();
+        this.rowSelector.select(index);
+        this.lastSelectedIndex = index;
+    }
+
+    // selects or deselects an item, without affecting the others.
+    // returns true if the item is selected; false if de-selected.
+    toggleSelectOneRow(index: any) {
+        if (this.rowSelector.contains(index)) {
+            this.rowSelector.deselect(index);
+            return false;
+        }
+
+        this.rowSelector.select(index);
+        return true;
+    }
+
+    selectRowByPos(pos: number) {
+        const row = this.dataSource.data[pos];
+        if (row) {
+            this.selectOneRow(this.getRowIndex(row));
+        }
+    }
+
+    selectPreviousRow() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos === this.pager.offset) {
+            this.toPrevPage().then(ok => this.selectLastRow(), err => {});
+        } else {
+            this.selectRowByPos(pos - 1);
+        }
+    }
+
+    selectNextRow() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos === (this.pager.offset + this.pager.limit - 1)) {
+            this.toNextPage().then(ok => this.selectFirstRow(), err => {});
+        } else {
+            this.selectRowByPos(pos + 1);
+        }
+    }
+
+    selectFirstRow() {
+        this.selectRowByPos(this.pager.offset);
+    }
+
+    selectLastRow() {
+        this.selectRowByPos(this.pager.offset + this.pager.limit - 1);
+    }
+
+    toPrevPage(): Promise<any> {
+        if (this.pager.isFirstPage()) {
+            return Promise.reject('on first');
+        }
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.decrement();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+    toNextPage(): Promise<any> {
+        if (this.pager.isLastPage()) {
+            return Promise.reject('on last');
+        }
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.increment();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+    getAllRows(): Promise<any> {
+        const pager = new Pager();
+        pager.offset = 0;
+        pager.limit = MAX_ALL_ROW_COUNT;
+        return this.dataSource.requestPage(pager);
+    }
+
+    // Returns a key/value pair object of visible column data as text.
+    getRowAsFlatText(row: any): any {
+        const flatRow = {};
+        this.columnSet.displayColumns().forEach(col => {
+            flatRow[col.name] =
+                this.getColumnTextContent(row, col);
+        });
+        return flatRow;
+    }
+
+    getAllRowsAsText(): Observable<any> {
+        return Observable.create(observer => {
+            this.getAllRows().then(ok => {
+                this.dataSource.data.forEach(row => {
+                    observer.next(this.getRowAsFlatText(row));
+                });
+                observer.complete();
+            });
+        });
+    }
+
+    gridToCsv(): Promise<string> {
+
+        let csvStr = '';
+        const columns = this.columnSet.displayColumns();
+
+        // CSV header
+        columns.forEach(col => {
+            csvStr += this.valueToCsv(col.label),
+            csvStr += ',';
+        });
+
+        csvStr = csvStr.replace(/,$/, '\n');
+
+        return new Promise(resolve => {
+            this.getAllRowsAsText().subscribe(
+                row => {
+                    columns.forEach(col => {
+                        csvStr += this.valueToCsv(row[col.name]);
+                        csvStr += ',';
+                    });
+                    csvStr = csvStr.replace(/,$/, '\n');
+                },
+                err => {},
+                ()  => resolve(csvStr)
+            );
+        });
+    }
+
+
+    // prepares a string for inclusion within a CSV document
+    // by escaping commas and quotes and removing newlines.
+    valueToCsv(str: string): string {
+        str = '' + str;
+        if (!str) { return ''; }
+        str = str.replace(/\n/g, '');
+        if (str.match(/\,/) || str.match(/"/)) {
+            str = str.replace(/"/g, '""');
+            str = '"' + str + '"';
+        }
+        return str;
+    }
+
+    generateColumns() {
+        if (!this.columnSet.idlClass) { return; }
+
+        const pkeyField = this.idl.classes[this.columnSet.idlClass].pkey;
+
+        // generate columns for all non-virtual fields on the IDL class
+        this.idl.classes[this.columnSet.idlClass].fields
+        .filter(field => !field.virtual)
+        .forEach(field => {
+            const col = new GridColumn();
+            col.name = field.name;
+            col.label = field.label || field.name;
+            col.idlFieldDef = field;
+            col.datatype = field.datatype;
+            col.isIndex = (field.name === pkeyField);
+            col.isAuto = true;
+            this.columnSet.add(col);
+        });
+    }
+
+    saveGridConfig(): Promise<any> {
+        if (!this.persistKey) {
+            throw new Error('Grid persistKey required to save columns');
+        }
+        const conf = new GridPersistConf();
+        conf.version = 2;
+        conf.limit = this.pager.limit;
+        conf.columns = this.columnSet.compileSaveObject();
+
+        return this.store.setItem('eg.grid.' + this.persistKey, conf);
+    }
+
+    // TODO: saveGridConfigAsOrgSetting(...)
+
+    getGridConfig(persistKey: string): Promise<GridPersistConf> {
+        if (!persistKey) { return Promise.resolve(null); }
+        return this.store.getItem('eg.grid.' + persistKey);
+    }
+}
+
+
+// Actions apply to specific rows
+export class GridToolbarAction {
+    label: string;
+    action: (rows: any[]) => any;
+}
+
+// Buttons are global actions
+export class GridToolbarButton {
+    label: string;
+    action: () => any;
+    disabled: boolean;
+}
+
+export class GridToolbarCheckbox {
+    label: string;
+    onChange: (checked: boolean) => void;
+}
+
+export class GridDataSource {
+
+    data: any[];
+    sort: any[];
+    allRowsRetrieved: boolean;
+    getRows: (pager: Pager, sort: any[]) => Observable<any>;
+
+    constructor() {
+        this.sort = [];
+        this.reset();
+    }
+
+    reset() {
+        this.data = [];
+        this.allRowsRetrieved = false;
+    }
+
+    // called from the template -- no data fetching
+    getPageOfRows(pager: Pager): any[] {
+        if (this.data) {
+            return this.data.slice(
+                pager.offset, pager.limit + pager.offset
+            ).filter(row => row !== undefined);
+        }
+        return [];
+    }
+
+    // called on initial component load and user action (e.g. paging, sorting).
+    requestPage(pager: Pager): Promise<any> {
+
+        if (
+            this.getPageOfRows(pager).length === pager.limit
+            // already have all data
+            || this.allRowsRetrieved
+            // have no way to get more data.
+            || !this.getRows
+        ) {
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            let idx = pager.offset;
+            return this.getRows(pager, this.sort).subscribe(
+                row => this.data[idx++] = row,
+                err => {
+                    console.error(`grid getRows() error ${err}`);
+                    reject(err);
+                },
+                ()  => {
+                    this.checkAllRetrieved(pager, idx);
+                    resolve();
+                }
+            );
+        });
+    }
+
+    // See if the last getRows() call resulted in the final set of data.
+    checkAllRetrieved(pager: Pager, idx: number) {
+        if (this.allRowsRetrieved) { return; }
+
+        if (idx === 0 || idx < (pager.limit + pager.offset)) {
+            // last query returned nothing or less than one page.
+            // confirm we have all of the preceding pages.
+            if (!this.data.includes(undefined)) {
+                this.allRowsRetrieved = true;
+                pager.resultCount = this.data.length;
+            }
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
new file mode 100644 (file)
index 0000000..2a4bd3a
--- /dev/null
@@ -0,0 +1,17 @@
+
+<!-- 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"
+  (click)="click$.next($event.target.value)"
+  (selectItem)="orgChanged($event)"
+  #instance="ngbTypeahead"
+/>
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
new file mode 100644 (file)
index 0000000..39e0cff
--- /dev/null
@@ -0,0 +1,212 @@
+/** TODO PORT ME TO <eg-combobox> */
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {mapTo} from 'rxjs/operators/mapTo';
+import {debounceTime} from 'rxjs/operators/debounceTime';
+import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
+import {merge} from 'rxjs/operators/merge';
+import {filter} from 'rxjs/operators/filter';
+import {Subject} from 'rxjs/Subject';
+import {AuthService} from '@eg/core/auth.service';
+import {StoreService} from '@eg/core/store.service';
+import {OrgService} from '@eg/core/org.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {PermService} from '@eg/core/perm.service';
+import {NgbTypeahead, 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 = ' '; // U+2007
+
+interface OrgDisplay {
+  id: number;
+  label: string;
+  disabled: boolean;
+}
+
+@Component({
+  selector: 'eg-org-select',
+  templateUrl: './org-select.component.html'
+})
+export class OrgSelectComponent implements OnInit {
+
+    selected: OrgDisplay;
+    hidden: number[] = [];
+    disabled: number[] = [];
+    click$ = new Subject<string>();
+    startOrg: IdlObject;
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Placeholder text for selector input
+    @Input() placeholder = '';
+    @Input() stickySetting: string;
+
+    // Org unit field displayed in the selector
+    @Input() displayField = 'shortname';
+
+    // Apply a default org unit value when none is set.
+    // First tries workstation org unit, then user home org unit.
+    // An onChange event WILL be generated when a default is applied.
+    @Input() applyDefault = false;
+
+    // List of org unit IDs to exclude from the selector
+    @Input() set hideOrgs(ids: number[]) {
+        if (ids) { this.hidden = ids; }
+    }
+
+    // List of org unit IDs to disable in the selector
+    @Input() set disableOrgs(ids: number[]) {
+        if (ids) { this.disabled = ids; }
+    }
+
+    // Apply an org unit value at load time.
+    // This will NOT result in an onChange event.
+    @Input() set initialOrg(org: IdlObject) {
+        if (org) { this.startOrg = org; }
+    }
+
+    // Apply an org unit value by ID at load time.
+    // This will NOT result in an onChange event.
+    @Input() set initialOrgId(id: number) {
+        if (id) { this.startOrg = this.org.get(id); }
+    }
+
+    // Modify the selected org unit via data binding.
+    // This WILL result in an onChange event firing.
+    @Input() set applyOrg(org: IdlObject) {
+        if (org) {
+            this.selected = this.formatForDisplay(org);
+        }
+    }
+
+    permLimitOrgs: number[];
+    @Input() set limitPerms(perms: string[]) {
+        this.applyPermLimitOrgs(perms);
+    }
+
+    // Modify the selected org unit by ID via data binding.
+    // This WILL result in an onChange event firing.
+    @Input() set applyOrgId(id: number) {
+        if (id) {
+            this.selected = this.formatForDisplay(this.org.get(id));
+        }
+    }
+
+    // Emitted when the org unit value is changed via the selector.
+    // Does not fire on initialOrg
+    @Output() onChange = new EventEmitter<IdlObject>();
+
+    constructor(
+      private auth: AuthService,
+      private store: StoreService,
+      private org: OrgService,
+      private perm: PermService
+    ) { }
+
+    ngOnInit() {
+
+        // Apply a default org unit if desired and possible.
+        if (!this.startOrg && this.applyDefault && this.auth.user()) {
+            // note: ws_ou defaults to home_ou on the server
+            // when when no workstation is used
+            this.startOrg = this.org.get(this.auth.user().ws_ou());
+            this.selected = this.formatForDisplay(
+                this.org.get(this.auth.user().ws_ou())
+            );
+
+            // avoid notifying mid-digest
+            setTimeout(() => this.onChange.emit(this.startOrg), 0);
+        }
+
+        if (this.startOrg) {
+            this.selected = this.formatForDisplay(this.startOrg);
+        }
+    }
+
+    //
+    applyPermLimitOrgs(perms: string[]) {
+
+        if (!perms) {
+            return;
+        }
+
+        // handle lazy clients that pass null perm names
+        perms = perms.filter(p => p !== null && p !== undefined);
+
+        if (perms.length === 0) {
+            return;
+        }
+
+        // NOTE: If permLimitOrgs is useful in a non-staff context
+        // we need to change this to support non-staff perm checks.
+        this.perm.hasWorkPermAt(perms, true).then(permMap => {
+            this.permLimitOrgs =
+                // safari-friendly version of Array.flat()
+                Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
+        });
+    }
+
+    // Format for display in the selector drop-down and input.
+    formatForDisplay(org: IdlObject): OrgDisplay {
+        return {
+            id : org.id(),
+            label : PAD_SPACE.repeat(org.ou_type().depth())
+              + org[this.displayField](),
+            disabled : false
+        };
+    }
+
+    // Fired by the typeahead to inform us of a change.
+    // TODO: this does not fire when the value is cleared :( -- implement
+    // change detection on this.selected to look specifically for NULL.
+    orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        // console.debug('org unit change occurred ' + selEvent.item);
+        this.onChange.emit(this.org.get(selEvent.item.id));
+    }
+
+    // Remove the tree-padding spaces when matching.
+    formatter = (result: OrgDisplay) => result.label.trim();
+
+    filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
+        return text$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+            merge(
+                // Inject a specifier indicating the source of the
+                // action is a user click
+                this.click$.pipe(filter(() => !this.instance.isPopupOpen()))
+                .pipe(mapTo('_CLICK_'))
+            ),
+            map(term => {
+
+                let orgs = this.org.list().filter(org =>
+                    this.hidden.filter(id => org.id() === id).length === 0
+                );
+
+                if (this.permLimitOrgs) {
+                    // Avoid showing org units where the user does
+                    // not have the requested permission.
+                    orgs = orgs.filter(org =>
+                        this.permLimitOrgs.includes(org.id()));
+                }
+
+                if (term !== '_CLICK_') {
+                    // For search-driven events, limit to the matching
+                    // org units.
+                    orgs = orgs.filter(org => {
+                        return term === '' || // show all
+                            org[this.displayField]()
+                                .toLowerCase().indexOf(term.toLowerCase()) > -1;
+
+                    });
+                }
+
+                return orgs.map(org => this.formatForDisplay(org));
+            })
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.html b/Open-ILS/src/eg2/src/app/share/print/print.component.html
new file mode 100644 (file)
index 0000000..12d05bc
--- /dev/null
@@ -0,0 +1,16 @@
+<!-- 
+Global print container.  
+There should only be one print component active in a page.
+-->
+
+<div id='eg-print-container'>
+  <!-- container for inline template compilation -->
+  <ng-container *ngIf="template">
+    <ng-container *ngTemplateOutlet="template; context:context">
+    </ng-container>
+  </ng-container>
+  <div id='eg-print-html-container'>
+  </div>
+<!-- container for pre-compiled HTML -->
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
new file mode 100644 (file)
index 0000000..4f69949
--- /dev/null
@@ -0,0 +1,133 @@
+import {Component, OnInit, TemplateRef, ElementRef, Renderer2} from '@angular/core';
+import {PrintService, PrintRequest} from './print.service';
+import {StoreService} from '@eg/core/store.service';
+
+@Component({
+    selector: 'eg-print',
+    templateUrl: './print.component.html'
+})
+
+export class PrintComponent implements OnInit {
+
+    // Template that requires local processing
+    template: TemplateRef<any>;
+
+    // Context data used for processing the template.
+    context: any;
+
+    // Insertion point for externally-compiled templates
+    htmlContainer: Element;
+
+    isPrinting: boolean;
+
+    printQueue: PrintRequest[];
+
+    constructor(
+        private renderer: Renderer2,
+        private elm: ElementRef,
+        private store: StoreService,
+        private printer: PrintService) {
+        this.isPrinting = false;
+        this.printQueue = [];
+    }
+
+    ngOnInit() {
+        this.printer.onPrintRequest$.subscribe(
+            printReq => this.handlePrintRequest(printReq));
+
+        this.htmlContainer =
+            this.renderer.selectRootElement('#eg-print-html-container');
+    }
+
+    handlePrintRequest(printReq: PrintRequest) {
+
+        if (this.isPrinting) {
+            // Avoid print collisions by queuing requests as needed.
+            this.printQueue.push(printReq);
+            return;
+        }
+
+        this.isPrinting = true;
+
+        this.applyTemplate(printReq);
+
+        // Give templates a chance to render before printing
+        setTimeout(() => {
+            this.dispatchPrint(printReq);
+            this.reset();
+        });
+    }
+
+    applyTemplate(printReq: PrintRequest) {
+
+        if (printReq.template) {
+            // Inline template.  Let Angular do the interpolationwork.
+            this.template = printReq.template;
+            this.context = {$implicit: printReq.contextData};
+            return;
+        }
+
+        if (printReq.text && true /* !this.hatch.isActive */) {
+            // Insert HTML into the browser DOM for in-browser printing only.
+
+            if (printReq.contentType === 'text/plain') {
+                // Wrap text/plain content in pre's to prevent
+                // unintended html formatting.
+                printReq.text = `<pre>${printReq.text}</pre>`;
+            }
+
+            this.htmlContainer.innerHTML = printReq.text;
+        }
+    }
+
+    // Clear the print data
+    reset() {
+        this.isPrinting = false;
+        this.template = null;
+        this.context = null;
+        this.htmlContainer.innerHTML = '';
+
+        if (this.printQueue.length) {
+            this.handlePrintRequest(this.printQueue.pop());
+        }
+    }
+
+    dispatchPrint(printReq: PrintRequest) {
+
+        if (!printReq.text) {
+            // Sometimes the results come from an externally-parsed HTML
+            // template, other times they come from an in-page template.
+            printReq.text = this.elm.nativeElement.innerHTML;
+        }
+
+        // Retain a copy of each printed document in localStorage
+        // so it may be reprinted.
+        this.store.setLocalItem('eg.print.last_printed', {
+            content: printReq.text,
+            context: printReq.printContext,
+            content_type: printReq.contentType,
+            show_dialog: printReq.showDialog
+        });
+
+        if (0 /* this.hatch.isActive */) {
+            this.printViaHatch(printReq);
+        } else {
+            // Here the needed HTML is already in the page.
+            window.print();
+        }
+    }
+
+    printViaHatch(printReq: PrintRequest) {
+
+        // Send a full HTML document to Hatch
+        const html = `<html><body>${printReq.text}</body></html>`;
+
+        /*
+        this.hatch.print({
+            printContext: printReq.printContext,
+            content: html
+        });
+        */
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.service.ts b/Open-ILS/src/eg2/src/app/share/print/print.service.ts
new file mode 100644 (file)
index 0000000..5ae6844
--- /dev/null
@@ -0,0 +1,41 @@
+import {Injectable, EventEmitter, TemplateRef} from '@angular/core';
+import {StoreService} from '@eg/core/store.service';
+
+export interface PrintRequest {
+    template?: TemplateRef<any>;
+    contextData?: any;
+    text?: string;
+    printContext: string;
+    contentType?: string; // defaults to text/html
+    showDialog?: boolean;
+}
+
+@Injectable()
+export class PrintService {
+
+    onPrintRequest$: EventEmitter<PrintRequest>;
+
+    constructor(private store: StoreService) {
+        this.onPrintRequest$ = new EventEmitter<PrintRequest>();
+    }
+
+    print(printReq: PrintRequest) {
+        this.onPrintRequest$.emit(printReq);
+    }
+
+    reprintLast() {
+        const prev = this.store.getLocalItem('eg.print.last_printed');
+
+        if (prev) {
+            const req: PrintRequest = {
+                text: prev.content,
+                printContext: prev.context || 'default',
+                contentType: prev.content_type || 'text/html',
+                showDialog: Boolean(prev.show_dialog)
+            };
+
+            this.print(req);
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.component.ts b/Open-ILS/src/eg2/src/app/share/string/string.component.ts
new file mode 100644 (file)
index 0000000..f092a7e
--- /dev/null
@@ -0,0 +1,74 @@
+/*j
+ * <eg-string #helloStr text="Hello, {{name}}" i18n-text></eg-string>
+ *
+ * import {StringComponent} from '@eg/share/string.component';
+ * @ViewChild('helloStr') private helloStr: StringComponent;
+ * ...
+ * this.helloStr.currrent().then(s => console.log(s));
+ *
+ */
+import {Component, Input, OnInit, ElementRef, TemplateRef} from '@angular/core';
+import {StringService} from '@eg/share/string/string.service';
+
+@Component({
+  selector: 'eg-string',
+  template: `
+    <span style='display:none'>
+    <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+    </span>
+  `
+})
+
+export class StringComponent implements OnInit {
+
+    // Storage key for future reference by the string service
+    @Input() key: string;
+
+    // Interpolation context
+    @Input() ctx: any;
+
+    // String template to interpolate
+    @Input() template: TemplateRef<any>;
+
+    // Static text -- no interpolation performed.
+    // This supersedes 'template'
+    @Input() text: string;
+
+    constructor(private elm: ElementRef, private strings: StringService) {
+        this.elm = elm;
+        this.strings = strings;
+    }
+
+    ngOnInit() {
+        // No key means it's an unregistered (likely static) string
+        // that does not need interpolation.
+        if (this.key) {
+            this.strings.register({
+                key: this.key,
+                resolver: (ctx: any) => {
+                    if (this.text) {
+                        // When passed text that does not require any
+                        // interpolation, just return it as-is.
+                        return Promise.resolve(this.text);
+                    } else {
+                        // Interpolate
+                        return this.current(ctx);
+                    }
+                }
+            });
+        }
+    }
+
+    // Apply the new context if provided, give our container a
+    // chance to update, then resolve with the current string.
+    // NOTE: talking to the native DOM element is not so great, but
+    // hopefully we can retire the String* code entirely once
+    // in-code translations are supported (Ang6?)
+    current(ctx?: any): Promise<string> {
+        if (ctx) { this.ctx = ctx; }
+        return new Promise(resolve => {
+            setTimeout(() => resolve(this.elm.nativeElement.textContent));
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.service.ts b/Open-ILS/src/eg2/src/app/share/string/string.service.ts
new file mode 100644 (file)
index 0000000..88d0c8a
--- /dev/null
@@ -0,0 +1,78 @@
+import {Injectable} from '@angular/core';
+
+interface StringAssignment {
+    key: string;     // keyboard command
+    resolver: (ctx: any) => Promise<string>;
+}
+
+interface PendingInterpolation {
+    key: string;
+    ctx: any;
+    resolve: (string) => any;
+    reject: (string) => any;
+}
+
+@Injectable()
+export class StringService {
+
+    strings: {[key: string]: StringAssignment} = {};
+
+    // This service can only interpolate one string at a time, since it
+    // maintains only one string component instance.  Avoid clobbering
+    // in-process interpolation requests by maintaining a request queue.
+    private pending: PendingInterpolation[];
+
+    constructor() {
+        this.pending = [];
+    }
+
+    register(assn: StringAssignment) {
+        this.strings[assn.key] = assn;
+    }
+
+    interpolate(key: string, ctx?: any): Promise<string> {
+
+        if (!this.strings[key]) {
+            return Promise.reject(`String key not found: "${key}"`);
+        }
+
+        return new Promise( (resolve, reject) => {
+            const pend: PendingInterpolation = {
+                key: key,
+                ctx: ctx,
+                resolve: resolve,
+                reject: reject
+            };
+
+            this.pending.push(pend);
+
+            // Avoid launching the pending string processer with >1
+            // pending, because the processor will have already started.
+            if (this.pending.length === 1) {
+                this.processPending();
+            }
+        });
+    }
+
+    processPending() {
+        const pstring = this.pending[0];
+        this.strings[pstring.key].resolver(pstring.ctx).then(
+            txt => {
+                pstring.resolve(txt);
+                this.pending.shift();
+                if (this.pending.length) {
+                    this.processPending();
+                }
+            },
+            err => {
+                pstring.reject(err);
+                this.pending.shift();
+                if (this.pending.length) {
+                    this.processPending();
+                }
+            }
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.css b/Open-ILS/src/eg2/src/app/share/toast/toast.component.css
new file mode 100644 (file)
index 0000000..1f70349
--- /dev/null
@@ -0,0 +1,11 @@
+#eg-toast-container {
+    min-width: 250px;
+    text-align: center;
+    border-radius: 2px;
+    padding: 10px;
+    position: fixed;
+    z-index: 1;
+    right: 15px;
+    bottom: 5px;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.html b/Open-ILS/src/eg2/src/app/share/toast/toast.component.html
new file mode 100644 (file)
index 0000000..6aa1545
--- /dev/null
@@ -0,0 +1,3 @@
+<div id="eg-toast-container" *ngIf="message">
+  <ngb-alert [type]="message.style" (close)="dismiss(message)">{{message.text}}</ngb-alert>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.ts b/Open-ILS/src/eg2/src/app/share/toast/toast.component.ts
new file mode 100644 (file)
index 0000000..9503ffd
--- /dev/null
@@ -0,0 +1,43 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {ToastService, ToastMessage} from '@eg/share/toast/toast.service';
+
+const EG_TOAST_TIMEOUT = 3000;
+
+@Component({
+  selector: 'eg-toast',
+  templateUrl: './toast.component.html',
+  styleUrls: ['./toast.component.css']
+})
+export class ToastComponent implements OnInit {
+
+    message: ToastMessage;
+
+    // track the most recent timeout event
+    timeout: any;
+
+    constructor(private toast: ToastService) {
+    }
+
+    ngOnInit() {
+        this.toast.messages$.subscribe(msg => this.show(msg));
+    }
+
+    show(msg: ToastMessage) {
+        this.dismiss(this.message);
+        this.message = msg;
+        this.timeout = setTimeout(
+            () => this.dismiss(this.message),
+            EG_TOAST_TIMEOUT
+        );
+    }
+
+    dismiss(msg: ToastMessage) {
+        this.message = null;
+        if (this.timeout) {
+            clearTimeout(this.timeout);
+            this.timeout = null;
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.service.ts b/Open-ILS/src/eg2/src/app/share/toast/toast.service.ts
new file mode 100644 (file)
index 0000000..5806592
--- /dev/null
@@ -0,0 +1,39 @@
+import {Injectable, EventEmitter} from '@angular/core';
+
+export interface ToastMessage {
+    text: string;
+    style: string;
+}
+
+@Injectable()
+export class ToastService {
+
+    messages$: EventEmitter<ToastMessage>;
+
+    constructor() {
+        this.messages$ = new EventEmitter<ToastMessage>();
+    }
+
+    sendMessage(msg: ToastMessage) {
+        this.messages$.emit(msg);
+    }
+
+    success(text: string) {
+        this.sendMessage({text: text, style: 'success'});
+    }
+
+    info(text: string) {
+        this.sendMessage({text: text, style: 'info'});
+    }
+
+    warning(text: string) {
+        this.sendMessage({text: text, style: 'warning'});
+    }
+
+    danger(text: string) {
+        this.sendMessage({text: text, style: 'danger'});
+    }
+
+    // Others?
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.css b/Open-ILS/src/eg2/src/app/share/tree/tree.component.css
new file mode 100644 (file)
index 0000000..0d29dd7
--- /dev/null
@@ -0,0 +1,19 @@
+
+.eg-tree-node-expandy .material-icons {
+  font-size: 16px;
+}
+
+.eg-tree-node {
+  padding: 2px;
+}
+
+.eg-tree-node.active {
+  background-color: rgba(0,0,0,.03);
+  border: 1px solid rgba(0,0,0,.125);
+  font-style: italic;
+}
+
+.eg-tree-node-nochild {
+  border-left: 2px dashed rgba(0,0,0,.125);
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.html b/Open-ILS/src/eg2/src/app/share/tree/tree.component.html
new file mode 100644 (file)
index 0000000..525fece
--- /dev/null
@@ -0,0 +1,20 @@
+
+
+<div class="eg-tree" *ngFor="let node of displayNodes()">
+  <div class="eg-tree-node-wrapper d-flex"
+    [ngStyle]="{'padding-left': (node.depth * 20) + 'px'}">
+    <div class="eg-tree-node-expandy">
+      <div *ngIf="node.children.length" (click)="node.toggleExpand()"
+        i18n-title title="Toggle Expand Node">
+        <span *ngIf="!node.expanded" class="material-icons">expand_more</span>
+        <span *ngIf="node.expanded" class="material-icons">expand_less</span>
+      </div>
+      <div *ngIf="!node.children.length" class="eg-tree-node-nochild">
+       &nbsp; 
+      </div>
+    </div>
+    <div class="eg-tree-node" [ngClass]="{active : node.selected}">
+      <a [routerLink]="" (click)="handleNodeClick(node)">{{node.label}}</a>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
new file mode 100644 (file)
index 0000000..d3fccda
--- /dev/null
@@ -0,0 +1,60 @@
+import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
+import {Tree, TreeNode} from './tree';
+
+/*
+Tree Widget:
+
+<eg-tree
+    [tree]="myTree"
+    (nodeClicked)="nodeClicked($event)">
+</eg-tree>
+
+----
+
+constructor() {
+
+    const rootNode = new TreeNode({
+        id: 1,
+        label: 'Root',
+        children: [
+            new TreeNode({id: 2, label: 'Child'}),
+            new TreeNode({id: 3, label: 'Child2'})
+        ]
+    ]});
+
+    this.myTree = new Tree(rootNode);
+}
+
+nodeClicked(node: TreeNode) {
+    console.log('someone clicked on ' + node.label);
+}
+*/
+
+@Component({
+    selector: 'eg-tree',
+    templateUrl: 'tree.component.html',
+    styleUrls: ['tree.component.css']
+})
+export class TreeComponent implements OnInit {
+
+    @Input() tree: Tree;
+    @Output() nodeClicked: EventEmitter<TreeNode>;
+
+    constructor() {
+        this.nodeClicked = new EventEmitter<TreeNode>();
+    }
+
+    ngOnInit() {}
+
+    displayNodes(): TreeNode[] {
+        return this.tree.nodeList(true);
+    }
+
+    handleNodeClick(node: TreeNode) {
+        this.tree.selectNode(node);
+        this.nodeClicked.emit(node);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts
new file mode 100644 (file)
index 0000000..3894fcd
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {TreeComponent} from './tree.component';
+
+@NgModule({
+    declarations: [
+        TreeComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        TreeComponent
+    ],
+    providers: [
+    ]
+})
+
+export class TreeModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.ts
new file mode 100644 (file)
index 0000000..cca36d4
--- /dev/null
@@ -0,0 +1,133 @@
+
+export class TreeNode {
+    // Unique identifier
+    id: any;
+
+    // Display label
+    label: string;
+
+    // True if child nodes should be visible
+    expanded: boolean;
+
+    children: TreeNode[];
+
+    // Set by the tree.
+    depth: number;
+
+    // Set by the tree.
+    selected: boolean;
+
+    // Optional link to user-provided stuff.
+    // This field is ignored by the tree.
+    callerData: any;
+
+    constructor(values: {[key: string]: any}) {
+        this.children = [];
+        this.expanded = true;
+        this.depth = 0;
+        this.selected = false;
+
+        if (!values) { return; }
+
+        if ('id' in values) { this.id = values.id; }
+        if ('label' in values) { this.label = values.label; }
+        if ('children' in values) { this.children = values.children; }
+        if ('expanded' in values) { this.expanded = values.expanded; }
+        if ('callerData' in values) { this.callerData = values.callerData; }
+    }
+
+    toggleExpand() {
+        this.expanded = !this.expanded;
+    }
+}
+
+export class Tree {
+
+    rootNode: TreeNode;
+    idMap: {[id: string]: TreeNode};
+
+    constructor(rootNode?: TreeNode) {
+        this.rootNode = rootNode;
+        this.idMap = {};
+    }
+
+    // Returns a depth-first list of tree nodes
+    // Tweaks node attributes along the way to match the shape of the tree.
+    nodeList(filterHidden?: boolean): TreeNode[] {
+
+        const nodes = [];
+
+        const recurseTree =
+            (node: TreeNode, depth: number, hidden: boolean) => {
+            if (!node) { return; }
+
+            node.depth = depth++;
+            this.idMap[node.id + ''] = node;
+
+            if (hidden) {
+                // it could be confusing for a hidden node to be selected.
+                node.selected = false;
+            }
+
+            if (hidden && filterHidden) {
+                // Avoid adding hidden child nodes to the list.
+            } else {
+                nodes.push(node);
+            }
+
+            node.children.forEach(n => recurseTree(n, depth, !node.expanded));
+        };
+
+        recurseTree(this.rootNode, 0, false);
+        return nodes;
+    }
+
+    findNode(id: any): TreeNode {
+        if (this.idMap[id + '']) {
+            return this.idMap[id + ''];
+        } else {
+            // nodeList re-indexes all the nodes.
+            this.nodeList();
+            return this.idMap[id + ''];
+        }
+    }
+
+    findParentNode(node: TreeNode) {
+        const list = this.nodeList();
+        for (let idx = 0; idx < list.length; idx++) {
+            const pnode = list[idx];
+            if (pnode.children.filter(c => c.id === node.id).length) {
+                return pnode;
+            }
+        }
+        return null;
+    }
+
+    removeNode(node: TreeNode) {
+        if (!node) { return; }
+        const pnode = this.findParentNode(node);
+        if (pnode) {
+            pnode.children = pnode.children.filter(n => n.id !== node.id);
+        } else {
+            this.rootNode = null;
+        }
+    }
+
+    expandAll() {
+        this.nodeList().forEach(node => node.expanded = true);
+    }
+
+    collapseAll() {
+        this.nodeList().forEach(node => node.expanded = false);
+    }
+
+    selectedNode(): TreeNode {
+        return this.nodeList().filter(node => node.selected)[0];
+    }
+
+    selectNode(node: TreeNode) {
+        this.nodeList().forEach(n => n.selected = false);
+        node.selected = true;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/util/audio.service.ts b/Open-ILS/src/eg2/src/app/share/util/audio.service.ts
new file mode 100644 (file)
index 0000000..3f3320a
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Plays audio files (alerts, generally) by key name.  Each sound uses a
+ * dot-path to indicate  the sound.
+ *
+ * For example:
+ *
+ * this.audio.play('warning.checkout.no_item');
+ *
+ * URLs are tested in the following order until an audio file is found
+ * or no other paths are left to check.
+ *
+ * /audio/notifications/warning/checkout/not_found.wav
+ * /audio/notifications/warning/checkout.wav
+ * /audio/notifications/warning.wav
+ *
+ * Files are only played when sounds are configured to play via
+ * workstation settings.
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {ServerStoreService} from '@eg/core/server-store.service';
+const AUDIO_BASE_URL = '/audio/notifications/';
+
+@Injectable()
+export class AudioService {
+
+    // map of requested audio path to resolved path
+    private urlCache: {[path: string]: string} = {};
+
+    constructor(private store: ServerStoreService) {}
+
+    play(path: string): void {
+        if (path) {
+            this.playUrl(path, path);
+        }
+    }
+
+    playUrl(path: string, origPath: string): void {
+        // console.debug(`audio: playUrl(${path}, ${origPath})`);
+
+        this.store.getItem('eg.audio.disable').then(audioDisabled => {
+            if (audioDisabled) { return; }
+
+            const url = this.urlCache[path] ||
+                AUDIO_BASE_URL + path.replace(/\./g, '/') + '.wav';
+
+            const player = new Audio(url);
+
+            player.onloadeddata = () => {
+                this.urlCache[origPath] = url;
+                player.play();
+                console.debug(`audio: ${url}`);
+            };
+
+            if (this.urlCache[path]) {
+                // when serving from the cache, avoid secondary URL lookups.
+                return;
+            }
+
+            player.onerror = () => {
+                // Unable to play path at the requested URL.
+
+                if (!path.match(/\./)) {
+                    // all fall-through options have been exhausted.
+                    // No path to play.
+                    console.warn(
+                        `No suitable URL found for path "${origPath}"`);
+                    return;
+                }
+
+                // Fall through to the next (more generic) option
+                path = path.replace(/\.[^\.]+$/, '');
+                this.playUrl(path, origPath);
+            };
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/util/pager.ts b/Open-ILS/src/eg2/src/app/share/util/pager.ts
new file mode 100644 (file)
index 0000000..267d4fc
--- /dev/null
@@ -0,0 +1,111 @@
+import {EventEmitter} from '@angular/core';
+
+/**
+ * Utility class for manage paged information.
+ */
+export class Pager {
+    offset = 0;
+    limit: number = null;
+    resultCount: number;
+    onChange$: EventEmitter<number>;
+
+    constructor() {
+        this.resultCount = null;
+        this.onChange$ = new EventEmitter<number>();
+    }
+
+    reset() {
+        this.resultCount = null;
+        this.offset = 0;
+    }
+
+    setLimit(l: number) {
+        if (l !== this.limit) {
+            this.limit = l;
+            this.setPage(1);
+        }
+    }
+
+    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);
+    }
+
+    toFirst() {
+        if (!this.isFirstPage()) {
+            this.setPage(1);
+        }
+    }
+
+    toLast() {
+        if (!this.isLastPage()) {
+            this.setPage(this.pageCount());
+        }
+    }
+
+    setPage(page: number): void {
+        this.offset = (this.limit * (page - 1));
+        this.onChange$.emit(this.offset);
+    }
+
+    pageCount(): number {
+        if (this.resultCount === null) { return -1; }
+        let pages = this.resultCount / this.limit;
+        if (Math.floor(pages) < pages) {
+            pages = Math.floor(pages) + 1;
+        }
+        return pages;
+    }
+
+    // Returns a list of pages numbers with @pivot at the center
+    // or as close to center as possible.
+    // @pivot is 1-based for consistency with page numbers.
+    // pageRange(25, 10) => [21,22,...29,30]
+    pageRange(pivot: number, size: number): number[] {
+
+        const diff = Math.floor(size / 2);
+        let start = pivot <= diff ? 1 : pivot - diff + 1;
+
+        const pcount = this.pageCount();
+
+        if (start + size > pcount) {
+            start = pcount - size + 1;
+            if (start < 1) { start = 1; }
+        }
+
+        if (start + size > pcount) {
+            size = pcount;
+        }
+
+        return this.pageList().slice(start - 1, start - 1 + size);
+    }
+
+    pageList(): number[] {
+        const list = [];
+        for (let i = 1; i <= this.pageCount(); i++) {
+            list.push(i);
+        }
+        return list;
+    }
+
+    // Given a zero-based page-specific offset, return the where in the
+    // entire data set the row lives, 1-based for UI friendliness.
+    rowNumber(offset: number): number {
+        return this.offset + offset + 1;
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/about.component.html b/Open-ILS/src/eg2/src/app/staff/about.component.html
new file mode 100644 (file)
index 0000000..9e8e1c4
--- /dev/null
@@ -0,0 +1,57 @@
+<eg-staff-banner bannerText="About Evergreen" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+  <div class="col-lg-4">
+    <div class="card">
+      <div class="card-header" i18n>Server Details</div>
+      <ul class="list-group list-group-flush">
+        <li class="list-group-item">
+          <div class="row pt-2">
+            <div class="col-lg-6" i18n>Evergreen Version</div>
+            <div class="col-lg-6">{{version}}</div>
+          </div>
+        </li>
+        <li class="list-group-item">
+          <div class="row pt-2">
+            <div class="col-lg-6" i18n>Hostname</div>
+            <div class="col-lg-6">{{server}}</div>
+          </div>
+        </li>
+      </ul>
+    </div><!-- card -->
+   </div>
+</div>
+<div class="row mt-4">
+  <div class="col-lg-8">
+    <h2 i18n>What is Evergreen?</h2>
+    <p i18n>Evergreen is library automation software that assists libraries
+       in day-to-day operations such as checking out materials, keeping
+       track of users, sharing resources among a group of libraries,
+       acquiring materials, and providing a web-based library catalog for
+       the public.
+    </p>
+    <p i18n>The open-source community developing and supporting Evergreen is
+       marked by a high degree of participation from developers and from
+       the librarians who use the software.
+    </p>
+    <p i18n>
+      More information can be found at 
+      <a href="https://evergreen-ils.org">https://evergreen-ils.org</a>. 
+      For help in using Evergreen, see our documentation at 
+      <a href="http://docs.evergreen-ils.org">http://docs.evergreen-ils.org</a>.
+    </p>
+    <p i18n>
+      Evergreen is Copyright &#0169; Georgia Public Library Service - 
+      A Unit of the University System of Georgia, and others. The 
+      Evergreen software is distributed under the 
+      <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html">
+      GNU General Public License, Version 2.
+      </a>
+    </p>
+  </div>
+</div>
+
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/about.component.ts b/Open-ILS/src/eg2/src/app/staff/about.component.ts
new file mode 100644 (file)
index 0000000..a494956
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, OnInit} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+
+@Component({
+    selector: 'eg-about',
+    templateUrl: 'about.component.html'
+})
+
+export class AboutComponent implements OnInit {
+    server: string;
+    version: string;
+
+    constructor(
+        private net: NetService
+    ) {}
+
+    ngOnInit() {
+        this.server = window.location.hostname;
+        this.net.request(
+            'open-ils.actor',
+            'opensrf.open-ils.system.ils_version'
+        ).subscribe(v => this.version = v);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html
new file mode 100644 (file)
index 0000000..58fcbf6
--- /dev/null
@@ -0,0 +1,60 @@
+<eg-staff-banner bannerText="Acquisitions Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="container">
+  <eg-link-table columnCount="3">
+    <eg-link-table-link i18n-label label="Cancel Reasons"
+      routerLink="/staff/admin/acq/cancel_reason"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Event Types"
+      routerLink="/staff/admin/acq/claim_event_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Policies"
+      routerLink="/staff/admin/acq/claim_policy"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Policy Actions"
+      routerLink="/staff/admin/acq/claim_policy_action"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Types"
+      routerLink="/staff/admin/acq/claim_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Currency Types"
+      routerLink="/staff/admin/acq/currency_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Distribution Formulas"
+      url="/eg/staff/admin/acq/conify/distribution_formula"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/distribution_formula"></eg-link-table-link>
+      -->
+    <eg-link-table-link i18n-label label="EDI Accounts"
+      routerLink="/staff/admin/acq/edi_account"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="EDI Messages"
+      routerLink="/staff/admin/acq/edi_message"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="EDI Attribute Sets"
+      url="/eg/staff/admin/acq/edi_attr_set"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/edi_attr_set"></eg-link-table-link>
+      -->
+    <eg-link-table-link i18n-label label="Exchange Rates"
+      routerLink="/staff/admin/acq/exchange_rate"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Fund Tags"
+      routerLink="/staff/admin/acq/fund_tag"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Funding Sources"
+      url="/eg/staff/admin/acq/funding_source/list"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/funding_source"></eg-link-table-link>
+      -->
+    <!-- TODO fund admin page w/ year filter and rollover -->
+    <eg-link-table-link i18n-label label="Funds"
+      url="/eg/staff/admin/acq/fund/list"></eg-link-table-link>
+      <!-- routerLink="/staff/admin/acq/fund" -->
+    <eg-link-table-link i18n-label label="Invoice Item Types"
+      routerLink="/staff/admin/acq/invoice_item_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Invoice Payment Method"
+      routerLink="/staff/admin/acq/invoice_payment_method"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Line Item Alerts"
+      routerLink="/staff/admin/acq/lineitem_alert_text"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Line Item MARC Attribute Definitions"
+      routerLink="/staff/admin/acq/lineitem_marc_attr_definition"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Providers"
+      url="/eg/staff/admin/acq/conify/provider"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/provider"></eg-link-table-link>
+      -->
+  </eg-link-table>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts
new file mode 100644 (file)
index 0000000..dc47db9
--- /dev/null
@@ -0,0 +1,11 @@
+import {Component, Input, ViewChildren,
+    AfterViewInit, QueryList} from '@angular/core';
+
+@Component({
+    templateUrl: './admin-acq-splash.component.html'
+})
+
+export class AdminAcqSplashComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts
new file mode 100644 (file)
index 0000000..5c57b3d
--- /dev/null
@@ -0,0 +1,24 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminAcqRoutingModule} from './routing.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminAcqSplashComponent} from './admin-acq-splash.component';
+
+@NgModule({
+  declarations: [
+      AdminAcqSplashComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    AdminAcqRoutingModule
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class AdminAcqModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts
new file mode 100644 (file)
index 0000000..c07dd5d
--- /dev/null
@@ -0,0 +1,22 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AdminAcqSplashComponent} from './admin-acq-splash.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+const routes: Routes = [{
+    path: 'splash',
+    component: AdminAcqSplashComponent
+}, {
+    path: ':table',
+    component: BasicAdminPageComponent,
+    // All ACQ admin pages cover data in the acq.* schema.  No need to
+    // duplicate it within the URL path.  Pass it manually instead.
+    data: [{schema: 'acq'}]
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminAcqRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
new file mode 100644 (file)
index 0000000..0d6be84
--- /dev/null
@@ -0,0 +1,61 @@
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService} from '@eg/core/idl.service';
+
+/**
+ * Generic IDL class editor page.
+ */
+
+@Component({
+    template: `
+      <eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
+      </eg-staff-banner>
+      <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}"></eg-admin-page>
+    `
+})
+
+export class BasicAdminPageComponent implements OnInit {
+
+    idlClass: string;
+    classLabel: string;
+    persistKeyPfx: string;
+
+    constructor(
+        private route: ActivatedRoute,
+        private idl: IdlService
+    ) {
+    }
+
+    ngOnInit() {
+        let schema = this.route.snapshot.paramMap.get('schema');
+        if (!schema) {
+            // Allow callers to pass the schema via static route data
+            const data = this.route.snapshot.data[0];
+            if (data) { schema = data.schema; }
+        }
+        const table = schema + '.' + this.route.snapshot.paramMap.get('table');
+
+        // Set the prefix to "server", "local", "workstation",
+        // extracted from the URL path.
+        this.persistKeyPfx = this.route.snapshot.parent.url[0].path;
+        if (this.persistKeyPfx === 'acq') {
+            // ACQ is a special case, becaus unlike 'server', 'local',
+            // 'workstation', the schema ('acq') is the root of the path.
+            this.persistKeyPfx = '';
+        }
+
+        Object.keys(this.idl.classes).forEach(class_ => {
+            const classDef = this.idl.classes[class_];
+            if (classDef.table === table) {
+                this.idlClass = class_;
+                this.classLabel = classDef.label;
+            }
+        });
+
+        if (!this.idlClass) {
+            throw new Error('Unable to find IDL class for table ' + table);
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/common.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/common.module.ts
new file mode 100644 (file)
index 0000000..5bd71d3
--- /dev/null
@@ -0,0 +1,28 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {LinkTableComponent, LinkTableLinkComponent} from '@eg/staff/share/link-table/link-table.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+@NgModule({
+  declarations: [
+    LinkTableComponent,
+    LinkTableLinkComponent,
+    BasicAdminPageComponent
+  ],
+  imports: [
+    StaffCommonModule
+  ],
+  exports: [
+    StaffCommonModule,
+    LinkTableComponent,
+    LinkTableLinkComponent,
+    BasicAdminPageComponent
+  ],
+  providers: [
+  ]
+})
+
+export class AdminCommonModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts
new file mode 100644 (file)
index 0000000..a93f9ee
--- /dev/null
@@ -0,0 +1,23 @@
+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#AdminWsRoutingModule'
+  }, {
+    path: 'server',
+    loadChildren: '@eg/staff/admin/server/admin-server.module#AdminServerModule'
+  }, {
+    path: 'acq',
+    loadChildren: '@eg/staff/admin/acq/admin-acq.module#AdminAcqModule'
+  }]
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
new file mode 100644 (file)
index 0000000..5e6058d
--- /dev/null
@@ -0,0 +1,99 @@
+<eg-staff-banner bannerText="Server Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="container">
+  <eg-link-table columnCount="3">
+    <eg-link-table-link i18n-label label="Actor Stat Cat Sip Fields"  
+      routerLink="/staff/admin/server/actor/stat_cat_sip_fields"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Age Hold Protect Rules"  
+      routerLink="/staff/admin/server/config/rule_age_hold_protect"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Asset Stat Cat Sip Fields"  
+      routerLink="/staff/admin/server/asset/stat_cat_sip_fields"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Browse Axes"  
+      routerLink="/staff/admin/server/authority/browse_axis"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Control Sets"  
+      routerLink="/staff/admin/server/authority/control_set"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Heading Fields"  
+      routerLink="/staff/admin/server/authority/heading_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Thesauri"  
+      routerLink="/staff/admin/server/authority/thesaurus"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Best-Hold Selection Sort Order"  
+      routerLink="/staff/admin/server/config/best_hold_order"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Billing Types"  
+      routerLink="/staff/admin/server/config/billing_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Call Number Prefixes"  
+      routerLink="/staff/admin/server/asset/call_number_prefix"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Call Number Suffixes"  
+      routerLink="/staff/admin/server/asset/call_number_suffix"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Duration Rules"  
+      routerLink="/staff/admin/server/config/rule_circ_duration"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Limit Groups"  
+      routerLink="/staff/admin/server/config/circ_limit_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Matchpoint Weights"  
+      routerLink="/staff/admin/server/config/circ_matrix_weights"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Max Fine Rules"  
+      routerLink="/staff/admin/server/config/rule_max_fine"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Modifiers"  
+      routerLink="/staff/admin/server/config/circ_modifier"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Recurring Fine Rules"  
+      routerLink="/staff/admin/server/config/rule_recurring_fine"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Copy Statuses"  
+      routerLink="/staff/admin/server/config/copy_status"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Copy Tag Types"  
+      routerLink="/staff/admin/server/config/copy_tag_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Custom Org Unit Trees"  
+      url="/eg/staff/admin/server/actor/org_unit_custom_tree"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Floating Groups"  
+      routerLink="/staff/admin/server/config/floating_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Global Flags"  
+      routerLink="/staff/admin/server/config/global_flag"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Hard Due Date Changes"  
+      routerLink="/staff/admin/server/config/hard_due_date"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Hold Matchpoint Weights"  
+      routerLink="/staff/admin/server/config/hold_matrix_weights"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Import Match Sets"  
+      routerLink="/staff/admin/server/vandelay/match_set"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Coded Value Maps"  
+      routerLink="/staff/admin/server/config/coded_value_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Import Remove Fields"  
+      routerLink="/staff/admin/server/vandelay/import_bib_trash_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Record Attributes"  
+      routerLink="/staff/admin/server/config/record_attr_definition"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Class FTS Maps"  
+      routerLink="/staff/admin/server/config/metabib_class_ts_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Classes"  
+      routerLink="/staff/admin/server/config/metabib_class"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Field FTS Maps"  
+      routerLink="/staff/admin/server/config/metabib_field_ts_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Fields"  
+      routerLink="/staff/admin/server/config/metabib_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Tag Tables"  
+      routerLink="/staff/admin/server/config/marc_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Org Unit Proximity Adjustments"  
+      routerLink="/staff/admin/server/actor/org_unit_proximity_adjustment"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Organization Types"  
+      url="/eg/staff/admin/server/legacy/actor/org_unit_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Org Unit Setting Types"  
+      routerLink="/staff/admin/server/config/org_unit_setting_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Organizational Units"  
+      url="/eg/staff/admin/server/legacy/actor/org_unit"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Permission Groups"  
+      url="/eg/staff/admin/server/legacy/permission/grp_tree"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Permissions"  
+      routerLink="/staff/admin/server/permission/perm_list"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Remote Accounts"  
+      routerLink="/staff/admin/server/config/remote_account"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="SMS Carriers"  
+      routerLink="/staff/admin/server/config/sms_carrier"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="User Activity Types"  
+      routerLink="/staff/admin/server/config/usr_activity_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="User Setting Types"  
+      routerLink="/staff/admin/server/config/usr_setting_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Weights Association"  
+      routerLink="/staff/admin/server/config/weight_assoc"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Z39.50 Index Field Maps"  
+      routerLink="/staff/admin/server/config/z3950_index_field_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Z39.50 Servers"  
+      routerLink="/staff/admin/server/config/z3950_source"></eg-link-table-link>
+  </eg-link-table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts
new file mode 100644 (file)
index 0000000..9debf57
--- /dev/null
@@ -0,0 +1,11 @@
+import {Component, Input, ViewChildren,
+    AfterViewInit, QueryList} from '@angular/core';
+
+@Component({
+    templateUrl: './admin-server-splash.component.html'
+})
+
+export class AdminServerSplashComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
new file mode 100644 (file)
index 0000000..1f00a8a
--- /dev/null
@@ -0,0 +1,24 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminServerRoutingModule} from './routing.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminServerSplashComponent} from './admin-server-splash.component';
+
+@NgModule({
+  declarations: [
+      AdminServerSplashComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    AdminServerRoutingModule
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class AdminServerModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
new file mode 100644 (file)
index 0000000..ceb60f2
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AdminServerSplashComponent} from './admin-server-splash.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+const routes: Routes = [{
+    path: 'splash',
+    component: AdminServerSplashComponent
+}, {
+    path: ':schema/:table',
+    component: BasicAdminPageComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminServerRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts
new file mode 100644 (file)
index 0000000..acdb9a1
--- /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/workstations.module#ManageWorkstationsModule'
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminWsRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts
new file mode 100644 (file)
index 0000000..cebf138
--- /dev/null
@@ -0,0 +1,25 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {WorkstationsComponent} from './workstations.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/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html
new file mode 100644 (file)
index 0000000..a2358d2
--- /dev/null
@@ -0,0 +1,92 @@
+<eg-staff-banner bannerText="Workstation Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<!-- this will remain hidden until opened -->
+<eg-confirm-dialog 
+  #workstationExistsDialog 
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Workstation Exists"
+  dialogBody='Workstation "{{newName}}" already exists.  Use it anyway?'>
+</eg-confirm-dialog>
+
+<div class="row">
+  <div class="col-lg-8 offset-1 mt-3">
+    <div class="alert alert-warning" *ngIf="removeWorkstation" i18n>
+      Workstation {{removeWorkstation}} 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-lg-2">
+        <eg-org-select 
+          [applyDefault]="true"
+          (onChange)="orgOnChange($event)"
+          [hideOrgs]="hideOrgs"
+          [disableOrgs]="disableOrgs"
+          i18n-placeholder
+          placeholder="Owner..." >
+        </eg-org-select>
+      </div>
+      <div class="col-lg-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-outline-dark" 
+              [disabled]="!newName || !newOwner"
+              (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-lg-8">
+        <select class="form-control" [(ngModel)]="selectedName">
+          <option *ngFor="let ws of workstations" value="{{ws.name}}">
+            <span *ngIf="ws.name == defaultName" i18n>
+              {{ws.name}} (Default)
+            </span>
+            <span *ngIf="ws.name != defaultName">
+              {{ws.name}}
+            </span>
+          </option>
+        </select>
+      </div>
+    </div>
+    <div class="row mt-2">
+      <div class="col-lg-6">
+        <button i18n class="btn btn-success" 
+          (click)="useNow()" [disabled]="!selected">
+          Use Now
+        </button>
+        <button i18n class="btn btn-outline-dark" 
+          (click)="setDefault()" [disabled]="!selected">
+          Mark As Default
+        </button>
+        <button i18n class="btn btn-danger"
+          (click)="removeSelected()"
+          [disabled]="!selected || !canDeleteSelected()">
+          Remove
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
new file mode 100644 (file)
index 0000000..a5c72e2
--- /dev/null
@@ -0,0 +1,186 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {EventService} from '@eg/core/event.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+// Slim version of the WS that's stored in the cache.
+interface Workstation {
+    id: number;
+    name: string;
+    owning_lib: number;
+}
+
+@Component({
+  templateUrl: 'workstations.component.html'
+})
+export class WorkstationsComponent implements OnInit {
+
+    selectedName: string;
+    workstations: Workstation[] = [];
+    removeWorkstation: string;
+    newOwner: IdlObject;
+    newName: string;
+    defaultName: string;
+
+    @ViewChild('workstationExistsDialog')
+    private wsExistsDialog: ConfirmDialogComponent;
+
+    // Org selector options.
+    hideOrgs: number[];
+    disableOrgs: number[];
+    orgOnChange = (org: IdlObject): void => {
+        this.newOwner = org;
+    }
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private evt: EventService,
+        private net: NetService,
+        private store: StoreService,
+        private auth: AuthService,
+        private org: OrgService,
+        private perm: PermService
+    ) {}
+
+    ngOnInit() {
+        this.workstations = this.store.getLocalItem('eg.workstation.all') || [];
+        this.defaultName = this.store.getLocalItem('eg.workstation.default');
+        this.selectedName = this.auth.workstation() || this.defaultName;
+        const rm = this.route.snapshot.paramMap.get('remove');
+        if (rm) {
+            this.removeSelected(this.removeWorkstation = rm);
+        }
+
+        // TODO: use the org selector limitPerm option
+        this.perm.hasWorkPermAt(['REGISTER_WORKSTATION'], true)
+        .then(perms => {
+            // Disable org units that cannot have users and any
+            // that this user does not have work perms for.
+            this.disableOrgs =
+                this.org.filterList({canHaveUsers : false}, true)
+                .concat(this.org.filterList(
+                    {notInList : perms.REGISTER_WORKSTATION}, true));
+        });
+    }
+
+    selected(): Workstation {
+        return this.workstations.filter(
+          ws => ws.name === this.selectedName)[0];
+    }
+
+    useNow(): void {
+        if (this.selected()) {
+            this.router.navigate(['/staff/login'],
+                {queryParams: {workstation: this.selected().name}});
+        }
+    }
+
+    setDefault(): void {
+      if (this.selected()) {
+            this.defaultName = this.selected().name;
+            this.store.setLocalItem('eg.workstation.default', this.defaultName);
+        }
+    }
+
+    removeSelected(name?: string): void {
+        if (!name) {
+            name = this.selected().name;
+        }
+
+        this.workstations = this.workstations.filter(w => w.name !== name);
+        this.store.setLocalItem('eg.workstation.all', this.workstations);
+
+        if (this.defaultName === name) {
+            this.defaultName = null;
+            this.store.removeLocalItem('eg.workstation.default');
+        }
+    }
+
+    canDeleteSelected(): boolean {
+        return true;
+    }
+
+    registerWorkstation(): void {
+        console.log(`Registering new workstation ` +
+            `"${this.newName}" at ${this.newOwner.shortname()}`);
+
+        this.newName = this.newOwner.shortname() + '-' + this.newName;
+
+        this.registerWorkstationApi().then(
+            wsId => this.registerWorkstationLocal(wsId),
+            notOk => console.log('Workstation registration canceled/failed')
+        );
+    }
+
+    private handleCollision(): Promise<number> {
+        return new Promise((resolve, reject) => {
+            this.wsExistsDialog.open()
+            .then(
+                confirmed => {
+                    this.registerWorkstationApi(true).then(
+                        wsId => resolve(wsId),
+                        notOk => reject(notOk)
+                    );
+                },
+                dismissed => reject(dismissed)
+            );
+        });
+    }
+
+
+    private registerWorkstationApi(override?: boolean): Promise<number> {
+        let method = 'open-ils.actor.workstation.register';
+        if (override) {
+            method += '.override';
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor', method,
+                this.auth.token(), this.newName, this.newOwner.id()
+            ).subscribe(wsId => {
+                const evt = this.evt.parse(wsId);
+                if (evt) {
+                    if (evt.textcode === 'WORKSTATION_NAME_EXISTS') {
+                        this.handleCollision().then(
+                            id => resolve(id),
+                            notOk => reject(notOk)
+                        );
+                    } else {
+                        console.error(`Registration failed ${evt}`);
+                        reject();
+                    }
+                } else {
+                   resolve(wsId);
+                }
+            });
+        });
+    }
+
+    private registerWorkstationLocal(wsId: number) {
+        const ws: Workstation = {
+            id: wsId,
+            name: this.newName,
+            owning_lib: this.newOwner.id()
+        };
+
+        this.workstations.push(ws);
+        this.store.setLocalItem('eg.workstation.all', this.workstations);
+        this.newName = '';
+        // when registering our first workstation, mark it as the
+        // default and show it as selected in the ws selector.
+        if (this.workstations.length === 1) {
+            this.selectedName = ws.name;
+            this.setDefault();
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts
new file mode 100644 (file)
index 0000000..cbd8dd6
--- /dev/null
@@ -0,0 +1,18 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {WorkstationsRoutingModule} from './routing.module';
+import {WorkstationsComponent} from './workstations.component';
+
+@NgModule({
+  declarations: [
+    WorkstationsComponent,
+  ],
+  imports: [
+    StaffCommonModule,
+    WorkstationsRoutingModule
+  ]
+})
+
+export class ManageWorkstationsModule {}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.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/src/eg2/src/app/staff/catalog/catalog.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
new file mode 100644 (file)
index 0000000..8b2206c
--- /dev/null
@@ -0,0 +1,18 @@
+import {Component, OnInit} from '@angular/core';
+import {StaffCatalogService} from './catalog.service';
+
+@Component({
+  templateUrl: 'catalog.component.html'
+})
+export class CatalogComponent implements OnInit {
+
+    constructor(private staffCat: StaffCatalogService) {}
+
+    ngOnInit() {
+        // Create the search context that will be used by all of my
+        // child components.  After initial creation, the context is
+        // reset and updated as needed to apply new search parameters.
+        this.staffCat.createContext();
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
new file mode 100644 (file)
index 0000000..20e17a0
--- /dev/null
@@ -0,0 +1,44 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {CatalogRoutingModule} from './routing.module';
+import {CatalogComponent} from './catalog.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 {ResultPaginationComponent} from './result/pagination.component';
+import {ResultFacetsComponent} from './result/facets.component';
+import {ResultRecordComponent} from './result/record.component';
+import {StaffCatalogService} from './catalog.service';
+import {RecordPaginationComponent} from './record/pagination.component';
+import {RecordActionsComponent} from './record/actions.component';
+import {HoldingsService} from '@eg/staff/share/holdings.service';
+
+@NgModule({
+  declarations: [
+    CatalogComponent,
+    ResultsComponent,
+    RecordComponent,
+    CopiesComponent,
+    SearchFormComponent,
+    ResultRecordComponent,
+    ResultFacetsComponent,
+    ResultPaginationComponent,
+    RecordPaginationComponent,
+    RecordActionsComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    CatalogCommonModule,
+    CatalogRoutingModule
+  ],
+  providers: [
+    StaffCatalogService,
+    HoldingsService
+  ]
+})
+
+export class CatalogModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
new file mode 100644 (file)
index 0000000..1e50d9b
--- /dev/null
@@ -0,0 +1,87 @@
+import {Injectable} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogUrlService} 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 = 0;
+    defaultSearchOrg: IdlObject;
+    defaultSearchLimit: number;
+
+    // TODO: does unapi support pref-lib for result-page copy counts?
+    prefOrg: IdlObject;
+
+    // Cache the currently selected detail record (i.g. catalog/record/123)
+    // summary so the record detail component can avoid duplicate fetches
+    // during record tab navigation.
+    currentDetailRecordSummary: any;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private org: OrgService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService
+    ) { }
+
+    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; // service, not searchOrg
+        this.searchContext.isStaff = true;
+        this.applySearchDefaults();
+    }
+
+    applySearchDefaults(): void {
+        if (!this.searchContext.searchOrg) {
+            this.searchContext.searchOrg =
+                this.defaultSearchOrg || this.org.root();
+        }
+
+        if (!this.searchContext.pager.limit) {
+            this.searchContext.pager.limit = this.defaultSearchLimit || 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 {
+        if (!this.searchContext.isSearchable()) { return; }
+
+        const params = this.catUrl.toUrlParams(this.searchContext);
+
+        // 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/src/eg2/src/app/staff/catalog/record/actions.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
new file mode 100644 (file)
index 0000000..6fd9454
--- /dev/null
@@ -0,0 +1,70 @@
+
+<eg-string key="catalog.record.toast.conjoined" 
+  i18n-text text="Conjoined Record Target Set"></eg-string>
+<eg-string key="catalog.record.toast.overlay" 
+  i18n-text text="Record Overlay Target Set"></eg-string>
+<eg-string key="catalog.record.toast.holdTransfer" 
+  i18n-text text="Hold Transfer Target Set"></eg-string>
+<eg-string key="catalog.record.toast.volumeTransfer" 
+  i18n-text text="Volume Transfer Target Set"></eg-string>
+<eg-string key="catalog.record.toast.cleared" 
+  text="Record Marks Cleared"></eg-string>
+
+<eg-record-bucket-dialog #recordBucketDialog [recordId]="recId">
+</eg-record-bucket-dialog>
+
+<div class="row ml-0 mr-0">
+
+  <button class="btn btn-info ml-1" (click)="addVolumes()" i18n>
+    Add Volumes
+  </button>
+
+  <div ngbDropdown placement="bottom-right" class="ml-1">
+    <button class="btn btn-info" id="actionsForDd" 
+      ngbDropdownToggle i18n>Mark For...</button>
+    <div ngbDropdownMenu aria-labelledby="actionsForDd">
+      <button class="dropdown-item" (click)="mark('conjoined')">
+        <span i18n>
+          Conjoined Items<ng-container *ngIf="targets.conjoined.current"> 
+            (Currently {{targets.conjoined.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('overlay')">
+        <span i18n>
+          Overlay Target<ng-container *ngIf="targets.overlay.current"> 
+            (Currently {{targets.overlay.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('holdTransfer')">
+        <span i18n>
+          Title Hold Transfer<ng-container *ngIf="targets.holdTransfer.current"> 
+            (Currently {{targets.holdTransfer.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('volumeTransfer')">
+        <span i18n>
+          Volume Transfer<ng-container *ngIf="targets.volumeTransfer.current"> 
+            (Currently {{targets.volumeTransfer.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="clearMarks()">
+        <span i18n>Reset Record Marks</span>
+      </button>
+    </div>
+  </div>
+
+  <div ngbDropdown placement="bottom-right" class="ml-1">
+    <button class="btn btn-info" id="otherActionsForDd" 
+      ngbDropdownToggle i18n>Other Actions</button>
+    <div ngbDropdownMenu aria-labelledby="otherActionsForDd">
+      <button class="dropdown-item" (click)="recordBucketDialog.open({size: 'lg'})">
+        <span i18n>Add To Bucket</span>
+      </button>
+      <a class="dropdown-item" 
+        href="/eg/staff/acq/legacy/lineitem/related/{{recId}}?target=bib">
+        <span i18n>View/Place Orders</span>
+      </a>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
new file mode 100644 (file)
index 0000000..b65bfae
--- /dev/null
@@ -0,0 +1,96 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.service';
+import {StringService} from '@eg/share/string/string.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {HoldingsService} from '@eg/staff/share/holdings.service';
+
+@Component({
+  selector: 'eg-catalog-record-actions',
+  templateUrl: 'actions.component.html'
+})
+export class RecordActionsComponent implements OnInit {
+
+    recId: number;
+    initDone = false;
+    searchContext: CatalogSearchContext;
+
+    targets = {
+        conjoined: {
+          key: 'eg.cat.marked_conjoined_record',
+          current: null
+        },
+        overlay: {
+            key: 'eg.cat.marked_overlay_record',
+            current: null
+        },
+        holdTransfer: {
+            key: 'eg.circ.hold.title_transfer_target',
+            current: null
+        },
+        volumeTransfer: {
+            key: 'eg.cat.marked_volume_transfer_record',
+            current: null
+        }
+    };
+
+    @Input() set recordId(recId: number) {
+        this.recId = recId;
+        if (this.initDone) {
+            // Fire any record specific actions here
+        }
+    }
+
+    constructor(
+        private router: Router,
+        private store: StoreService,
+        private strings: StringService,
+        private toast: ToastService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService,
+        private holdings: HoldingsService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+
+        Object.keys(this.targets).forEach(name => {
+            const target = this.targets[name];
+            target.current = this.store.getLocalItem(target.key);
+        });
+    }
+
+    mark(name: string) {
+        const target = this.targets[name];
+        target.current = this.recId;
+        this.store.setLocalItem(target.key, this.recId);
+        this.strings.interpolate('catalog.record.toast.' + name)
+            .then(txt => this.toast.success(txt));
+    }
+
+    clearMarks() {
+        Object.keys(this.targets).forEach(name => {
+            const target = this.targets[name];
+            target.current = null;
+            this.store.removeLocalItem(target.key);
+        });
+        this.strings.interpolate('catalog.record.toast.cleared')
+            .then(txt => this.toast.success(txt));
+    }
+
+    // TODO: Support adding copies to existing volumes by getting
+    // selected volumes from the holdings grid.
+    // TODO: Support adding like volumes by getting selected
+    // volumes from the holdings grid.
+    addVolumes() {
+        this.holdings.spawnAddHoldingsUi(this.recId);
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
new file mode 100644 (file)
index 0000000..e7d8249
--- /dev/null
@@ -0,0 +1,53 @@
+<ng-template #cnTemplate let-copy="row">
+  {{copy.call_number_prefix_label}}
+  {{copy.call_number_label}}
+  {{copy.call_number_suffix_label}}
+</ng-template>
+
+<ng-template #barcodeTemplate let-copy="row">
+  <div>{{copy.barcode}}</div>
+  <div>
+  <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>
+</ng-template>
+
+<ng-template #holdableTemplate let-copy="row" let-context="userContext">
+  <span *ngIf="context.holdable(copy)" i18n>Yes</span>
+  <span *ngIf="!context.holdable(copy)" i18n>No</span>
+</ng-template>
+
+<div class='eg-copies w-100 mt-3'>
+  <eg-grid #copyGrid [dataSource]="gridDataSource" 
+    [sortable]="false" persistKey="catalog.record.copies">
+    <eg-grid-column i18n-label label="Copy ID" path="id" 
+      [hidden]="true" [index]="true">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Location" path="circ_lib" datatype="org_unit">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Call Number / Copy Notes" 
+      name="callnumber" [cellTemplate]="cnTemplate">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Barcode" name="barcode"
+      [cellTemplate]="barcodeTemplate">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Shelving Location" path="copy_location">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Circulation Modifier" path="circ_modifier">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Age Hold Protection" path="age_protect">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Active/Create Date" 
+      path="active_date" datatype="timestamp">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Holdable?" name="holdable" 
+      [cellTemplate]="holdableTemplate" [cellContext]="copyContext">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Status" path="copy_status">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Due Date" path="due_date" datatype="timestamp">
+    </eg-grid-column>
+  </eg-grid>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts
new file mode 100644 (file)
index 0000000..68908ec
--- /dev/null
@@ -0,0 +1,91 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {of} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {StaffCatalogService} from '../catalog.service';
+import {Pager} from '@eg/share/util/pager';
+import {OrgService} from '@eg/core/org.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+
+@Component({
+  selector: 'eg-catalog-copies',
+  templateUrl: 'copies.component.html'
+})
+export class CopiesComponent implements OnInit {
+
+    recId: number;
+    initDone = false;
+    gridDataSource: GridDataSource;
+    copyContext: any; // grid context
+    @ViewChild('copyGrid') copyGrid: GridComponent;
+
+    @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.copyGrid.reload();
+        }
+    }
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private staffCat: StaffCatalogService,
+    ) {
+        this.gridDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.initDone = true;
+
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            // sorting not currently supported
+            return this.fetchCopies(pager);
+        };
+
+        this.copyContext = {
+            holdable: (copy: any) => {
+                return copy.holdable === 't'
+                    && copy.location_holdable === 't'
+                    && copy.status_holdable === 't';
+            }
+        };
+    }
+
+    collectData() {
+        if (!this.recId) { return; }
+    }
+
+    orgName(orgId: number): string {
+        return this.org.get(orgId).shortname();
+    }
+
+    fetchCopies(pager: Pager): Observable<any> {
+        if (!this.recId) { return of([]); }
+
+        // "Show Result from All Libraries" i.e. global search displays
+        // copies from all branches, sorted by search/pref libs.
+        const copy_depth = this.staffCat.searchContext.global ?
+            this.org.root().ou_type().depth() :
+            this.staffCat.searchContext.searchOrg.ou_type().depth();
+
+        return this.net.request(
+            'open-ils.search',
+            'open-ils.search.bib.copies.staff',
+            this.recId,
+            this.staffCat.searchContext.searchOrg.id(),
+            copy_depth,
+            pager.limit,
+            pager.offset,
+            this.staffCat.prefOrg ? this.staffCat.prefOrg.id() : null
+        ).pipe(map(copy => {
+            copy.active_date = copy.active_date || copy.create_date;
+            return copy;
+        }));
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html b/Open-ILS/src/eg2/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/src/eg2/src/app/staff/catalog/record/pagination.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts
new file mode 100644 (file)
index 0000000..793767b
--- /dev/null
@@ -0,0 +1,164 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.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 = 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: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService,
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        this.setIndex();
+    }
+
+    firstRecord(): void {
+        this.findRecordAtIndex(0).then(id => {
+            const 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 => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+    nextRecord(): void {
+        this.findRecordAtIndex(this.index + 1).then(id => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+    prevRecord(): void {
+        this.findRecordAtIndex(this.index - 1).then(id => {
+            const 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) => {
+            const 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 => {
+                    const rid = this.searchContext.resultIdAt(index);
+                    if (rid) {
+                        resolve(rid);
+                    } else {
+                        reject('no record found');
+                    }
+                }
+            );
+        });
+    }
+
+    refreshSearch(limit?: number): Promise<any> {
+
+        console.debug('paginator refreshing search');
+
+        if (!this.searchContext.isSearchable()) {
+            return Promise.resolve();
+        }
+
+        const origPager = this.searchContext.pager;
+        const 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/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
new file mode 100644 (file)
index 0000000..4c74316
--- /dev/null
@@ -0,0 +1,37 @@
+
+<div id="staff-catalog-record-container">
+  <div class="row ml-0 mr-0">
+    <div id='staff-catalog-bib-navigation'>
+      <div *ngIf="searchContext.isSearchable()">
+        <eg-catalog-record-pagination [recordId]="recordId">
+        </eg-catalog-record-pagination>
+      </div>
+    </div>
+    <!-- push the actions component to the right -->
+    <div class="flex-1"></div>
+    <div id='staff-catalog-bib-navigation'>
+      <eg-catalog-record-actions [recordId]="recordId">
+      </eg-catalog-record-actions>
+    </div>
+  </div>
+  <div id='staff-catalog-bib-summary-container' class='mt-1'>
+    <eg-bib-summary [bibSummary]="summary">
+    </eg-bib-summary>
+  </div>
+  <div id='staff-catalog-bib-tabs-container' class='mt-3'>
+    <ngb-tabset #recordTabs [activeId]="recordTab" (tabChange)="onTabChange($event)">
+      <ngb-tab title="Copy Table" i18n-title id="copy_table">
+        <ng-template ngbTabContent>
+          <eg-catalog-copies [recordId]="recordId"></eg-catalog-copies>
+        </ng-template>
+      </ngb-tab>
+      <ngb-tab title="MARC View" i18n-title id="marc_view">
+        <ng-template ngbTabContent>
+          <eg-marc-html [recordId]="recordId" recordType="bib"></eg-marc-html>
+        </ng-template>
+      </ngb-tab>
+    </ngb-tabset>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
new file mode 100644 (file)
index 0000000..b217e5c
--- /dev/null
@@ -0,0 +1,84 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {StaffCatalogService} from '../catalog.service';
+import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
+
+@Component({
+  selector: 'eg-catalog-record',
+  templateUrl: 'record.component.html'
+})
+export class RecordComponent implements OnInit {
+
+    recordId: number;
+    recordTab: string;
+    summary: BibRecordSummary;
+    searchContext: CatalogSearchContext;
+    @ViewChild('recordTabs') recordTabs: NgbTabset;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+
+        // Watch for URL record ID changes
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.recordTab = params.get('tab') || 'copy_table';
+            this.recordId = +params.get('id');
+            this.searchContext = this.staffCat.searchContext;
+            this.loadRecord();
+        });
+    }
+
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    onTabChange(evt: NgbTabChangeEvent) {
+        this.recordTab = evt.nextId;
+
+        // prevent tab changing until after route navigation
+        evt.preventDefault();
+
+        let url = '/staff/catalog/record/' + this.recordId;
+        if (this.recordTab !== 'copy_table') {
+            url += '/' + this.recordTab;
+        }
+
+        // Retain search parameters
+        this.router.navigate([url], {queryParamsHandling: 'merge'});
+    }
+
+    loadRecord(): void {
+
+        // Avoid re-fetching the same record summary during tab navigation.
+        if (this.staffCat.currentDetailRecordSummary &&
+            this.recordId === this.staffCat.currentDetailRecordSummary.id) {
+            this.summary = this.staffCat.currentDetailRecordSummary;
+            return;
+        }
+
+        this.summary = null;
+        this.bib.getBibSummary(
+            this.recordId,
+            this.searchContext.searchOrg.id(),
+            this.searchContext.searchOrg.ou_type().depth()).toPromise()
+        .then(summary => {
+            this.summary =
+                this.staffCat.currentDetailRecordSummary = summary;
+            this.bib.fleshBibUsers([summary.record]);
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
new file mode 100644 (file)
index 0000000..729beea
--- /dev/null
@@ -0,0 +1,59 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRouteSnapshot} from '@angular/router';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {StaffCatalogService} from './catalog.service';
+
+@Injectable()
+export class CatalogResolver implements Resolve<Promise<any[]>> {
+
+    constructor(
+        private router: Router,
+        private store: ServerStoreService,
+        private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Promise<any[]> {
+
+        console.debug('CatalogResolver:resolve()');
+
+        return Promise.all([
+            this.cat.fetchCcvms(),
+            this.cat.fetchCmfs(),
+            this.fetchSettings()
+        ]);
+    }
+
+    fetchSettings(): Promise<any> {
+        const promises = [];
+
+        promises.push(
+            this.store.getItem('eg.search.search_lib').then(
+                id => this.staffCat.defaultSearchOrg = this.org.get(id)
+            )
+        );
+
+        promises.push(
+            this.store.getItem('eg.search.pref_lib').then(
+                id => this.staffCat.prefOrg = this.org.get(id)
+            )
+        );
+
+        return Promise.all(promises);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html
new file mode 100644 (file)
index 0000000..9681747
--- /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-lg-9">
+                    <a class="card-link"
+                      href='javascript:;'
+                      (click)="applyFacet(facetConf.facetClass, name, value.value)">
+                      {{value.value}}
+                    </a>
+                  </div>
+                  <div class="col-lg-3">{{value.count}}</div>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts
new file mode 100644 (file)
index 0000000..44583b8
--- /dev/null
@@ -0,0 +1,48 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, FacetFilter} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.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: CatalogService,
+        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/src/eg2/src/app/staff/catalog/result/pagination.component.css b/Open-ILS/src/eg2/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/src/eg2/src/app/staff/catalog/result/pagination.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html
new file mode 100644 (file)
index 0000000..7cdda00
--- /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 currentPageList()"
+    [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/src/eg2/src/app/staff/catalog/result/pagination.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts
new file mode 100644 (file)
index 0000000..15214b5
--- /dev/null
@@ -0,0 +1,51 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+
+@Component({
+  selector: 'eg-catalog-result-pagination',
+  styleUrls: ['pagination.component.css'],
+  templateUrl: 'pagination.component.html'
+})
+export class ResultPaginationComponent implements OnInit {
+
+    searchContext: CatalogSearchContext;
+
+    // Maximum number of jump-to-page buttons displayed.
+    @Input() numPages: number;
+
+    constructor(
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {
+        this.numPages = 10;
+    }
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+    }
+
+    currentPageList(): number[] {
+        const pgr = this.searchContext.pager;
+        return pgr.pageRange(pgr.currentPage(), this.numPages);
+    }
+
+    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/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
new file mode 100644 (file)
index 0000000..54ad3db
--- /dev/null
@@ -0,0 +1,132 @@
+<!-- 
+  TODO
+  routerLink's
+  egDateFilter's
+-->
+<eg-record-bucket-dialog #addToListDialog>
+</eg-record-bucket-dialog>
+
+<div class="col-lg-12 card tight-card mb-2 bg-light">
+  <div class="card-body">
+    <div class="row">
+      <div class="col-lg-1">
+        <a href="javascript:void(0)" (click)="navigatToRecord(summary.id)">
+          <img style="height:80px"
+            src="/opac/extras/ac/jacket/small/r/{{summary.id}}"/>
+        </a>
+      </div>
+      <div class="col-lg-5">
+        <div class="row">
+          <div class="col-lg-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(summary.id)">
+              {{summary.display.title || '&nbsp;'}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <!-- nbsp allows the column to take shape when no value exists -->
+            <a href="javascript:void(0)"
+              (click)="searchAuthor(summary)">
+              {{summary.display.author || '&nbsp;'}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <!-- only shows the first icon format -->
+            <span *ngIf="summary.attributes.icon_format && summary.attributes.icon_format[0]">
+              <img class="pr-1"
+                src="/images/format_icons/icon_format/{{summary.attributes.icon_format[0]}}.png"/>
+              <span>{{iconFormatLabel(summary.attributes.icon_format[0])}}</span>
+            </span>
+            <span class='pl-1'>{{summary.display.edition}}</span>
+            <span class='pl-1'>{{summary.display.pubdate}}</span>
+          </div>
+        </div>
+      </div>
+      <div class="col-lg-2">
+        <div class="row" [ngClass]="{'pt-2':copyIndex > 0}" 
+          *ngFor="let copyCount of summary.holdingsSummary; 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-lg-1">
+        <div class="row">
+          <div class="w-100">
+            TCN: {{summary.record.tcn_value()}}
+          </div>
+        </div>
+        <div class="row">
+          <div class="w-100">
+            Holds: {{summary.holdCount}}
+          </div>
+        </div>
+      </div>
+      <div class="col-lg-3">
+        <div class="row">
+          <div class="col-lg-12">
+            <div class="float-right small-text-1">
+              Created {{summary.record.create_date() | date:'shortDate'}} by
+              <!-- creator if fleshed after the initial data set is loaded -->
+              <a *ngIf="summary.record.creator().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.creator().id()}}/checkout">
+                  {{summary.record.creator().usrname()}}
+              </a>
+              <!-- add a spacer pending data to reduce page shuffle -->
+              <span *ngIf="!summary.record.creator().usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <div class="float-right small-text-1" i18n>
+              Edited {{summary.record.edit_date() | date:'shortDate'}} by
+              <a *ngIf="summary.record.editor().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.editor().id()}}/checkout">
+                  {{summary.record.editor().usrname()}}
+              </a>
+              <span *ngIf="!summary.record.editor().usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-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 class="pl-1">
+                <button 
+                  (click)="addToListDialog.recordId=summary.record.id(); addToListDialog.open({size: 'lg'})"
+                  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/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
new file mode 100644 (file)
index 0000000..bfcfd45
--- /dev/null
@@ -0,0 +1,77 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.service';
+
+@Component({
+  selector: 'eg-catalog-result-record',
+  templateUrl: 'record.component.html'
+})
+export class ResultRecordComponent implements OnInit {
+
+    @Input() index: number;  // 0-index display row
+    @Input() summary: BibRecordSummary;
+    searchContext: CatalogSearchContext;
+
+    constructor(
+        private router: Router,
+        private org: OrgService,
+        private net: NetService,
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+        this.summary.getHoldCount();
+    }
+
+    orgName(orgId: number): string {
+        return this.org.get(orgId).shortname();
+    }
+
+    iconFormatLabel(code: string): string {
+        if (this.cat.ccvmMap) {
+            const ccvm = this.cat.ccvmMap.icon_format.filter(
+                format => format.code() === code)[0];
+            if (ccvm) {
+                return ccvm.search_label();
+            }
+        }
+    }
+
+    placeHold(): void {
+        alert('Placing hold on bib ' + this.summary.id);
+    }
+
+    addToList(): void {
+        alert('Adding to list for bib ' + this.summary.id);
+    }
+
+    searchAuthor(summary: any) {
+        this.searchContext.reset();
+        this.searchContext.fieldClass = ['author'];
+        this.searchContext.query = [summary.display.author];
+        this.staffCat.search();
+    }
+
+    /**
+     * Propagate the search params along when navigating to each record.
+     */
+    navigatToRecord(id: number) {
+        const params = this.catUrl.toUrlParams(this.searchContext);
+
+        this.router.navigate(
+          ['/staff/catalog/record/' + id], {queryParams: params});
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
new file mode 100644 (file)
index 0000000..ee9ca8d
--- /dev/null
@@ -0,0 +1,30 @@
+
+<div id="staff-catalog-results-container" *ngIf="searchIsDone()">
+  <div class="row">
+    <div class="col-lg-2"><!--match pagination margin-->
+      <h3 i18n>Search Results ({{searchContext.result.count}})</h3>
+    </div>
+    <div class="col-lg-1"></div>
+    <div class="col-lg-9">
+      <div class="float-right">
+                               <eg-catalog-result-pagination></eg-catalog-result-pagination>
+      </div>
+    </div>
+  </div>
+       <div class="row mt-2">
+               <div class="col-lg-2">
+      <eg-catalog-result-facets></eg-catalog-result-facets>
+               </div>
+               <div class="col-lg-10">
+                       <div *ngIf="searchContext.result">
+                               <div *ngFor="let summary of searchContext.result.records; let idx = index">
+          <div *ngIf="summary">
+                                         <eg-catalog-result-record [summary]="summary" [index]="idx">
+                                         </eg-catalog-result-record>
+          </div>
+                               </div>
+                       </div>
+               </div>
+       </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
new file mode 100644 (file)
index 0000000..d9b7062
--- /dev/null
@@ -0,0 +1,84 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {ActivatedRoute, ParamMap} from '@angular/router';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService} from '@eg/share/catalog/bib-record.service';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StaffCatalogService} from '../catalog.service';
+import {IdlObject} from '@eg/core/idl.service';
+
+@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]: IdlObject} = {};
+
+    constructor(
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private cat: CatalogService,
+        private bib: BibRecordService,
+        private catUrl: CatalogUrlService,
+        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);
+
+        if (this.searchContext.isSearchable()) {
+
+            this.cat.search(this.searchContext)
+            .then(ok => {
+                this.cat.fetchFacets(this.searchContext);
+                this.cat.fetchBibSummaries(this.searchContext)
+                .then(ok2 => this.fleshSearchResults());
+            });
+        }
+    }
+
+    fleshSearchResults(): void {
+        const records = this.searchContext.result.records;
+        if (!records || records.length === 0) { return; }
+
+        // Flesh the creator / editor fields with the user object.
+        this.bib.fleshBibUsers(records.map(r => r.record));
+    }
+
+    searchIsDone(): boolean {
+        return this.searchContext.searchState === CatalogSearchState.COMPLETE;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts
new file mode 100644 (file)
index 0000000..0e3c96f
--- /dev/null
@@ -0,0 +1,30 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CatalogComponent} from './catalog.component';
+import {ResultsComponent} from './result/results.component';
+import {RecordComponent} from './record/record.component';
+import {CatalogResolver} from './resolver.service';
+
+const routes: Routes = [{
+  path: '',
+  component: CatalogComponent,
+  resolve: {catResolver : CatalogResolver},
+  children : [{
+    path: 'search',
+    component: ResultsComponent
+  }, {
+    path: 'record/:id',
+    component: RecordComponent
+  }, {
+    path: 'record/:id/:tab',
+    component: RecordComponent
+  }]
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: [CatalogResolver]
+})
+
+export class CatalogRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css
new file mode 100644 (file)
index 0000000..6201dff
--- /dev/null
@@ -0,0 +1,16 @@
+
+/* filter checkbox labels move to bottom */
+.checkbox label {
+  margin-bottom: .1rem;
+}
+
+/* BS default height is 2.25rem + 2px which is quite chunky.
+ * This better matches the text input heights */
+select.form-control:not([size]):not([multiple]) {
+  padding: .355rem .55rem;
+  height: 2.2rem;
+}
+
+#staffcat-search-form {
+  border-bottom: 2px dashed rgba(0,0,0,.225);
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html
new file mode 100644 (file)
index 0000000..da54f4a
--- /dev/null
@@ -0,0 +1,244 @@
+<!--
+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-lg-9 d-flex">
+      <div class="flex-1">
+        <div *ngIf="idx == 0">
+          <select class="form-control" [(ngModel)]="searchContext.format">
+            <option i18n 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 i18n value='&&'>And</option>
+            <option i18n value='||'>Or</option>
+          </select>
+        </div>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.fieldClass[idx]">
+          <option i18n value='keyword'>Keyword</option>
+          <option i18n value='title'>Title</option>
+          <option i18n value='jtitle'>Journal Title</option>
+          <option i18n value='author'>Author</option>
+          <option i18n value='subject'>Subject</option>
+          <option i18n value='series'>Series</option>
+        </select>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.matchOp[idx]">
+          <option i18n value='contains'>Contains</option>
+          <option i18n value='nocontains'>Does not contain</option>
+          <option i18n value='phrase'>Contains phrase</option>
+          <option i18n value='exact'>Matches exactly</option>
+          <option i18n value='starts'>Starts with</option>
+        </select>
+      </div>
+      <div class="flex-2 pl-1">
+        <div class="form-group">
+          <div *ngIf="idx == 0">
+            <input type="text" class="form-control"
+              id='first-query-input'
+              [(ngModel)]="searchContext.query[idx]"
+              (keyup.enter)="formEnter('query')"
+              placeholder="Query..."/>
+          </div>
+          <div *ngIf="idx > 0">
+            <input type="text" class="form-control"
+              [(ngModel)]="searchContext.query[idx]"
+              (keyup.enter)="formEnter('query')"
+              placeholder="Query..."/>
+          </div>
+        </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-lg-3">
+      <div *ngIf="idx == 0" class="float-right">
+        <button class="btn btn-success mr-1" type="button"
+          [disabled]="searchIsActive()"
+          (click)="searchContext.pager.offset=0;searchByForm()">
+          Search
+        </button>
+        <button class="btn btn-warning mr-1" 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-lg-9 d-flex">
+      <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-lg-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-lg-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-lg-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-lg-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-lg-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 class="col-lg-2">
+      <select class="form-control"
+        [(ngModel)]="searchContext.identQueryType">
+        <option i18n value="identifier|isbn">ISBN</option>
+        <option i18n value="identifier|issn">ISSN</option>
+        <option i18n disabled value="cnbrowse">Call Number (Shelf Browse)</option>
+        <option i18n value="identifier|lccn">LCCN</option>
+        <option i18n value="identifier|tcn">TCN</option>
+        <option i18n disabled value="item_barcode">Item Barcode</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <input id='ident-query-input' type="text" class="form-control"
+        [(ngModel)]="searchContext.identQuery"
+        (keyup.enter)="formEnter('ident')"
+        placeholder="Numeric Query..."/>
+    </div>
+  </div>
+  <div class="row pt-2" *ngIf="showAdvanced()">
+    <div class="col-lg-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-lg-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-lg-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-lg-2">
+      <i>Copy location filter goes here...</i>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
new file mode 100644 (file)
index 0000000..52a26f2
--- /dev/null
@@ -0,0 +1,137 @@
+import {Component, OnInit, AfterViewInit, Renderer2} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from './catalog.service';
+
+@Component({
+  selector: 'eg-catalog-search-form',
+  styleUrls: ['search-form.component.css'],
+  templateUrl: 'search-form.component.html'
+})
+export class SearchFormComponent implements OnInit, AfterViewInit {
+
+    searchContext: CatalogSearchContext;
+    ccvmMap: {[ccvm: string]: IdlObject[]} = {};
+    cmfMap: {[cmf: string]: IdlObject} = {};
+    showAdvancedSearch = false;
+
+    constructor(
+        private renderer: Renderer2,
+        private org: OrgService,
+        private cat: CatalogService,
+        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();
+
+    }
+
+    ngAfterViewInit() {
+        // Query inputs are generated from search context data,
+        // so they are not available until after the first render.
+        // Search context data is extracted synchronously from the URL.
+
+        if (this.searchContext.identQuery) {
+            // Focus identifier query input if identQuery is in progress
+            this.renderer.selectRootElement('#ident-query-input').focus();
+        } else {
+            // Otherwise focus the main query input
+            this.renderer.selectRootElement('#first-query-input').focus();
+        }
+    }
+
+    /**
+     * 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;
+            }
+        });
+
+        if (this.searchContext.identQuery) {
+            show = true;
+        }
+
+        return show;
+    }
+
+    orgOnChange = (org: IdlObject): 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);
+    }
+
+    formEnter(source) {
+        this.searchContext.pager.offset = 0;
+
+        switch (source) {
+
+            case 'query': // main search form query input
+
+                // Be sure a previous ident search does not take precedence
+                // over the newly entered/submitted search query
+                this.searchContext.identQuery = null;
+                break;
+
+            case 'ident': // identifier query input
+                const iq = this.searchContext.identQuery;
+                const qt = this.searchContext.identQueryType;
+                if (iq) {
+                    // Ident queries ignore search-specific filters.
+                    this.searchContext.reset();
+                    this.searchContext.identQuery = iq;
+                    this.searchContext.identQueryType = qt;
+                }
+                break;
+        }
+
+        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/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html
new file mode 100644 (file)
index 0000000..e83cf9e
--- /dev/null
@@ -0,0 +1,19 @@
+
+<eg-staff-banner bannerText="Search for Patron by Barcode" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="col-lg-4">
+  <div class="input-group">
+    <div class="input-group-prepend">
+      <span class="input-group-text" i18n>Barcode:</span>
+    </div>
+    <input type='text' id='barcode-search-input' class="form-control" 
+      placeholder="Barcode" i18n-placeholder [ngModel]='barcode'/>
+    <div class="input-group-append">
+      <button class="btn btn-outline-secondary" 
+        (click)="findUser()" i18n>Submit</button>
+    </div>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
new file mode 100644 (file)
index 0000000..dac5048
--- /dev/null
@@ -0,0 +1,36 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+
+@Component({
+  templateUrl: 'bcsearch.component.html'
+})
+
+export class BcSearchComponent implements OnInit {
+
+    barcode = '';
+
+    constructor(
+        private route: ActivatedRoute,
+        private renderer: Renderer2,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    ngOnInit() {
+
+        this.renderer.selectRootElement('#barcode-search-input').focus();
+        this.barcode = this.route.snapshot.paramMap.get('barcode');
+
+        if (this.barcode) {
+            this.findUser();
+        }
+    }
+
+    findUser(): void {
+        alert('Searching for user ' + this.barcode);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts
new file mode 100644 (file)
index 0000000..d1b16df
--- /dev/null
@@ -0,0 +1,17 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {BcSearchRoutingModule} from './routing.module';
+import {BcSearchComponent} from './bcsearch.component';
+
+@NgModule({
+  declarations: [
+    BcSearchComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    BcSearchRoutingModule,
+  ],
+})
+
+export class BcSearchModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts
new file mode 100644 (file)
index 0000000..ce6783d
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {BcSearchComponent} from './bcsearch.component';
+
+const routes: Routes = [
+  { path: '',
+    component: BcSearchComponent
+  },
+  { path: ':barcode',
+    component: BcSearchComponent
+  },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class BcSearchRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
new file mode 100644 (file)
index 0000000..9033f92
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'bcsearch',
+    loadChildren: '@eg/staff/circ/patron/bcsearch/bcsearch.module#BcSearchModule'
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CircPatronRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
new file mode 100644 (file)
index 0000000..2409977
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'patron',
+    loadChildren: '@eg/staff/circ/patron/routing.module#CircPatronRoutingModule'
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CircRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts
new file mode 100644 (file)
index 0000000..e83143c
--- /dev/null
@@ -0,0 +1,84 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {AudioService} from '@eg/share/util/audio.service';
+import {GridModule} from '@eg/share/grid/grid.module';
+import {StaffBannerComponent} from './share/staff-banner.component';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
+import {OpChangeComponent} from '@eg/staff/share/op-change/op-change.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ToastComponent} from '@eg/share/toast/toast.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {RecordBucketDialogComponent} from '@eg/staff/share/buckets/record-bucket-dialog.component';
+import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
+import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
+
+/**
+ * Imports the EG common modules and adds modules common to all staff UI's.
+ */
+
+@NgModule({
+  declarations: [
+    StaffBannerComponent,
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    OrgSelectComponent,
+    AccessKeyDirective,
+    AccessKeyInfoComponent,
+    ToastComponent,
+    StringComponent,
+    OpChangeComponent,
+    FmRecordEditorComponent,
+    DateSelectComponent,
+    RecordBucketDialogComponent,
+    BibSummaryComponent,
+    TranslateComponent,
+    AdminPageComponent
+  ],
+  imports: [
+    EgCommonModule,
+    GridModule
+  ],
+  exports: [
+    EgCommonModule,
+    GridModule,
+    StaffBannerComponent,
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    OrgSelectComponent,
+    AccessKeyDirective,
+    AccessKeyInfoComponent,
+    ToastComponent,
+    StringComponent,
+    OpChangeComponent,
+    FmRecordEditorComponent,
+    DateSelectComponent,
+    RecordBucketDialogComponent,
+    BibSummaryComponent,
+    TranslateComponent,
+    AdminPageComponent
+  ]
+})
+
+export class StaffCommonModule {
+    static forRoot(): ModuleWithProviders {
+        return {
+            ngModule: StaffCommonModule,
+            providers: [ // Export staff-wide services
+                AccessKeyService,
+                AudioService,
+                StringService,
+                ToastService
+            ]
+        };
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.html b/Open-ILS/src/eg2/src/app/staff/login.component.html
new file mode 100644 (file)
index 0000000..ba474f8
--- /dev/null
@@ -0,0 +1,58 @@
+<div class="container">
+  <div class="col-lg-6 offset-lg-3">
+    <fieldset>
+      <legend class="mb-0" i18n>Sign In</legend>
+      <hr class="mt-1"/>
+      <form (ngSubmit)="handleSubmit()" #loginForm="ngForm" class="form-validated">
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" for="username" i18n>Username</label>
+          <input 
+            type="text" 
+            class="form-control col-lg-8"
+            id="username" 
+            name="username"
+            required
+            autocomplete="username"
+            i18n-placeholder
+            placeholder="Username" 
+            [(ngModel)]="args.username"/>
+        </div>
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" for="password" i18n>Password</label>
+          <input 
+            type="password" 
+            class="form-control col-lg-8"
+            id="password" 
+            name="password"
+            required
+            autocomplete="current-password"
+            i18n-placeholder
+            placeholder="Password" 
+            [(ngModel)]="args.password"/>
+        </div>
+
+        <div class="form-group row" *ngIf="workstations && workstations.length">
+          <label class="col-lg-4 text-right font-weight-bold" for="workstation" i18n>Workstation</label>
+          <select 
+            class="form-control col-lg-8" 
+            id="workstation" 
+            name="workstation"
+            required
+            [(ngModel)]="args.workstation">
+            <option *ngFor="let ws of workstations" [value]="ws.name">
+              {{ws.name}}
+            </option>
+          </select>
+        </div>
+
+        <div class="row">
+          <div class="col-lg-8 offset-lg-4 pl-0">
+            <button type="submit" class="btn btn-outline-dark" i18n>Sign in</button>
+          </div>
+        </div>
+      </form>
+    </fieldset>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.ts b/Open-ILS/src/eg2/src/app/staff/login.component.ts
new file mode 100644 (file)
index 0000000..2c1ac2a
--- /dev/null
@@ -0,0 +1,96 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {Location} from '@angular/common';
+import {Router, ActivatedRoute} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {StoreService} from '@eg/core/store.service';
+
+@Component({
+  templateUrl : './login.component.html'
+})
+
+export class StaffLoginComponent implements OnInit {
+
+    workstations: any[];
+
+    args = {
+      username : '',
+      password : '',
+      workstation : '',
+      type : 'staff'
+    };
+
+    constructor(
+      private router: Router,
+      private route: ActivatedRoute,
+      private ngLocation: Location,
+      private renderer: Renderer2,
+      private auth: AuthService,
+      private store: StoreService
+    ) {}
+
+    ngOnInit() {
+        // clear out any stale auth data
+        this.auth.logout();
+
+        // Focus username
+        this.renderer.selectRootElement('#username').focus();
+
+        this.workstations = this.store.getLocalItem('eg.workstation.all');
+        this.args.workstation =
+            this.store.getLocalItem('eg.workstation.default');
+        this.applyWorkstation();
+    }
+
+    applyWorkstation() {
+        const wanted = this.route.snapshot.queryParamMap.get('workstation');
+        if (!wanted) { return; } // use the default
+
+        const exists = this.workstations.filter(w => w.name === wanted)[0];
+        if (exists) {
+            this.args.workstation = wanted;
+        } else {
+            console.error(`Unknown workstation requested: ${wanted}`);
+        }
+    }
+
+    handleSubmit() {
+
+        // post-login URL
+        let url: string = this.auth.redirectUrl || '/staff/splash';
+
+        // prevent sending the user back to the login page
+        if (url.startsWith('/staff/login')) {
+            url = '/staff/splash';
+        }
+
+        const workstation: string = this.args.workstation;
+
+        this.auth.login(this.args).then(
+            ok => {
+                this.auth.redirectUrl = null;
+
+                if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) {
+                    // User attempted to login with a workstation that is
+                    // unknown to the server. Redirect to the WS admin page.
+                    // Reset the WS state to avoid looping back to WS removal
+                    // page before the new workstation can be activated.
+                    this.auth.workstationState = AuthWsState.PENDING;
+                    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/src/eg2/src/app/staff/nav.component.css b/Open-ILS/src/eg2/src/app/staff/nav.component.css
new file mode 100644 (file)
index 0000000..63d3e37
--- /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 {
+    padding-left: 0px;
+}
+
+#staff-navbar {
+    background: -webkit-linear-gradient(#00593d, #007a54);
+    background-color: #007a54;
+    color: #fff;
+    font-size: 14px;
+}
+
+#staff-navbar .navbar-nav {
+  padding: 4px;
+}
+
+/* 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/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
new file mode 100644 (file)
index 0000000..419f5d5
--- /dev/null
@@ -0,0 +1,432 @@
+<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"
+          egAccessKey keyCtx="navbar"
+          keySpec="alt+h" i18n-keySpec
+          keyDesc="Navigate Home" i18n-keyDesc>
+          <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"
+            egAccessKey keyCtx="navbar"
+            keySpec="alt+c" i18n-keySpec
+            keyDesc="Navigate To Catalog" i18n-keyDesc>
+            <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"
+            egAccessKey keyCtx="navbar"
+            keySpec="f5" i18n-keySpec
+            keyDesc="Navigate To Item Status" i18n-keyDesc>
+            <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" (click)="reprintLast()">
+            <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>
+
+    <!-- CATALOGING -->
+    
+    <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 href="/eg/staff/cat/catalog/index" class="dropdown-item">
+            <span class="material-icons">search</span>
+            <span i18n>Search the Catalog</span>
+          </a>
+          <!--
+            Link to experimental Angular staff catalog.
+            Leaving disabled until more functionality can be fleshed out.
+          -->
+          <!--
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>Staff Catalog (Experimental)</span>
+          </a>
+          -->
+          <a href="/eg/staff/cat/bucket/record/view" class="dropdown-item">
+            <span class="material-icons">list_alt</span>
+            <span i18n>Record Buckets</span>
+          </a>
+          <a href="/eg/staff/cat/bucket/copy/view" class="dropdown-item">
+            <span class="material-icons">list_alt</span>
+            <span i18n>Copy Buckets</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/retrieve_by_id" class="dropdown-item">
+            <span class="material-icons">collections</span>
+            <span i18n>Retrieve Bib Record by ID</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/retrieve_by_tcn"
+            eg-accesskey="shift+f3" 
+            eg-accesskey-desc="Retrieve Last Bib Record" class="dropdown-item">
+            <span class="material-icons">collections_bookmark</span>
+            <span i18n>Retrieve Bib Record by TCN</span>
+          </a>
+          <a href="" ng-click="retrieveLastRecord()"
+            eg-accesskey="shift+f8" 
+            eg-accesskey-desc="Retrieve Last Bib Record" class="dropdown-item">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Last Bib Record</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/new_bib" class="dropdown-item">
+            <span class="material-icons">add</span>
+            <span i18n>Create New MARC Record</span>
+          </a>
+          <a href="/eg/staff/cat/z3950/index" class="dropdown-item">
+            <span class="material-icons">cloud_download</span>
+            <span i18n>Import Record from Z39.50</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/vandelay" class="dropdown-item">
+            <span class="material-icons">import_export</span>
+            <span i18n>MARC Batch Import/Export</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/batchEdit" class="dropdown-item">
+            <span class="material-icons">format_paint</span>
+            <span i18n>MARC Batch Edit</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/verifyURLs" class="dropdown-item">
+            <span class="material-icons">link</span>
+            <span i18n>Link Checker</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/manageAuthorities" class="dropdown-item">
+            <span class="material-icons">lock</span>
+            <span i18n>Manage Authorities</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <!-- ACQUISITIONS -->
+
+    <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" 
+            href="/eg/staff/acq/legacy/search/unified">
+            <span class="material-icons">search</span>
+            <span i18n>General Search</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=pl">
+            <span class="material-icons">view_list</span>
+            <span i18n>My Selection Lists</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/brief_record">
+            <span class="material-icons">edit</span>
+            <span i18n>New Brief Record</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/user_request">
+            <span class="material-icons">thumb_up</span>
+            <span i18n>Patron Requests</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/bib_search">
+            <span class="material-icons">cloud_download</span>
+            <span i18n>MARC Federated Search</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/from_bib">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Load Catalog Record IDs</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/upload">
+            <span class="material-icons">cloud_upload</span>
+            <span i18n>Load MARC Order Records</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=po">
+            <span class="material-icons">shopping_cart</span>
+            <span i18n>Purchase Orders</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/acq/legacy/po/create">
+            <span class="material-icons">add_shopping_cart</span>
+            <span i18n>Create Purchase Order</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/financial/claim_eligible">
+            <span class="material-icons">contact_phone</span>
+            <span i18n>Claim-Ready Items</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=inv">
+            <span class="material-icons">attach_money</span>
+            <span i18n>Open Invoices</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/invoice/view?create=1">
+            <span class="material-icons">monetization_on</span>
+            <span i18n>Create Invoice</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" href="/eg/staff/booking/legacy/booking/reservation">
+            <span class="material-icons">add</span>
+            <span i18n>Create Reservations</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pull_list">
+            <span class="material-icons">list</span>
+            <span i18n>Pull List</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/capture">
+            <span class="material-icons">pin_drop</span>
+            <span i18n>Capture Resources</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pickup">
+            <span class="material-icons">trending_up</span>
+            <span i18n>Pick Up Reservations</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/return">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Return Reservations</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" href="/eg/staff/admin/workstation/index">
+            <span class="material-icons">computer</span>
+            <span i18n>Workstation</span>
+          </a>
+          <!--
+          Leaving here as a reminder this UI exists.
+          <a class="dropdown-item"
+              routerLink="/staff/admin/workstation/workstations/manage">
+            <span class="material-icons">computer</span>
+            <span i18n>Registered Workstations</span>
+          </a>
+          -->
+          <a class="dropdown-item" href="/eg/staff/admin/user_perms">
+            <span class="material-icons">person</span>
+            <span i18n>User Permission Editor</span>
+          </a>
+          <!-- Angular version
+          <a class="dropdown-item"
+              routerLink="/staff/admin/server/splash">
+            <span class="material-icons">account_balance</span>
+            <span i18n>Server Administration</span>
+          </a>
+          -->
+          <a class="dropdown-item" href="/eg/staff/admin/server/index">
+            <span class="material-icons">account_balance</span>
+            <span i18n>Server Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/local/index">
+            <span class="material-icons">landscape</span>
+            <span i18n>Local Administration</span>
+          </a>
+          <a class="dropdown-item"
+              routerLink="/staff/admin/acq/splash">
+            <span class="material-icons">attach_money</span>
+            <span i18n>Acquisitions Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/serials/index">
+            <span class="material-icons">layers</span>
+            <span i18n>Serials Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/booking/index">
+            <span class="material-icons">business_center</span>
+            <span i18n>Booking Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/reporter/legacy/main">
+            <span class="material-icons">insert_chart_outlined</span>
+            <span i18n>Reports</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+
+    <div class="navbar-nav mr-auto"></div>
+    <div class="navbar-nav" *ngIf="user()">
+      <span i18n>{{user()}} @ {{workstation()}}</span>
+    </div>
+    <div class="navbar-nav" *ngIf="locales.length > 1 && currentLocale">
+      <div ngbDropdown class="nav-item dropdown" placement="bottom-right">
+        <a ngbDropdownToggle i18n i18n-title
+          title="Select Locale"
+          class="nav-link dropdown-toggle no-caret with-material-icon">
+          <i class="material-icons">flag</i>
+          <span>{{currentLocale.name()}}</span>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" (click)="setLocale(locale)" 
+            [ngClass]="{disabled: currentLocale.code() == locale.code()}"
+            *ngFor="let locale of locales">
+            <span class="material-icons">add_location</span>
+            <span i18n>{{locale.name()}}</span>
+          </a>
+        </div>
+      </div>
+    </div>
+    <div class="navbar-nav" *ngIf="user()">
+      <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>
+          <eg-op-change #navOpChange
+            i18n-failMessage
+            i18n-successMessage
+            failMessage="Operator Change Failed"
+            successMessage="Operator Change Succeeded">
+          </eg-op-change>
+          <a class="dropdown-item" *ngIf="!opChangeActive()" 
+            (click)="navOpChange.open()">
+            <span class="material-icons">transform</span>
+            <span i18n>Change Operator</span>
+          </a>
+          <a *ngIf="opChangeActive()" class="dropdown-item" 
+            (click)="navOpChange.restore()">
+            <span class="material-icons">transform</span>
+            <span i18n>Restore Operator</span>
+          </a>
+          <a class="dropdown-item" (click)="logout()">
+            <span class="material-icons">lock_outline</span>
+            <span i18n>Logout</span>
+          </a>
+          <a class="dropdown-item" routerLink="/staff/about">
+            <span class="material-icons">info_outline</span>
+            <span i18n>About</span>
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.ts b/Open-ILS/src/eg2/src/app/staff/nav.component.ts
new file mode 100644 (file)
index 0000000..c477c11
--- /dev/null
@@ -0,0 +1,72 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Location} from '@angular/common';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+@Component({
+    selector: 'eg-staff-nav-bar',
+    styleUrls: ['nav.component.css'],
+    templateUrl: 'nav.component.html'
+})
+
+export class StaffNavComponent implements OnInit {
+
+    // Locales that have Angular staff translations
+    locales: any[];
+    currentLocale: any;
+
+    constructor(
+        private router: Router,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private locale: LocaleService,
+        private printer: PrintService
+    ) {
+        this.locales = [];
+    }
+
+    ngOnInit() {
+
+        this.locale.supportedLocales().subscribe(
+            l => this.locales.push(l),
+            err => {},
+            () => {
+                this.currentLocale = this.locales.filter(
+                    l => l.code() === this.locale.currentLocaleCode())[0];
+            }
+        );
+    }
+
+    user() {
+        return this.auth.user() ? this.auth.user().usrname() : '';
+    }
+
+    workstation() {
+        return this.auth.user() ? this.auth.workstation() : '';
+    }
+
+    setLocale(locale: any) {
+        this.locale.setLocale(locale.code());
+    }
+
+    opChangeActive(): boolean {
+        return this.auth.opChangeIsActive();
+    }
+
+    // Broadcast to all tabs that we're logging out.
+    // Redirect to the login page, which performs the remaining
+    // logout duties.
+    logout(): void {
+        this.auth.broadcastLogout();
+        this.router.navigate(['/staff/login']);
+    }
+
+    reprintLast() {
+        this.printer.reprintLast();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
new file mode 100644 (file)
index 0000000..ccee922
--- /dev/null
@@ -0,0 +1,143 @@
+import {Injectable} from '@angular/core';
+import {Location} from '@angular/common';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {of} from 'rxjs';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {PermService} from '@eg/core/perm.service';
+import {OrgService} from '@eg/core/org.service';
+import {FormatService} from '@eg/core/format.service';
+
+const LOGIN_PATH = '/staff/login';
+const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
+
+/**
+ * Load data used by all staff modules.
+ */
+@Injectable()
+export class StaffResolver implements Resolve<Observable<any>> {
+
+    // Tracks the primary resolve observable.
+    observer: Observer<any>;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private ngLocation: Location,
+        private store: StoreService,
+        private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
+        private perm: PermService,
+        private format: FormatService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Observable<any> {
+
+        // 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');
+
+        // Not sure how to get the path without params... using this for now.
+        const path = state.url.split('?')[0];
+        if (path === '/staff/login') {
+            return of(true);
+        }
+
+        const observable: Observable<any>
+            = Observable.create(o => this.observer = o);
+
+        this.auth.testAuthToken().then(
+            tokenOk => {
+                this.confirmStaffPerms().then(
+                    hasPerms => {
+                        this.auth.verifyWorkstation().then(
+                            wsOk => {
+                                this.loadStartupData()
+                                .then(ok => this.observer.complete());
+                            },
+                            wsNotOk => this.handleInvalidWorkstation(path)
+                        );
+                    },
+                    hasNotPerms => {
+                        this.observer.error(
+                            'User does not have staff permissions');
+                    }
+                );
+            },
+            tokenNotOk => this.handleInvalidToken(state)
+        );
+
+        return observable;
+    }
+
+
+    // Confirm the user has the STAFF_LOGIN permission anywhere before
+    // allowing the staff sub-tree to load. This will prevent users
+    // with valid, non-staff authtokens from attempting to connect and
+    // subsequently getting redirected to the workstation admin page
+    // (since they won't have a valid WS either).
+    confirmStaffPerms(): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.perm.hasWorkPermAt(['STAFF_LOGIN']).then(
+                permMap => {
+                    if (permMap.STAFF_LOGIN.length) {
+                        resolve('perm check OK');
+                    } else {
+                        reject('perm check faield');
+                    }
+                }
+            );
+        });
+    }
+
+
+    // A page that's not the login page was requested without a
+    // valid auth token.  Send the caller back to the login page.
+    handleInvalidToken(state: RouterStateSnapshot): void {
+        console.debug('StaffResolver: authtoken is not valid');
+        this.auth.redirectUrl = state.url;
+        this.router.navigate([LOGIN_PATH]);
+        this.observer.error('invalid or no auth token');
+    }
+
+    handleInvalidWorkstation(path: string): void {
+
+        if (path.startsWith(WS_MANAGE_PATH)) {
+            // user is navigating to the WS admin page.
+            this.observer.complete();
+        } else {
+            this.router.navigate([WS_MANAGE_PATH]);
+            this.observer.error(`Auth session linked to no
+                workstation or a workstation unknown to this browser`);
+        }
+    }
+
+    /**
+     * Fetches data common to all staff interfaces.
+     */
+    loadStartupData(): Promise<void> {
+
+        // Fetch settings needed globally.  This will cache the values
+        // in the org service.
+        return this.org.settings([
+            'lib.timezone',
+            'webstaff.format.dates',
+            'webstaff.format.date_and_time',
+            'ui.staff.max_recent_patrons'
+        ]).then(settings => {
+            this.format.wsOrgTimezone = settings['lib.timezone'];
+            this.format.dateFormat = settings['webstaff.format.dates'];
+            this.format.dateTimeFormat = settings['webstaff.format.date_and_time'];
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
new file mode 100644 (file)
index 0000000..b515f38
--- /dev/null
@@ -0,0 +1,52 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {StaffResolver} from './resolver.service';
+import {StaffComponent} from './staff.component';
+import {StaffLoginComponent} from './login.component';
+import {StaffSplashComponent} from './splash.component';
+import {AboutComponent} from './about.component';
+
+// Not using 'canActivate' because it's called before all resolvers,
+// even the parent resolver, but the resolvers parse the IDL, load settings,
+// etc.  Chicken, meet egg.
+
+const routes: Routes = [{
+  path: '',
+  component: StaffComponent,
+  resolve: {staffResolver : StaffResolver},
+  children: [{
+    path: '',
+    redirectTo: 'splash',
+    pathMatch: 'full',
+  }, {
+    path: 'about',
+    component: AboutComponent
+  }, {
+    path: 'login',
+    component: StaffLoginComponent
+  }, {
+    path: 'splash',
+    component: StaffSplashComponent
+  }, {
+    path: 'circ',
+    loadChildren : '@eg/staff/circ/routing.module#CircRoutingModule'
+  }, {
+    path: 'catalog',
+    loadChildren : '@eg/staff/catalog/catalog.module#CatalogModule'
+  }, {
+    path: 'sandbox',
+    loadChildren : '@eg/staff/sandbox/sandbox.module#SandboxModule'
+  }, {
+    path: 'admin',
+    loadChildren : '@eg/staff/admin/routing.module#AdminRoutingModule'
+  }]
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: [StaffResolver]
+})
+
+export class StaffRoutingModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/README b/Open-ILS/src/eg2/src/app/staff/sandbox/README
new file mode 100644 (file)
index 0000000..66e77dc
--- /dev/null
@@ -0,0 +1 @@
+Place for experimenting with code.
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts
new file mode 100644 (file)
index 0000000..144f0d2
--- /dev/null
@@ -0,0 +1,16 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {SandboxComponent} from './sandbox.component';
+
+const routes: Routes = [{
+  path: '',
+  component: SandboxComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: []
+})
+
+export class SandboxRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
new file mode 100644 (file)
index 0000000..289ed50
--- /dev/null
@@ -0,0 +1,133 @@
+
+<eg-staff-banner bannerText="Sandbox" i18n-bannerText>
+</eg-staff-banner>
+
+<!-- FM Editor Experiments ----------------------------- -->
+<div class="row mb-3">
+  <ng-template #descriptionTemplate 
+      let-field="field" let-record="record" let-hello="hello">
+  <!-- example custom template for editing the 'description' field -->
+    <textarea
+      placeholder="{{hello}}"
+      class="form-control"
+      name="{{field.name}}"
+      [readonly]="field.readOnly"
+      [required]="field.isRequired()"
+      [ngModel]="record[field.name]()"
+      (ngModelChange)="record[field.name]($event)">
+    </textarea>
+  </ng-template>
+  <eg-fm-record-editor #fmRecordEditor 
+      idlClass="cmrcfld" mode="create" 
+      [customFieldTemplates]="{description:{template:descriptionTemplate,context:{'hello':'goodbye'}}}"
+      recordId="1" orgDefaultAllowed="owner">
+  </eg-fm-record-editor>
+  <button class="btn btn-dark" (click)="fmRecordEditor.open({size:'lg'})">
+      Fm Record Editor
+  </button>
+</div>
+<!-- / FM Editor Experiments ----------------------------- -->
+
+<!-- Progress Dialog Experiments ----------------------------- -->
+<div class="row mb-3">
+  <div class="col-lg-3">
+    <button class="btn btn-outline-danger" (click)="progress.increment()">Increment Inline</button>
+  </div>
+  <div class="col-lg-3">
+    <eg-progress-inline [max]="100" [value]="1" #progress></eg-progress-inline>
+  </div>
+</div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+    <eg-progress-dialog #progressDialog>
+    </eg-progress-dialog>
+    <button class="btn btn-light" (click)="showProgress()">Test Progress Dialog</button>
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox [allowFreeText]="true" 
+      placeholder="Combobox with static data"
+      [entries]="cbEntries"></eg-combobox>
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox
+      placeholder="Combobox with dynamic data"
+      [asyncDataSource]="cbAsyncSource"></eg-combobox>
+  </div>
+</div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+   <button class="btn btn-info" (click)="testToast()">Test Toast Message</button>
+  </div>
+  <div class="col-lg-2">
+    Org select with limit perms
+  </div>
+  <div class="col-lg-2">
+    <eg-org-select [limitPerms]="['REGISTER_WORKSTATION']">
+    </eg-org-select>
+  </div>
+</div>
+<!-- /Progress Dialog Experiments ----------------------------- -->
+
+<!-- eg strings -->
+<!--
+<div class="row mb-3">
+    <eg-string #helloString text="Hello, {{name}}" i18n-text></eg-string>
+    <button class="btn btn-success" (click)="testStrings()">Test Strings</button>
+</div>
+-->
+
+<div class="row mb-3">
+    <ng-template #helloStrTmpl let-name="name" i18n>Hello, {{name}}</ng-template>
+    <!--
+    <eg-string #helloStr key="helloKey" [template]="helloStrTmpl"></eg-string>
+    -->
+    <eg-string key="staff.sandbox.test" [template]="helloStrTmpl"></eg-string>
+    <button class="btn btn-success" (click)="testStrings()">Test Strings</button>
+</div>
+
+<div class="row">
+  <div class="form-group">
+    <eg-date-select (onChangeAsDate)="changeDate($event)"
+        initialYmd="2017-03-04">
+    </eg-date-select>
+  </div>
+  <div>HERE: {{testDate}}</div>
+</div>
+
+<!-- printing -->
+
+<button class="btn btn-secondary" (click)="doPrint()">Test Print</button>
+<ng-template #printTemplate let-context>Hello, {{context.world}}!</ng-template>
+
+<br/><br/>
+HERasdfE
+<div class="row">
+  <div class="col-lg-3">
+    <eg-translate #translate [idlObject]="oneBtype" fieldName="name"></eg-translate>
+    <button class="btn btn-info"
+      (click)="translate.open({size:'lg'})">Translate</button>
+  </div>
+</div>
+<br/><br/>
+
+<!-- grid stuff -->
+<ng-template #cellTmpl let-row="row" let-col="col" let-userContext="userContext">
+  HELLO {{userContext.hello}}
+  <button>{{row.id()}}</button>
+</ng-template>
+<eg-grid #cbtGrid idlClass="cbt" 
+  [dataSource]="btSource" 
+  [rowClassCallback]="btGridRowClassCallback"
+  [rowFlairIsEnabled]="true"
+  [rowFlairCallback]="btGridRowFlairCallback"
+  [cellClassCallback]="btGridCellClassCallback"
+  [sortable]="true">
+  <eg-grid-column name="test" [cellTemplate]="cellTmpl" 
+    [cellContext]="btGridTestContext" [sortable]="false">
+  </eg-grid-column>
+  <eg-grid-column [sortable]="false" path="owner.name"></eg-grid-column>
+</eg-grid>
+
+<br/><br/>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
new file mode 100644 (file)
index 0000000..92b18dc
--- /dev/null
@@ -0,0 +1,188 @@
+import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringService} from '@eg/share/string/string.service';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/timer';
+import {of} from 'rxjs';
+import {map} from 'rxjs/operators/map';
+import {take} from 'rxjs/operators/take';
+import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {Pager} from '@eg/share/util/pager';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  templateUrl: 'sandbox.component.html'
+})
+export class SandboxComponent implements OnInit {
+
+    @ViewChild('progressDialog')
+    private progressDialog: ProgressDialogComponent;
+
+    @ViewChild('dateSelect')
+    private dateSelector: DateSelectComponent;
+
+    @ViewChild('printTemplate')
+    private printTemplate: TemplateRef<any>;
+
+    // @ViewChild('helloStr') private helloStr: StringComponent;
+
+    gridDataSource: GridDataSource = new GridDataSource();
+
+    cbEntries: ComboboxEntry[];
+    // supplier of async combobox data
+    cbAsyncSource: (term: string) => Observable<ComboboxEntry>;
+
+    btSource: GridDataSource = new GridDataSource();
+    world = 'world'; // for local template version
+    btGridTestContext: any = {hello : this.world};
+
+    renderLocal = false;
+
+    testDate: any;
+
+    testStr: string;
+    @Input() set testString(str: string) {
+        this.testStr = str;
+    }
+
+    oneBtype: IdlObject;
+
+    name = 'Jane';
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private strings: StringService,
+        private toast: ToastService,
+        private printer: PrintService
+    ) {
+    }
+
+    ngOnInit() {
+
+        this.gridDataSource.data = [
+            {name: 'Jane', state: 'AZ'},
+            {name: 'Al', state: 'CA'},
+            {name: 'The Tick', state: 'TX'}
+        ];
+
+        this.pcrud.retrieveAll('cmrcfld', {order_by: {cmrcfld: 'name'}})
+        .subscribe(format => {
+            if (!this.cbEntries) { this.cbEntries = []; }
+            this.cbEntries.push({id: format.id(), label: format.name()});
+        });
+
+        this.cbAsyncSource = term => {
+            return this.pcrud.search(
+                'cmrcfld',
+                {name: {'ilike': `%${term}%`}}, // could -or search on label
+                {order_by: {cmrcfld: 'name'}}
+            ).pipe(map(marcField => {
+                return {id: marcField.id(), label: marcField.name()};
+            }));
+        };
+
+        this.btSource.getRows = (pager: Pager, sort: any[]) => {
+
+            const orderBy: any = {cbt: 'name'};
+            if (sort.length) {
+                orderBy.cbt = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            return this.pcrud.retrieveAll('cbt', {
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            }).pipe(map(cbt => {
+                // example of inline fleshing
+                cbt.owner(this.org.get(cbt.owner()));
+                this.oneBtype = cbt;
+                return cbt;
+            }));
+        };
+    }
+
+    btGridRowClassCallback(row: any): string {
+        if (row.id() === 1) {
+            return 'text-uppercase font-weight-bold text-danger';
+        }
+    }
+
+    btGridRowFlairCallback(row: any): GridRowFlairEntry {
+        const flair = {icon: null, title: null};
+        if (row.id() === 2) {
+            flair.icon = 'priority_high';
+            flair.title = 'I Am ID 2';
+        } else if (row.id() === 3) {
+            flair.icon = 'not_interested';
+        }
+        return flair;
+    }
+
+    // apply to all 'name' columns regardless of row
+    btGridCellClassCallback(row: any, col: GridColumn): string {
+        if (col.name === 'name') {
+            if (row.id() === 7) {
+                return 'text-lowercase font-weight-bold text-info';
+            }
+            return 'text-uppercase font-weight-bold text-success';
+        }
+    }
+
+    doPrint() {
+        this.printer.print({
+            template: this.printTemplate,
+            contextData: {world : this.world},
+            printContext: 'default'
+        });
+
+        this.printer.print({
+            text: '<b>hello</b>',
+            printContext: 'default'
+        });
+
+    }
+
+    changeDate(date) {
+        console.log('HERE WITH ' + date);
+        this.testDate = date;
+    }
+
+    showProgress() {
+        this.progressDialog.open();
+
+        // every 250ms emit x*10 for 0-10
+        Observable.timer(0, 250).pipe(
+            map(x => x * 10),
+            take(11)
+        ).subscribe(
+            val => this.progressDialog.update({value: val, max: 100}),
+            err => {},
+            ()  => this.progressDialog.close()
+        );
+    }
+
+    testToast() {
+        this.toast.success('HELLO TOAST TEST');
+        setTimeout(() => this.toast.danger('DANGER TEST AHHH!'), 4000);
+    }
+
+    testStrings() {
+        this.strings.interpolate('staff.sandbox.test', {name : 'janey'})
+            .then(txt => this.toast.success(txt));
+
+        setTimeout(() => {
+            this.strings.interpolate('staff.sandbox.test', {name : 'johnny'})
+                .then(txt => this.toast.success(txt));
+        }, 4000);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts
new file mode 100644 (file)
index 0000000..58910dd
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {SandboxRoutingModule} from './routing.module';
+import {SandboxComponent} from './sandbox.component';
+
+@NgModule({
+  declarations: [
+    SandboxComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    SandboxRoutingModule,
+  ],
+  providers: [
+  ]
+})
+
+export class SandboxModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/share/README b/Open-ILS/src/eg2/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/src/eg2/src/app/staff/share/admin-page/admin-page.component.html b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
new file mode 100644 (file)
index 0000000..194f06b
--- /dev/null
@@ -0,0 +1,59 @@
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Succeessfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+  <div class="d-flex">
+    <div>
+      <div class="input-group">
+        <div class="input-group-prepend">
+          <span class="input-group-text">{{orgFieldLabel}}</span>
+        </div>
+        <eg-org-select 
+          [limitPerms]="viewPerms"
+          [initialOrg]="contextOrg"
+          (onChange)="orgOnChange($event)">
+        </eg-org-select>
+      </div>
+    </div>
+    <div class="pl-2">
+      <div class="form-check">
+        <input type="checkbox" (click)="grid.reload()" 
+          [disabled]="disableAncestorSelector()"
+          [(ngModel)]="includeOrgAncestors"
+          class="form-check-input" id="include-ancestors">
+        <label class="form-check-label" for="include-ancestors" i18n>+ Ancestors</label>
+      </div>
+      <div class="form-check">
+        <input type="checkbox" (click)="grid.reload()" 
+          [disabled]="disableDescendantSelector()"
+          [(ngModel)]="includeOrgDescendants" 
+          class="form-check-input" id="include-descendants">
+        <label class="form-check-label" for="include-descendants" i18n>+ Descendants</label>
+      </div>
+    </div>
+  </div>
+  <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" 
+    [sortable]="true" persistKey="{{persistKey}}">
+  <eg-grid-toolbar-button [disabled]="!canCreate" 
+    label="New {{idlClassDef.label}}" i18n-label [action]="createNew">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-button [disabled]="translatableFields.length == 0" 
+    label="Apply Translations" i18n-label [action]="translate">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label [action]="deleteSelected">
+  </eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}">
+</eg-fm-record-editor>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts
new file mode 100644 (file)
index 0000000..be4452b
--- /dev/null
@@ -0,0 +1,311 @@
+import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {Pager} from '@eg/share/util/pager';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/**
+ * General purpose CRUD interface for IDL objects
+ *
+ * Object types using this component must be editable via PCRUD.
+ */
+
+@Component({
+    selector: 'eg-admin-page',
+    templateUrl: './admin-page.component.html'
+})
+
+export class AdminPageComponent implements OnInit {
+
+    @Input() idlClass: string;
+
+    // Default sort field, used when no grid sorting is applied.
+    @Input() sortField: string;
+
+    // Data source may be provided by the caller.  This gives the caller
+    // complete control over the contents of the grid.  If no data source
+    // is provided, a generic one is create which is sufficient for data
+    // that requires no special handling, filtering, etc.
+    @Input() dataSource: GridDataSource;
+
+    // Size of create/edito dialog.  Uses large by default.
+    @Input() dialogSize: 'sm' | 'lg' = 'lg';
+
+    // If an org unit field is specified, an org unit filter
+    // is added to the top of the page.
+    @Input() orgField: string;
+
+    // Disable the auto-matic org unit field filter
+    @Input() disableOrgFilter: boolean;
+
+    // Include objects linking to org units which are ancestors
+    // of the selected org unit.
+    @Input() includeOrgAncestors: boolean;
+
+    // Ditto includeOrgAncestors, but descendants.
+    @Input() includeOrgDescendants: boolean;
+
+    // Optional grid persist key.  This is the part of the key
+    // following eg.grid.
+    @Input() persistKey: string;
+
+    // Optional path component to add to the generated grid persist key,
+    // formatted as (for example):
+    // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
+    @Input() persistKeyPfx: string;
+
+    @ViewChild('grid') grid: GridComponent;
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('successString') successString: StringComponent;
+    @ViewChild('createString') createString: StringComponent;
+    @ViewChild('translator') translator: TranslateComponent;
+
+    idlClassDef: any;
+    pkeyField: string;
+    createNew: () => void;
+    deleteSelected: (rows: IdlObject[]) => void;
+
+    // True if any columns on the object support translations
+    translateRowIdx: number;
+    translateFieldIdx: number;
+    translatableFields: string[];
+    translate: () => void;
+
+    contextOrg: IdlObject;
+    orgFieldLabel: string;
+    viewPerms: string;
+    canCreate: boolean;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private perm: PermService,
+        private toast: ToastService
+    ) {
+        this.translatableFields = [];
+    }
+
+    applyOrgValues() {
+
+        if (this.disableOrgFilter) {
+            this.orgField = null;
+            return;
+        }
+
+        if (!this.orgField) {
+            // If no org unit field is specified, try to find one.
+            // If an object type has multiple org unit fields, the
+            // caller should specify one or disable org unit filter.
+            this.idlClassDef.fields.forEach(field => {
+                if (field['class'] === 'aou') {
+                    this.orgField = field.name;
+                }
+            });
+        }
+
+        if (this.orgField) {
+            this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
+            this.contextOrg = this.org.root();
+        }
+    }
+
+    ngOnInit() {
+        this.idlClassDef = this.idl.classes[this.idlClass];
+        this.pkeyField = this.idlClassDef.pkey || 'id';
+
+        this.translatableFields =
+            this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
+
+        if (!this.persistKey) {
+            this.persistKey =
+                'admin.' +
+                (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
+                this.idlClassDef.table;
+        }
+
+        // Limit the view org selector to orgs where the user has
+        // permacrud-encoded view permissions.
+        const pc = this.idlClassDef.permacrud;
+        if (pc && pc.retrieve) {
+            this.viewPerms = pc.retrieve.perms;
+        }
+
+        this.checkCreatePerms();
+        this.applyOrgValues();
+
+        // If the caller provides not data source, create a generic one.
+        if (!this.dataSource) {
+            this.initDataSource();
+        }
+
+        // TODO: pass the row activate handler via the grid markup
+        this.grid.onRowActivate.subscribe(
+            (idlThing: IdlObject) => {
+                this.editDialog.mode = 'update';
+                this.editDialog.recId = idlThing[this.pkeyField]();
+                this.editDialog.open({size: this.dialogSize}).then(
+                    ok => {
+                        this.successString.current()
+                            .then(str => this.toast.success(str));
+                        this.grid.reload();
+                    },
+                    err => {}
+                );
+            }
+        );
+
+        this.createNew = () => {
+            this.editDialog.mode = 'create';
+            this.editDialog.open({size: this.dialogSize}).then(
+                ok => {
+                    this.createString.current()
+                        .then(str => this.toast.success(str));
+                    this.grid.reload();
+                },
+                err => {}
+            );
+        };
+
+        this.deleteSelected = (idlThings: IdlObject[]) => {
+            idlThings.forEach(idlThing => idlThing.isdeleted(true));
+            this.pcrud.autoApply(idlThings).subscribe(
+                val => console.debug('deleted: ' + val),
+                err => {},
+                ()  => this.grid.reload()
+            );
+        };
+
+        // Open the field translation dialog.
+        // Link the next/previous actions to cycle through each translatable
+        // field on each row.
+        this.translate = () => {
+            this.translateRowIdx = 0;
+            this.translateFieldIdx = 0;
+            this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
+            this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
+
+            this.translator.nextString = () => {
+
+                if (this.translateFieldIdx < this.translatableFields.length - 1) {
+                    this.translateFieldIdx++;
+
+                } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
+                    this.translateRowIdx++;
+                    this.translateFieldIdx = 0;
+                }
+
+                this.translator.idlObject =
+                    this.dataSource.data[this.translateRowIdx];
+                this.translator.fieldName =
+                    this.translatableFields[this.translateFieldIdx];
+            };
+
+            this.translator.prevString = () => {
+
+                if (this.translateFieldIdx > 0) {
+                    this.translateFieldIdx--;
+
+                } else if (this.translateRowIdx > 0) {
+                    this.translateRowIdx--;
+                    this.translateFieldIdx = 0;
+                }
+
+                this.translator.idlObject =
+                    this.dataSource.data[this.translateRowIdx];
+                this.translator.fieldName =
+                    this.translatableFields[this.translateFieldIdx];
+            };
+
+            this.translator.open({size: 'lg'});
+        };
+    }
+
+    checkCreatePerms() {
+        this.canCreate = false;
+        const pc = this.idlClassDef.permacrud || {};
+        const perms = pc.create ? pc.create.perms : [];
+        if (perms.length === 0) { return; }
+
+        this.perm.hasWorkPermAt(perms, true).then(permMap => {
+            Object.keys(permMap).forEach(key => {
+                if (permMap[key].length > 0) {
+                    this.canCreate = true;
+                }
+            });
+        });
+    }
+
+    orgOnChange(org: IdlObject) {
+        this.contextOrg = org;
+        this.grid.reload();
+    }
+
+    initDataSource() {
+        this.dataSource = new GridDataSource();
+
+        this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy: any = {};
+
+            if (sort.length) {
+                // Sort specified from grid
+                orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
+
+            } else if (this.sortField) {
+                // Default sort field
+                orderBy[this.idlClass] = this.sortField;
+            }
+
+            const searchOps = {
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            };
+
+            if (this.contextOrg) {
+                // Filter rows by those linking to the context org and
+                // optionally ancestor and descendant org units.
+
+                let orgs = [this.contextOrg.id()];
+
+                if (this.includeOrgAncestors) {
+                    orgs = this.org.ancestors(this.contextOrg, true);
+                }
+
+                if (this.includeOrgDescendants) {
+                    // can result in duplicate workstation org IDs... meh
+                    orgs = orgs.concat(
+                        this.org.descendants(this.contextOrg, true));
+                }
+
+                const search = {};
+                search[this.orgField] = orgs;
+                return this.pcrud.search(this.idlClass, search, searchOps);
+            }
+
+            // No org filter -- fetch all rows
+            return this.pcrud.retrieveAll(this.idlClass, searchOps);
+        };
+    }
+
+    disableAncestorSelector(): boolean {
+        return this.contextOrg &&
+            this.contextOrg.id() === this.org.root().id();
+    }
+
+    disableDescendantSelector(): boolean {
+        return this.contextOrg && this.contextOrg.children().length === 0;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
new file mode 100644 (file)
index 0000000..d49de1b
--- /dev/null
@@ -0,0 +1,70 @@
+
+<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.display.title}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
+          <div class="flex-1">{{summary.display.edition}}</div>
+          <div class="flex-1 font-weight-bold" i18n>TCN:</div>
+          <div class="flex-1">{{summary.record.tcn_value()}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created By:</div>
+          <div class="flex-1" *ngIf="summary.record.creator().usrname">
+            <a href="/eg/staff/circ/patron/{{summary.record.creator().id()}}/checkout">
+              {{summary.record.creator().usrname()}}
+            </a>
+          </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.display.author}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Pubdate:</div>
+          <div class="flex-1">{{summary.display.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.record.editor().usrname">
+            <a href="/eg/staff/circ/patron/{{summary.record.editor().id()}}/checkout">
+              {{summary.record.editor().usrname()}}
+            </a>
+          </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.bibCallNumber}}</div>
+          <div class="flex-1 font-weight-bold" i18n>Record Owner:</div>
+          <div class="flex-1">{{orgName(summary.record.owner())}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created On:</div>
+          <div class="flex-1">{{summary.record.create_date() | date:'short'}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited On:</div>
+          <div class="flex-1">{{summary.record.edit_date() | date:'short'}}</div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
new file mode 100644 (file)
index 0000000..78d2653
--- /dev/null
@@ -0,0 +1,67 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+@Component({
+  selector: 'eg-bib-summary',
+  templateUrl: 'bib-summary.component.html',
+  styles: ['.eg-bib-summary .card-header {padding: .25rem .5rem}']
+})
+export class BibSummaryComponent implements OnInit {
+
+    initDone = false;
+    expandDisplay = true;
+
+    // If provided, the record will be fetched by the component.
+    @Input() recordId: number;
+
+    // Otherwise, we'll use the provided bib summary object.
+    summary: BibRecordSummary;
+    @Input() set bibSummary(s: any) {
+        this.summary = s;
+        if (this.initDone && this.summary) {
+            this.summary.getBibCallNumber();
+        }
+    }
+
+    constructor(
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        if (this.summary) {
+            this.summary.getBibCallNumber();
+        } else {
+            if (this.recordId) {
+                this.loadSummary();
+            }
+        }
+    }
+
+    loadSummary(): void {
+        this.bib.getBibSummary(this.recordId).toPromise()
+        .then(summary => {
+            summary.getBibCallNumber();
+            this.bib.fleshBibUsers([summary.record]);
+            this.summary = summary;
+            console.log(this.summary.display);
+        });
+    }
+
+    orgName(orgId: number): string {
+        if (orgId) {
+            return this.org.get(orgId).shortname();
+        }
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html
new file mode 100644 (file)
index 0000000..f5e4c94
--- /dev/null
@@ -0,0 +1,56 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Add To Record #{{recId}} to Bucket</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-lg-3 font-weight-bold" i18n>Name of existing bucket</div>
+      <div class="col-lg-5">
+         <select 
+          class="form-control"
+          placeholder="Existing Bucket..."
+          i18n-placeholder
+          [(ngModel)]="selectedBucket">
+          <option *ngFor="let bkt of buckets" 
+            value="{{bkt.id()}}">{{bkt.name()}}</option>
+        </select>
+      </div>
+      <div class="col-lg-4">
+        <button class="btn btn-info" (click)="addToSelected()" i18n 
+          [disabled]="!selectedBucket">
+          Add To Selected Bucket
+        </button>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-lg-3 font-weight-bold" i18n>Name of new bucket</div>
+      <div class="col-lg-5">
+        <input type="text" class="form-control" 
+          placeholder="New Bucket Name..."
+          i18n-placeholder
+          [(ngModel)]="newBucketName"/>
+      </div>
+      <div class="col-lg-4">
+        <button class="btn btn-info" (click)="addToNew()" i18n 
+          [disabled]="!newBucketName">
+          Add To New Bucket
+        </button>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-lg-3 font-weight-bold" i18n>New bucket description</div>
+      <div class="col-lg-5">
+        <textarea size="3" type="text" class="form-control" 
+          placeholder="Optional New Bucket Description..."
+          i18n-placeholder
+          [(ngModel)]="newBucketDesc">
+        </textarea>
+      </div>
+    </div>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts
new file mode 100644 (file)
index 0000000..1f127b4
--- /dev/null
@@ -0,0 +1,109 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog for adding bib records to new and existing record buckets.
+ */
+
+@Component({
+  selector: 'eg-record-bucket-dialog',
+  templateUrl: 'record-bucket-dialog.component.html'
+})
+
+export class RecordBucketDialogComponent
+    extends DialogComponent implements OnInit {
+
+    selectedBucket: number;
+    newBucketName: string;
+    newBucketDesc: string;
+    buckets: any[];
+
+    recId: number;
+    @Input() set recordId(id: number) {
+        this.recId = id;
+    }
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private toast: ToastService,
+        private idl: IdlService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {
+
+        this.onOpen$.subscribe(ok => {
+            // Reset data on dialog open
+
+            this.selectedBucket = null;
+            this.newBucketName = '';
+            this.newBucketDesc = '';
+
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.container.retrieve_by_class.authoritative',
+                this.auth.token(), this.auth.user().id(),
+                'biblio', 'staff_client'
+            ).subscribe(buckets => this.buckets = buckets);
+        });
+    }
+
+    addToSelected() {
+        this.addToBucket(this.selectedBucket);
+    }
+
+    // Create a new bucket then add the record
+    addToNew() {
+        const bucket = this.idl.create('cbreb');
+
+        bucket.owner(this.auth.user().id());
+        bucket.name(this.newBucketName);
+        bucket.description(this.newBucketDesc);
+        bucket.btype('staff_client');
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.container.create',
+            this.auth.token(), 'biblio', bucket
+        ).subscribe(bktId => {
+            const evt = this.evt.parse(bktId);
+            if (evt) {
+                this.toast.danger(evt.desc);
+            } else {
+                this.addToBucket(bktId);
+            }
+        });
+    }
+
+    // Add the record to the selected existing bucket
+    addToBucket(id: number) {
+        const item = this.idl.create('cbrebi');
+        item.bucket(id);
+        item.target_biblio_record_entry(this.recId);
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.container.item.create',
+            this.auth.token(), 'biblio', item
+        ).subscribe(resp => {
+            const evt = this.evt.parse(resp);
+            if (evt) {
+                this.toast.danger(evt.toString());
+            } else {
+                this.close();
+            }
+        });
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
new file mode 100644 (file)
index 0000000..d2596b5
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Common code for mananging holdings
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+
+interface NewVolumeData {
+    owner: number;
+    label?: string;
+}
+
+@Injectable()
+export class HoldingsService {
+
+    constructor(private net: NetService) {}
+
+    // Open the holdings editor UI in a new browser window/tab.
+    spawnAddHoldingsUi(
+        recordId: number,                   // Bib record ID
+        addToVols: number[] = [],           // Add copies to existing volumes
+        volumeData: NewVolumeData[] = []) { // Creating new volumes
+
+        const raw: any[] = [];
+
+        if (addToVols) {
+            addToVols.forEach(volId => raw.push({callnumber: volId}));
+        } else if (volumeData) {
+            volumeData.forEach(data => raw.push(data));
+        }
+
+        if (raw.length === 0) { raw.push({}); }
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'edit-these-copies', {
+                record_id: recordId,
+                raw: raw,
+                hide_vols : false,
+                hide_copies : false
+            }
+        ).subscribe(
+            key => {
+                if (!key) {
+                    console.error('Could not create holds cache key!');
+                    return;
+                }
+                setTimeout(() => {
+                    const url = `/eg/staff/cat/volcopy/${key}`;
+                    window.open(url, '_blank');
+                });
+            }
+        );
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html
new file mode 100644 (file)
index 0000000..0d82279
--- /dev/null
@@ -0,0 +1,22 @@
+
+<div class="d-flex border-top border-light" 
+    *ngFor="let row of rowBuckets; let rowIdx = index">
+  <div class="flex-1 p-2" *ngFor="let col of colList">
+    <ng-container *ngIf="row[col]">
+      <!-- avoid mixing [href] and [routerLink] in one link, 
+          because routerLink will take precedence, even if it's empty -->
+      <ng-container *ngIf="row[col].url">
+        <a [href]="row[col].url" class="with-material-icon">
+          <span class="material-icons">edit</span>
+          <span>{{row[col].label}}</span>
+        </a>
+      </ng-container>
+      <ng-container *ngIf="row[col].routerLink">
+        <a [routerLink]="row[col].routerLink" class="with-material-icon">
+          <span class="material-icons">edit</span>
+          <span>{{row[col].label}}</span>
+        </a>
+      </ng-container>
+    </ng-container>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts
new file mode 100644 (file)
index 0000000..9b06c92
--- /dev/null
@@ -0,0 +1,73 @@
+import {Component, Input, OnInit, AfterViewInit, Host} from '@angular/core';
+
+interface LinkTableLink {
+    label: string;
+    url?: string;
+    routerLink?: string;
+}
+
+@Component({
+    selector: 'eg-link-table',
+    templateUrl: './link-table.component.html'
+})
+
+export class LinkTableComponent implements AfterViewInit {
+    @Input() columnCount: number;
+    links: LinkTableLink[];
+    rowBuckets: any[];
+    colList: number[];
+    colWidth: number;
+
+    constructor() {
+        this.links = [];
+        this.rowBuckets = [];
+        this.colList = [];
+    }
+
+    ngAfterViewInit() {
+        // table-ize the links
+        const rowCount = Math.ceil(this.links.length / this.columnCount);
+        this.colWidth = Math.floor(12 / this.columnCount); // Bootstrap 12-grid
+
+        for (let col = 0; col < this.columnCount; col++) {
+            this.colList.push(col);
+        }
+
+        // Modifying values in AfterViewInit without other activity
+        // happening can result in the modified values not getting
+        // displayed until some action occurs.  Modifing after
+        // via timeout works though.
+        setTimeout(() => {
+            for (let row = 0; row < rowCount; row++) {
+                this.rowBuckets[row] = [
+                    this.links[row],
+                    this.links[row + Number(rowCount)],
+                    this.links[row + Number(rowCount * 2)]
+                ];
+            }
+        });
+    }
+}
+
+@Component({
+    selector: 'eg-link-table-link',
+    template: '<ng-template></ng-template>'
+})
+
+export class LinkTableLinkComponent implements OnInit {
+    @Input() label: string;
+    @Input() url: string;
+    @Input() routerLink: string;
+
+    constructor(@Host() private linkTable: LinkTableComponent) {}
+
+    ngOnInit() {
+        this.linkTable.links.push({
+            label : this.label,
+            url: this.url,
+            routerLink: this.routerLink
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html
new file mode 100644 (file)
index 0000000..e5a6f49
--- /dev/null
@@ -0,0 +1,65 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Change Operator</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <form class="form-validated">
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" for="username" i18n>Username</label>
+        <input 
+          type="text" 
+          class="form-control col-lg-7"
+          id="username" 
+          name="username"
+          required
+          (keyup.enter)="login()"
+          autocomplete="username"
+          i18n-placeholder
+          placeholder="Username..." 
+          [(ngModel)]="username"/>
+      </div>
+
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" 
+            for="password" i18n>Password</label>
+        <input 
+          type="password" 
+          class="form-control col-lg-7"
+          id="password" 
+          name="password"
+          required
+          (keyup.enter)="login()"
+          autocomplete="current-password"
+          i18n-placeholder
+          placeholder="Password..." 
+          [(ngModel)]="password"/>
+      </div>
+
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" 
+            for="loginType" i18n>Login Type</label>
+        <select 
+          class="form-control col-lg-7" 
+          id="loginType" 
+          name="loginType"
+          placeholder="Login Type..."
+          i18n-placeholder
+          required
+          [(ngModel)]="loginType">
+          <option value="temp" selected i18n>Temporary</option>                   
+          <option value="staff" i18n>Staff</option>             
+          <option value="persist" i18n>Persistent</option>      
+        </select>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button (click)="login()" class="btn btn-info" i18n>OK/Continue</button>
+    <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts
new file mode 100644 (file)
index 0000000..95d4db8
--- /dev/null
@@ -0,0 +1,77 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'eg-op-change',
+  templateUrl: 'op-change.component.html'
+})
+
+export class OpChangeComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() username: string;
+    @Input() password: string;
+    @Input() loginType = 'temp';
+
+    @Input() successMessage: string;
+    @Input() failMessage: string;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private toast: ToastService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+
+        // Focus the username any time the dialog is opened.
+        this.onOpen$.subscribe(
+            val => this.renderer.selectRootElement('#username').focus()
+        );
+    }
+
+    login(): Promise<any> {
+        if (!(this.username && this.password)) {
+            return Promise.reject('Missing Params');
+        }
+
+        return this.auth.login(
+            {   username    : this.username,
+                password    : this.password,
+                workstation : this.auth.workstation(),
+                type        : this.loginType
+            },  true        // isOpChange
+        ).then(
+            ok => {
+                this.password = '';
+                this.username = '';
+
+                // Fetch the user object
+                this.auth.testAuthToken().then(
+                    ok2 => {
+                        this.close();
+                        this.toast.success(this.successMessage);
+                    }
+                );
+            },
+            notOk => {
+                this.password = '';
+                this.toast.danger(this.failMessage);
+            }
+        );
+    }
+
+    restore(): Promise<any> {
+        return this.auth.undoOpChange().then(
+            ok => this.toast.success(this.successMessage),
+            err => this.toast.danger(this.failMessage)
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts
new file mode 100644 (file)
index 0000000..13ac684
--- /dev/null
@@ -0,0 +1,15 @@
+import {Component, OnInit, Input} from '@angular/core';
+
+@Component({
+  selector: 'eg-staff-banner',
+  template:
+    '<div class="lead alert alert-primary text-center pt-1 pb-1" role="alert">' +
+      '<span>{{bannerText}}</span>' +
+    '</div>'
+})
+
+export class StaffBannerComponent {
+    @Input() public bannerText: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html
new file mode 100644 (file)
index 0000000..7aa59b4
--- /dev/null
@@ -0,0 +1,63 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>
+      {{idlClassDef.label}}
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body form-common form-validated" *ngIf="idlObj">
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Field Name</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlClassDef.field_map[field].label}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Current Value</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlObj[field]()}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Select Locale</label>
+      <select class="form-control col-lg-7" 
+        (change)="localeChanged($event)"
+        [(ngModel)]="selectedLocale">
+        <option value="{{locale.code()}}" *ngFor="let locale of locales">
+          {{locale.name()}}
+        </option>
+      </select>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" i18n>Translation</label>
+      <input 
+        id='translation-input'
+        type="text" 
+        class="form-control col-lg-7"
+        required
+        i18n-placeholder
+        (keyup.enter)="translate()"
+        placeholder="Translation..." 
+        [(ngModel)]="translatedValue"/>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button *ngIf="prevString" (click)="prevString()" 
+      class="btn btn-info" i18n>Prev String</button>
+    <button *ngIf="nextString" (click)="nextString()" 
+      class="btn btn-info mr-3" i18n>Next String</button>
+    <button (click)="translate()" class="btn btn-info" i18n>Apply</button>
+    <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts
new file mode 100644 (file)
index 0000000..9c7361c
--- /dev/null
@@ -0,0 +1,145 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'eg-translate',
+  templateUrl: 'translate.component.html'
+})
+
+export class TranslateComponent
+    extends DialogComponent implements OnInit {
+
+    idlClassDef: any;
+    locales: IdlObject[];
+    selectedLocale: string;
+    translatedValue: string;
+    existingTranslation: IdlObject;
+
+    // These actions should update the idlObject and/or fieldName values,
+    // forcing the dialog to load a new string to translate.  When set,
+    // applying a translation in the dialog will leave the dialog window open
+    // so the next/prev buttons can be used to fetch the next string.
+    nextString: () => void;
+    prevString: () => void;
+
+    idlObj: IdlObject;
+    @Input() set idlObject(o: IdlObject) {
+        if (o) {
+            this.idlObj = o;
+            this.idlClassDef = this.idl.classes[o.classname];
+            this.fetchTranslation();
+        }
+    }
+
+    field: string;
+    @Input() set fieldName(n: string) {
+        this.field = n;
+    }
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private idl: IdlService,
+        private toast: ToastService,
+        private locale: LocaleService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        // Default to the login locale
+        this.selectedLocale = this.locale.currentLocaleCode();
+        this.locales = [];
+        this.locale.supportedLocales().subscribe(l => this.locales.push(l));
+
+        this.onOpen$.subscribe(() => {
+            const elm = this.renderer.selectRootElement('#translation-input');
+            if (elm) {
+                elm.focus();
+                elm.select();
+            }
+        });
+    }
+
+    localeChanged(code: string) {
+        this.fetchTranslation();
+    }
+
+    fetchTranslation() {
+        const exist = this.existingTranslation;
+
+        if (exist
+            && exist.fq_field() === this.fqField()
+            && exist.identity_value() === this.identValue()) {
+            // Already have the current translation object.
+            return;
+        }
+
+        this.translatedValue = '';
+        this.existingTranslation = null;
+
+        this.pcrud.search('i18n', {
+            translation: this.selectedLocale,
+            fq_field : this.fqField(),
+            identity_value: this.identValue()
+        }).subscribe(tr => {
+            this.existingTranslation = tr;
+            this.translatedValue = tr.string();
+            console.debug('found existing translation ', tr);
+        });
+    }
+
+    fqField(): string {
+        return this.idlClassDef.classname + '.' + this.field;
+    }
+
+    identValue(): string {
+        return this.idlObj[this.idlClassDef.pkey || 'id']();
+    }
+
+    translate() {
+        if (!this.translatedValue) { return; }
+
+        let entry;
+
+        if (this.existingTranslation) {
+            entry = this.existingTranslation;
+            entry.string(this.translatedValue);
+
+            this.pcrud.update(entry).toPromise().then(
+                ok => {
+                    if (!this.nextString) {
+                        this.close('Translation updated');
+                    }
+                },
+                err => console.error(err)
+            );
+
+            return;
+        }
+
+        entry = this.idl.create('i18n');
+        entry.fq_field(this.fqField());
+        entry.identity_value(this.identValue());
+        entry.translation(this.selectedLocale);
+        entry.string(this.translatedValue);
+
+        this.pcrud.create(entry).toPromise().then(
+            ok => {
+                if (!this.nextString) {
+                    this.close('Translation created');
+                }
+            },
+            err => console.error('Translation creation failed')
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/splash.component.html b/Open-ILS/src/eg2/src/app/staff/splash.component.html
new file mode 100644 (file)
index 0000000..b37bec3
--- /dev/null
@@ -0,0 +1,128 @@
+
+
+<style>
+    /* TODO change BS color scheme so this isn't necessary */
+    .bg-evergreen {
+      background: -webkit-linear-gradient(#00593d, #007a54);
+      background-color: #007a54;
+      color: #fff;
+    }
+
+    /* Match the ang1 splash page */
+    .card-header {
+        color: #3c763d;
+        background-color: #dff0d8;
+        border-color: #d6e9c6;
+    }
+</style>
+
+<div class="container">
+
+  <!-- header icon -->
+  <div class="row mb-3">
+    <div class="col-lg-12 text-center">
+      <img src="/images/portal/logo.png"/>
+    </div>
+  </div>
+
+  <div class="row" id="splash-nav">
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>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" i18n>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" i18n>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" i18n>Search For Patron By Name</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>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.enter)="searchCatalog()"
+                  i18n-placeholder placeholder="Search for...">
+                <span class="input-group-btn">
+                  <button class="btn btn-outline-secondary" 
+                    (click)="searchCatalog()" type="button" i18n>
+                    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/" i18n>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/" i18n>Copy Buckets</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>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/" i18n>
+                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" i18n>
+                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" i18n>
+                Reports
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/splash.component.ts b/Open-ILS/src/eg2/src/app/staff/splash.component.ts
new file mode 100644 (file)
index 0000000..af6b647
--- /dev/null
@@ -0,0 +1,40 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {Router} from '@angular/router';
+
+@Component({
+    templateUrl: 'splash.component.html'
+})
+
+export class StaffSplashComponent implements OnInit {
+
+    catSearchQuery: string;
+
+    constructor(
+        private renderer: Renderer2,
+        private router: Router
+    ) {}
+
+    ngOnInit() {
+
+        // Focus catalog search form
+        this.renderer.selectRootElement('#catalog-search-input').focus();
+    }
+
+    searchCatalog(): void {
+        if (!this.catSearchQuery) { return; }
+
+        /* Route to angular6 catalog
+        this.router.navigate(
+            ['/staff/catalog/search'],
+            {queryParams: {query : this.catSearchQuery}}
+        );
+        */
+
+        // Route to AngularJS / TPAC catalog
+        window.location.href =
+            '/eg/staff/cat/catalog/results?query=' +
+            encodeURIComponent(this.catSearchQuery);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.css b/Open-ILS/src/eg2/src/app/staff/staff.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/src/eg2/src/app/staff/staff.component.html b/Open-ILS/src/eg2/src/app/staff/staff.component.html
new file mode 100644 (file)
index 0000000..2a2539c
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- top navigation bar -->
+<eg-staff-nav-bar></eg-staff-nav-bar>
+
+<div id='staff-content-container'>
+  <!-- page content -->
+  <router-outlet></router-outlet>
+</div>
+
+<!-- EgAccessKey Info Panel -->
+<eg-accesskey-info #egAccessKeyInfo></eg-accesskey-info>
+<a egAccessKey keyCtx="base"
+    keySpec="ctrl+h" i18n-keySpec
+    keyDesc="Display AccessKey Info Dialog" i18n-keyDesc
+    (click)="egAccessKeyInfo.open()">
+</a>
+
+<!-- global toast alerts -->
+<eg-toast></eg-toast>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.ts b/Open-ILS/src/eg2/src/app/staff/staff.component.ts
new file mode 100644 (file)
index 0000000..492b1df
--- /dev/null
@@ -0,0 +1,118 @@
+import {Component, OnInit, NgZone, HostListener} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
+
+const LOGIN_PATH = '/staff/login';
+const WS_BASE_PATH = '/staff/admin/workstation/workstations/';
+const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
+
+@Component({
+  templateUrl: 'staff.component.html',
+  styleUrls: ['staff.component.css']
+})
+
+export class StaffComponent implements OnInit {
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private zone: NgZone,
+        private net: NetService,
+        private auth: AuthService,
+        private keys: AccessKeyService
+    ) {}
+
+    ngOnInit() {
+
+        // Fires on all in-staff-app router navigation, but not initial
+        // page load.
+        this.router.events.subscribe(routeEvent => {
+            if (routeEvent instanceof NavigationEnd) {
+                // console.debug(`StaffComponent routing to ${routeEvent.url}`);
+                this.preventForbiddenNavigation(routeEvent.url);
+            }
+        });
+
+        // Redirect to the login page on any auth timeout events.
+        this.net.authExpired$.subscribe(expireEvent => {
+
+            // If the expired authtoken was identified locally (i.e.
+            // in this browser tab) notify all tabs of imminent logout.
+            if (!expireEvent.viaExternal) {
+                this.auth.broadcastLogout();
+            }
+
+            console.debug('Auth session has expired. Redirecting to login');
+            this.auth.redirectUrl = this.router.url;
+
+            // https://github.com/angular/angular/issues/18254
+            // When a tab redirects to a login page as a result of
+            // another tab broadcasting a logout, ngOnInit() fails to
+            // fire in the login component, until the user interacts
+            // with the page.  Fix it by wrapping it in zone.run().
+            // This is the only navigate() where I have seen this happen.
+            this.zone.run(() => {
+                this.router.navigate([LOGIN_PATH]);
+            });
+        });
+
+        this.route.data.subscribe((data: {staffResolver: any}) => {
+            // Data fetched via StaffResolver is available here.
+        });
+
+
+    }
+
+    /**
+     * Prevent the user from leaving the login page when they don't have
+     * a valid authoken.
+     *
+     * Prevent the user from leaving the workstation admin page when
+     * they don't have a valid workstation.
+     *
+     * This does not verify auth tokens with the server on every route,
+     * because that would be overkill.  This is only here to keep
+     * people boxed in with their authenication state was already
+     * known to be less then 100%.
+     */
+    preventForbiddenNavigation(url: string): void {
+
+        // No auth checks needed for login page.
+        if (url.startsWith(LOGIN_PATH)) {
+            return;
+        }
+
+        // We lost our authtoken, go back to the login page.
+        if (!this.auth.token()) {
+            this.router.navigate([LOGIN_PATH]);
+        }
+
+        // No workstation checks needed for workstation admin page.
+        if (url.startsWith(WS_BASE_PATH)) {
+            return;
+        }
+
+        if (this.auth.workstationState !== AuthWsState.VALID) {
+            this.router.navigate([WS_MANAGE_PATH]);
+        }
+    }
+
+    /**
+     * Listen for keyboard events here -- the root directive --  and pass
+     * events down to the key service for processing.
+     */
+    @HostListener('window:keydown', ['$event']) onKeyDown(evt: KeyboardEvent) {
+        this.keys.fire(evt);
+    }
+
+    /*
+    @ViewChild('egAccessKeyInfo')
+    private keyComponent: AccessKeyInfoComponent;
+    */
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.module.ts b/Open-ILS/src/eg2/src/app/staff/staff.module.ts
new file mode 100644 (file)
index 0000000..dd22f93
--- /dev/null
@@ -0,0 +1,26 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+
+import {StaffComponent} from './staff.component';
+import {StaffRoutingModule} from './routing.module';
+import {StaffNavComponent} from './nav.component';
+import {StaffLoginComponent} from './login.component';
+import {StaffSplashComponent} from './splash.component';
+import {AboutComponent} from './about.component';
+
+@NgModule({
+  declarations: [
+    StaffComponent,
+    StaffNavComponent,
+    StaffSplashComponent,
+    StaffLoginComponent,
+    AboutComponent
+  ],
+  imports: [
+    StaffCommonModule.forRoot(),
+    StaffRoutingModule
+  ]
+})
+
+export class StaffModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/welcome.component.html b/Open-ILS/src/eg2/src/app/welcome.component.html
new file mode 100644 (file)
index 0000000..eaa1c71
--- /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>
+  </p>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/welcome.component.ts b/Open-ILS/src/eg2/src/app/welcome.component.ts
new file mode 100644 (file)
index 0000000..a588661
--- /dev/null
@@ -0,0 +1,13 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  templateUrl : './welcome.component.html'
+})
+
+export class WelcomeComponent implements OnInit {
+    ngOnInit() {
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/assets/.gitkeep b/Open-ILS/src/eg2/src/assets/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/environments/environment.prod.ts b/Open-ILS/src/eg2/src/environments/environment.prod.ts
new file mode 100644 (file)
index 0000000..50385bf
--- /dev/null
@@ -0,0 +1,4 @@
+export const environment = {
+  production: true,
+  locales: ['en-US', 'fr-CA']
+};
diff --git a/Open-ILS/src/eg2/src/environments/environment.ts b/Open-ILS/src/eg2/src/environments/environment.ts
new file mode 100644 (file)
index 0000000..113cbe3
--- /dev/null
@@ -0,0 +1,10 @@
+// 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,
+  // currently locales are only supported in production builds.
+  locales: ['en-US']
+};
diff --git a/Open-ILS/src/eg2/src/favicon.ico b/Open-ILS/src/eg2/src/favicon.ico
new file mode 100644 (file)
index 0000000..8081c7c
Binary files /dev/null and b/Open-ILS/src/eg2/src/favicon.ico differ
diff --git a/Open-ILS/src/eg2/src/index.html b/Open-ILS/src/eg2/src/index.html
new file mode 100644 (file)
index 0000000..aee6bf8
--- /dev/null
@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title i18n="Page Title">AngEG</title>
+  <base href="/eg2">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <!-- see notes in styles.css regarding locally served fonts -->
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+</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/src/eg2/src/locale/.gitkeep b/Open-ILS/src/eg2/src/locale/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/main.ts b/Open-ILS/src/eg2/src/main.ts
new file mode 100644 (file)
index 0000000..2e303cf
--- /dev/null
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { BaseModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+  enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(BaseModule)
+  .catch(err => console.log(err));
diff --git a/Open-ILS/src/eg2/src/polyfills.ts b/Open-ILS/src/eg2/src/polyfills.ts
new file mode 100644 (file)
index 0000000..e073082
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * 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';
+
+// PhantomJS needs these
+import 'core-js/es6/array';
+import 'core-js/es6/string';
+
+/** 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/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css
new file mode 100644 (file)
index 0000000..b87ad78
--- /dev/null
@@ -0,0 +1,161 @@
+/* You can add global styles to this file, and also import other style files */
+
+/* bootstrap CSS only -- JS bits come from ng-bootstrap */
+@import '~bootstrap-css-only/css/bootstrap.min.css';
+
+/* Locally served material icon fonts.
+ * Note when I first tested these after installing the fonts
+ * via:  npm install --save material-design-icons
+ * some of the icons exhibited odd behavior, adding a lot of
+ * excess space to the left or right.  It only affected certain
+ * icons.  More research needed.
+ * /
+/*
+@import '~material-design-icons/iconfont/material-icons.css'; 
+*/
+
+/** BS default fonts are huge */
+body, .form-control, .btn, .input-group-text {
+  /* 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.
+ *  BS class="col" is roughly equivelent to flex-1, but col-2 is not
+ *  equivalent to flex-2, since col-2 really means 2/12 width. */
+.flex-1 {flex: 1}
+.flex-2 {flex: 2}
+.flex-3 {flex: 3}
+.flex-4 {flex: 4}
+.flex-5 {flex: 5}
+
+
+/* usefuf 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 */
+}
+
+.mat-icon-in-button {
+    line-height: inherit;
+}
+
+.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;
+}
+
+/* dropdown menu link/button with no downward carrot icon */
+.no-dropdown-caret::after {
+    display: none;
+}
+
+/* Default .card padding is extreme */
+.tight-card .card-body,
+.tight-card .list-group-item {
+  padding: .25rem;
+}
+.tight-card .card-header {
+  padding: .5rem;
+}
+
+@media all and (min-width: 800px) {                                            
+    /* scrollable typeahead menus for full-size screens */                               
+    ngb-typeahead-window {
+        height: auto;                                                          
+        max-height: 200px;                                                     
+        overflow-x: hidden;                                                    
+    }
+}
+
+/* --------------------------------------------------------------------------
+/* Form Validation CSS - https://angular.io/guide/form-validation
+ * TODO: these colors don't fit the EG color scheme
+ * Required valid fields are left-border styled in green-ish.
+ * Invalid fields are left-border styled in red-ish.
+ */
+.form-validated .ng-valid[required], .form-validated .ng-valid.required {
+  border-left: 5px solid #78FA89;
+}
+.form-validated .ng-invalid:not(form) {
+  border-left: 5px solid #FA787E;
+}
+
+/* Typical form CSS.
+ * Brings font size down 5% to squeeze a bit more in.
+ * Bold labels
+ * Fixes some bootstrap margin funkiness with checkboxes for
+ * better vertical alignment.
+ * Optional faint odd or even row striping.
+ */
+.common-form {
+  font-size: 95%;
+}
+.common-form .row {
+  margin: 5px;
+  padding: 3px;
+}
+
+.common-form label {
+  font-weight: bold;
+}
+.common-form input[type="checkbox"] {
+  /* BS adds a negative left margin */
+  margin-left: 0px;
+}
+.common-form.striped-even .row:nth-child(even) {
+  background-color: rgba(0,0,0,.03);
+  border-top: 1px solid rgba(0,0,0,.125);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+.common-form.striped-odd .row:nth-child(odd) {
+  background-color: rgba(0,0,0,.03);
+  border-top: 1px solid rgba(0,0,0,.125);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+
+/**
+ * Only display the print container when printing 
+ */
+#eg-print-container {
+    display: none;
+}
+@media print {
+  head {display: none} /* just to be safe */
+  body div:not([id="eg-print-container"]) {display: none}
+  div {display: none}
+  #eg-print-container {display: block}
+  #eg-print-container div {display: block}
+  #eg-print-container pre {border: none}
+}
+
diff --git a/Open-ILS/src/eg2/src/test.ts b/Open-ILS/src/eg2/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/src/eg2/src/test_data/eg_mock.js b/Open-ILS/src/eg2/src/test_data/eg_mock.js
new file mode 100644 (file)
index 0000000..3db3579
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Mock data required by multiple unit tests.
+ */
+
+window._eg_mock_data = {
+
+    // builds a mock org unit tree fleshed with ou_types and
+    // absorbs the tree into egEnv
+    generateOrgTree : function(idlService, orgService) {
+        var type1 = idlService.create('aout');
+        type1.id(1);
+        type1.depth(0);
+
+        var type2 = idlService.create('aout');
+        type2.id(2);
+        type2.depth(1);
+        type2.parent(1);
+
+        var type3 = idlService.create('aout');
+        type3.id(3);
+        type3.depth(2);
+        type3.parent(2);
+
+        var org1 = idlService.create('aou'); 
+        org1.id(1);
+        org1.ou_type(type1);
+        org1.shortname('ROOT');
+
+        var org2 = idlService.create('aou'); 
+        org2.id(2); 
+        org2.parent_ou(1);
+        org2.ou_type(type2);
+
+        var org3 = idlService.create('aou'); 
+        org3.id(3); 
+        org3.parent_ou(1);
+        org3.ou_type(type2);
+
+        var org4 = idlService.create('aou'); 
+        org4.id(4); 
+        org4.parent_ou(2);
+        org4.ou_type(type3);
+
+        org1.children([org2, org3]);
+        org2.children([org4]);
+        org3.children([]);
+        org4.children([]);
+
+        orgService.orgTree = org1;
+        orgService.absorbTree();
+    }
+}
diff --git a/Open-ILS/src/eg2/src/test_data/idl2js.pl b/Open-ILS/src/eg2/src/test_data/idl2js.pl
new file mode 100644 (file)
index 0000000..fa3f2a4
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+use strict; use warnings;
+use XML::LibXML;
+use XML::LibXSLT;
+my $out_file = 'IDL2js.js';
+my $idl_file = '../../../../examples/fm_IDL.xml';
+my $xsl_file = '../../../../xsl/fm_IDL2js.xsl'; 
+
+my $xslt = XML::LibXSLT->new();
+my $style_doc = XML::LibXML->load_xml(location => $xsl_file, no_cdata=>1);
+my $stylesheet = $xslt->parse_stylesheet($style_doc);
+my $idl_string = preprocess_idl_file($idl_file);
+my $idl_doc = XML::LibXML->load_xml(string => $idl_string);
+my $results = $stylesheet->transform($idl_doc);
+my $output = $stylesheet->output_as_bytes($results);
+
+open(IDL, ">$out_file") or die "Cannot open IDL2js file $out_file : $!\n";
+
+print IDL $output;
+
+close(IDL);
+
+
+sub preprocess_idl_file {
+       my $file = shift;
+       open my $idl_fh, '<', $file or die "Unable to open IDL file $file : $!\n";
+       local $/ = undef;
+       my $xml = <$idl_fh>;
+       close($idl_fh);
+       # These substitutions are taken from OpenILS::WWW::IDL2js
+       $xml =~ s/<!--.*?-->//sg;     # filter out XML comments ...
+       $xml =~ s/(?:^|\s+)--.*$//mg; # and SQL comments ...
+       $xml =~ s/^\s+/ /mg;          # and extra leading spaces ...
+       $xml =~ s/\R*//g;             # and newlines
+       return $xml;
+}
diff --git a/Open-ILS/src/eg2/src/tsconfig.app.json b/Open-ILS/src/eg2/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/src/eg2/src/tsconfig.spec.json b/Open-ILS/src/eg2/src/tsconfig.spec.json
new file mode 100644 (file)
index 0000000..18bad40
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/spec",
+    "baseUrl": "./",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "node"
+    ]
+  },
+  "files": [
+    "test.ts",
+    "polyfills.ts"
+  ],
+  "include": [
+    "**/*.spec.ts",
+    "**/*.d.ts"
+  ]
+}
diff --git a/Open-ILS/src/eg2/src/typings.d.ts b/Open-ILS/src/eg2/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/src/eg2/tsconfig.json b/Open-ILS/src/eg2/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/src/eg2/tslint.json b/Open-ILS/src/eg2/tslint.json
new file mode 100644 (file)
index 0000000..e8fbb40
--- /dev/null
@@ -0,0 +1,136 @@
+{
+  "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/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,
+      "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"
+      }
+    ],
+    "unified-signatures": true,
+    "variable-name": false,
+    "whitespace": [
+      true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type"
+    ],
+    "directive-selector": [
+      true,
+      "attribute",
+      "eg",
+      "camelCase"
+    ],
+    "component-selector": [
+      true,
+      "element",
+      "eg",
+      "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
+  }
+}