LP#1626157 staff login perm; tidying
authorBill Erickson <berickxx@gmail.com>
Fri, 20 Apr 2018 19:57:24 +0000 (15:57 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 20 Apr 2018 19:57:24 +0000 (15:57 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
19 files changed:
Open-ILS/src/eg2/src/app/common.module.ts
Open-ILS/src/eg2/src/app/core/perm.service.ts
Open-ILS/src/eg2/src/app/migration.module.ts
Open-ILS/src/eg2/src/app/share/README
Open-ILS/src/eg2/src/app/share/audio.service.ts [deleted file]
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/share/hello-world.component.ts [deleted file]
Open-ILS/src/eg2/src/app/share/org-select.component.html [deleted file]
Open-ILS/src/eg2/src/app/share/org-select.component.ts [deleted file]
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/unapi.service.ts [deleted file]
Open-ILS/src/eg2/src/app/share/util/audio.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/resolver.service.ts
Open-ILS/src/templates/staff/circ/checkin/t_checkin.tt2

index 4e2021d..8cc94c7 100644 (file)
@@ -14,7 +14,7 @@ import {EgAuthService} from '@eg/core/auth.service';
 import {EgPermService} from '@eg/core/perm.service';
 import {EgPcrudService} from '@eg/core/pcrud.service';
 import {EgOrgService} from '@eg/core/org.service';
-import {EgAudioService} from '@eg/share/audio.service';
+import {EgAudioService} from '@eg/share/util/audio.service';
 
 @NgModule({
   declarations: [
index a2c439b..2e535d1 100644 (file)
@@ -4,7 +4,7 @@ import {EgOrgService} from './org.service';
 import {EgAuthService} from './auth.service';
 
 interface HasPermAtResult {
-    [permName: string]: number[];
+    [permName: string]: any[]; // org IDs or org unit objects
 }
 
 interface HasPermHereResult {
index 88c84ae..5c878b5 100644 (file)
@@ -33,7 +33,6 @@ import {EgOrgService} from '@eg/core/org.service';
 // Downgraded components
 //import {EgDialogComponent} from '@eg/share/dialog/dialog.component';
 //import {EgConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
-import {EgHelloWorldComponent} from '@eg/share/hello-world.component';
 
 declare var angular: any;
 
@@ -46,12 +45,10 @@ declare var angular: any;
     EgCommonModule.forRoot()
   ],
   declarations: [
-    EgHelloWorldComponent,
     //EgDialogComponent,
     //EgConfirmDialogComponent
   ],
   entryComponents: [
-    EgHelloWorldComponent,
     //EgDialogComponent,
     //EgConfirmDialogComponent
   ]
@@ -81,8 +78,6 @@ export class EgMigrationModule {
             .factory('eg2Pcrud', downgradeInjectable(EgPcrudService))
             .factory('eg2Org',   downgradeInjectable(EgOrgService))
             .factory('ng2Title', downgradeInjectable(Title))
-            .directive('eg2HelloWorld',
-                downgradeComponent({component: EgHelloWorldComponent}))
             /*
             .directive('eg2ConfirmDialog',
                 downgradeComponent({component: EgConfirmDialogComponent}))
index 1a8b6e1..9da6f8a 100644 (file)
@@ -1,7 +1,5 @@
-Common Angular services and associated types/classes.  
-
-This collection of services MIGHT be used by practically all applications.
-They are NOT automatically imported/exported by the base module and should
-be loaded within the requesting application as needed.
+Shared Angular services, components, directives, and associated classes.  
 
+These items are NOT automatically imported to the base module.   Import
+as needed.
 
diff --git a/Open-ILS/src/eg2/src/app/share/audio.service.ts b/Open-ILS/src/eg2/src/app/share/audio.service.ts
deleted file mode 100644 (file)
index 971fe7e..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * 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 {EgStoreService} from '@eg/core/store.service';
-const AUDIO_BASE_URL = '/audio/notifications/';
-
-@Injectable()
-export class EgAudioService {
-
-    // map of requested audio path to resolved path
-    private urlCache: {[path:string] : string} = {};
-
-    constructor(private store: EgStoreService) {}
-
-    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;
-        
-            let url = this.urlCache[path] || 
-                AUDIO_BASE_URL + path.replace(/\./g, '/') + '.wav';
-
-            let 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);
-            }
-        });
-    }
-}
-
-
index f59c6c1..8b0483f 100644 (file)
@@ -1,6 +1,6 @@
 import {Injectable} from '@angular/core';
 import {EgOrgService} from '@eg/core/org.service';
-import {EgUnapiService} from '@eg/share/unapi.service';
+import {EgUnapiService} from '@eg/share/catalog/unapi.service';
 import {EgIdlObject} from '@eg/core/idl.service';
 import {EgNetService} from '@eg/core/net.service';
 import {EgPcrudService} from '@eg/core/pcrud.service';
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..9034ae4
--- /dev/null
@@ -0,0 +1,54 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {EgOrgService} 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 EgUnapiParams {
+    target: string; // bre, ...
+    id: number | string; // 1 | 1,2,3,4,5
+    extras: string; // {holdings_xml,mra,...}
+    format: string; // mods32, marxml, ...
+    orgId?: number; // org unit ID
+    depth?: number; // org unit depth
+};
+
+@Injectable()
+export class EgUnapiService {
+
+    constructor(private org: EgOrgService) {}
+
+    createUrl(params: EgUnapiParams): string {
+        let depth = params.depth || 0;
+        let org = params.orgId ? this.org.get(params.orgId) : this.org.root();
+
+        return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` +
+            `${org.shortname()}/${depth}&format=${params.format}`;
+    }
+
+    getAsXmlDocument(params: EgUnapiParams): Promise<XMLDocument> {
+        // XReq creates an XML document for us.  Seems like the right
+        // tool for the job.
+        let url = this.createUrl(params);
+        return new Promise((resolve, reject) => {
+            var xhttp = new XMLHttpRequest();
+            xhttp.onreadystatechange = function() {
+                if (this.readyState == 4) {
+                    if (this.status == 200) {
+                        resolve(xhttp.responseXML);
+                    } else {
+                        reject(`UNAPI request failed for ${url}`);
+                    }
+                }
+            }
+            xhttp.open("GET", url, true);
+            xhttp.send();
+        });
+    }
+}
+
+
index 3ba94a3..5e17f6f 100644 (file)
@@ -12,7 +12,7 @@ interface CustomFieldTemplate {
     // 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
+    context?: {[fields: string]: any}
 }
 
 interface CustomFieldContext {
@@ -268,7 +268,7 @@ export class FmRecordEditorComponent
 
     // Returns a context object to be inserted into a custom 
     // field template.
-    customTemplateFieldContext(fieldDef: any): FmEditorCustomFieldContext {
+    customTemplateFieldContext(fieldDef: any): CustomFieldContext {
         return Object.assign(
             {   record : this.record,
                 field: fieldDef // from this.fields
diff --git a/Open-ILS/src/eg2/src/app/share/hello-world.component.ts b/Open-ILS/src/eg2/src/app/share/hello-world.component.ts
deleted file mode 100644 (file)
index 92daf1c..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-import {Component, Input} from '@angular/core';
-
-@Component({
-  selector: 'eg-hello-world',
-  template: `
-    <div>Hello, World {{message}}!</div>
-    `
-})
-export class EgHelloWorldComponent {
-    @Input() public message: string;
-    constructor() {}
-}
-
-
diff --git a/Open-ILS/src/eg2/src/app/share/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select.component.html
deleted file mode 100644 (file)
index 2a4bd3a..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-
-<!-- todo disabled -->
-<ng-template #displayTemplate let-r="result">
-{{r.label}}
-</ng-template>
-
-<input type="text" 
-  class="form-control"
-  [placeholder]="placeholder"
-  [(ngModel)]="selected" 
-  [ngbTypeahead]="filter"
-  [resultTemplate]="displayTemplate"
-  [inputFormatter]="formatter"
-  (click)="click$.next($event.target.value)"
-  (selectItem)="orgChanged($event)"
-  #instance="ngbTypeahead"
-/>
diff --git a/Open-ILS/src/eg2/src/app/share/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select.component.ts
deleted file mode 100644 (file)
index 627dd4e..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import {map, debounceTime} from 'rxjs/operators';
-import {Subject} from 'rxjs/Subject'; 
-import {EgAuthService} from '@eg/core/auth.service';
-import {EgStoreService} from '@eg/core/store.service';
-import {EgOrgService} from '@eg/core/org.service';
-import {EgIdlObject} from '@eg/core/idl.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: string = ' '; // U+2007 
-
-interface OrgDisplay {
-  id: number;
-  label: string;
-  disabled: boolean;
-}
-
-@Component({
-  selector: 'eg-org-select',
-  templateUrl: './org-select.component.html'
-})
-export class EgOrgSelectComponent implements OnInit {
-
-    selected: OrgDisplay;
-    hidden: number[] = [];
-    disabled: number[] = [];
-    click$ = new Subject<string>();
-    startOrg: EgIdlObject;
-
-    @ViewChild('instance') instance: NgbTypeahead;
-
-    // Placeholder text for selector input
-    @Input() placeholder: string = '';
-    @Input() stickySetting: string;
-
-    // Org unit field displayed in the selector
-    @Input() displayField: string = '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: boolean = 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: EgIdlObject) {
-        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: EgIdlObject) {
-        if (org) this.selected = this.formatForDisplay(org);
-    }
-
-    // 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<EgIdlObject>();
-
-    constructor(
-      private auth: EgAuthService,
-      private store: EgStoreService,
-      private org: EgOrgService 
-    ) {}
-    
-    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);
-        }
-    }
-
-    // Format for display in the selector drop-down and input.
-    formatForDisplay(org: EgIdlObject): 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.
-    orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
-        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$
-            .debounceTime(200)
-            .distinctUntilChanged()
-            .merge(this.click$.filter(() => !this.instance.isPopupOpen()))
-            .map(term => {
-
-                return this.org.list().filter(org => {
-
-                    // Find orgs matching the search term
-                    return org[this.displayField]()
-                      .toLowerCase().indexOf(term.toLowerCase()) > -1
-
-                }).filter(org => { // Exclude hidden orgs
-                    return this.hidden.filter(
-                        id => {return org.id() == id}).length == 0;
-
-                }).map(org => {return this.formatForDisplay(org)})
-            });
-    }
-}
-
-
diff --git a/Open-ILS/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..627dd4e
--- /dev/null
@@ -0,0 +1,152 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map, debounceTime} from 'rxjs/operators';
+import {Subject} from 'rxjs/Subject'; 
+import {EgAuthService} from '@eg/core/auth.service';
+import {EgStoreService} from '@eg/core/store.service';
+import {EgOrgService} from '@eg/core/org.service';
+import {EgIdlObject} from '@eg/core/idl.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: string = ' '; // U+2007 
+
+interface OrgDisplay {
+  id: number;
+  label: string;
+  disabled: boolean;
+}
+
+@Component({
+  selector: 'eg-org-select',
+  templateUrl: './org-select.component.html'
+})
+export class EgOrgSelectComponent implements OnInit {
+
+    selected: OrgDisplay;
+    hidden: number[] = [];
+    disabled: number[] = [];
+    click$ = new Subject<string>();
+    startOrg: EgIdlObject;
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Placeholder text for selector input
+    @Input() placeholder: string = '';
+    @Input() stickySetting: string;
+
+    // Org unit field displayed in the selector
+    @Input() displayField: string = '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: boolean = 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: EgIdlObject) {
+        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: EgIdlObject) {
+        if (org) this.selected = this.formatForDisplay(org);
+    }
+
+    // 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<EgIdlObject>();
+
+    constructor(
+      private auth: EgAuthService,
+      private store: EgStoreService,
+      private org: EgOrgService 
+    ) {}
+    
+    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);
+        }
+    }
+
+    // Format for display in the selector drop-down and input.
+    formatForDisplay(org: EgIdlObject): 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.
+    orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        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$
+            .debounceTime(200)
+            .distinctUntilChanged()
+            .merge(this.click$.filter(() => !this.instance.isPopupOpen()))
+            .map(term => {
+
+                return this.org.list().filter(org => {
+
+                    // Find orgs matching the search term
+                    return org[this.displayField]()
+                      .toLowerCase().indexOf(term.toLowerCase()) > -1
+
+                }).filter(org => { // Exclude hidden orgs
+                    return this.hidden.filter(
+                        id => {return org.id() == id}).length == 0;
+
+                }).map(org => {return this.formatForDisplay(org)})
+            });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/unapi.service.ts b/Open-ILS/src/eg2/src/app/share/unapi.service.ts
deleted file mode 100644 (file)
index 9034ae4..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import {Injectable, EventEmitter} from '@angular/core';
-import {EgOrgService} 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 EgUnapiParams {
-    target: string; // bre, ...
-    id: number | string; // 1 | 1,2,3,4,5
-    extras: string; // {holdings_xml,mra,...}
-    format: string; // mods32, marxml, ...
-    orgId?: number; // org unit ID
-    depth?: number; // org unit depth
-};
-
-@Injectable()
-export class EgUnapiService {
-
-    constructor(private org: EgOrgService) {}
-
-    createUrl(params: EgUnapiParams): string {
-        let depth = params.depth || 0;
-        let org = params.orgId ? this.org.get(params.orgId) : this.org.root();
-
-        return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` +
-            `${org.shortname()}/${depth}&format=${params.format}`;
-    }
-
-    getAsXmlDocument(params: EgUnapiParams): Promise<XMLDocument> {
-        // XReq creates an XML document for us.  Seems like the right
-        // tool for the job.
-        let url = this.createUrl(params);
-        return new Promise((resolve, reject) => {
-            var xhttp = new XMLHttpRequest();
-            xhttp.onreadystatechange = function() {
-                if (this.readyState == 4) {
-                    if (this.status == 200) {
-                        resolve(xhttp.responseXML);
-                    } else {
-                        reject(`UNAPI request failed for ${url}`);
-                    }
-                }
-            }
-            xhttp.open("GET", url, true);
-            xhttp.send();
-        });
-    }
-}
-
-
diff --git a/Open-ILS/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..971fe7e
--- /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 {EgStoreService} from '@eg/core/store.service';
+const AUDIO_BASE_URL = '/audio/notifications/';
+
+@Injectable()
+export class EgAudioService {
+
+    // map of requested audio path to resolved path
+    private urlCache: {[path:string] : string} = {};
+
+    constructor(private store: EgStoreService) {}
+
+    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;
+        
+            let url = this.urlCache[path] || 
+                AUDIO_BASE_URL + path.replace(/\./g, '/') + '.wav';
+
+            let 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);
+            }
+        });
+    }
+}
+
+
index 7438ec9..4bb63fd 100644 (file)
@@ -1,6 +1,6 @@
 import {NgModule} from '@angular/core';
 import {EgStaffCommonModule} from '@eg/staff/common.module';
-import {EgUnapiService} from '@eg/share/unapi.service';
+import {EgUnapiService} from '@eg/share/catalog/unapi.service';
 import {EgCatalogRoutingModule} from './routing.module';
 import {EgCatalogService} from '@eg/share/catalog/catalog.service';
 import {EgCatalogUrlService} from '@eg/share/catalog/catalog-url.service';
index 762c1aa..c8aeb1d 100644 (file)
@@ -1,7 +1,7 @@
 import {NgModule, ModuleWithProviders} from '@angular/core';
 import {EgCommonModule} from '@eg/common.module';
 import {EgStaffBannerComponent} from './share/staff-banner.component';
-import {EgOrgSelectComponent} from '@eg/share/org-select.component';
+import {EgOrgSelectComponent} from '@eg/share/org-select/org-select.component';
 import {EgDialogComponent} from '@eg/share/dialog/dialog.component';
 import {EgConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {EgPromptDialogComponent} from '@eg/share/dialog/prompt.component';
index 8f2fe5a..301979f 100644 (file)
@@ -6,6 +6,7 @@ import {Router, Resolve, RouterStateSnapshot,
 import {EgStoreService} from '@eg/core/store.service';
 import {EgNetService} from '@eg/core/net.service';
 import {EgAuthService, EgAuthWsState} from '@eg/core/auth.service';
+import {EgPermService} from '@eg/core/perm.service';
 
 const LOGIN_PATH = '/staff/login';
 const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
@@ -25,7 +26,8 @@ export class EgStaffResolver implements Resolve<Observable<any>> {
         private ngLocation: Location,
         private store: EgStoreService,
         private net: EgNetService,
-        private auth: EgAuthService
+        private auth: EgAuthService,
+        private perm: EgPermService,
     ) {}
 
     resolve(
@@ -50,12 +52,20 @@ export class EgStaffResolver implements Resolve<Observable<any>> {
         this.auth.testAuthToken().then(
             tokenOk => {
                 console.debug('EgStaffResolver: authtoken verified');
-                this.auth.verifyWorkstation().then(
-                    wsOk => {
-                        this.loadStartupData()
-                        .then(ok => this.observer.complete())
-                    },
-                    wsNotOk => this.handleInvalidWorkstation(path)
+                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)
@@ -64,6 +74,27 @@ export class EgStaffResolver implements Resolve<Observable<any>> {
         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 {
index 974f08a..42d6e67 100644 (file)
@@ -5,16 +5,6 @@
   </div>
 </div>
 
-<!--
-<eg2-confirm-dialog                                                             
-  #testDialog                                                     
-  dialogTitle="Test Dialog"
-  dialogBody='Test Dialog Body'>
-</eg2-confirm-dialog> 
--->
-
-<eg2-hello-world #testHello message="Look out, I'm testing here!"></eg2-hello-world>
-
 <div class="row">
   <div class="col-md-12">
     <div ng-if="is_backdate()" class="alert-danger pad-all-min">