LP#1775466 Server settings continued, needs a bit more testing
authorBill Erickson <berickxx@gmail.com>
Fri, 3 Aug 2018 22:41:16 +0000 (18:41 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 3 Aug 2018 22:41:16 +0000 (18:41 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/core/auth.service.ts
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
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/share/util/audio.service.ts
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/login.component.ts

index 7d58dda..84897f0 100644 (file)
@@ -286,23 +286,22 @@ export class AuthService {
         }
 
         return new Promise((resolve, reject) => {
-            this.store.getItem('eg.workstation.all')
-            .then(workstations => {
-
-                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();
-                    }
+            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();
-            });
+            this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
+            reject();
         });
     }
 
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..c08dab8
--- /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);
+    }
+}
+
index 43ffa6f..af1e0e9 100644 (file)
@@ -1,18 +1,16 @@
 /**
  * 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, Injector} from '@angular/core';
+import {Injectable} from '@angular/core';
 import {CookieService} from 'ngx-cookie';
-import {NetService} from './net.service';
-
-// Settings summary objects returned by the API
-export interface ServerSettingSummary {
-    name: string;
-    value: string;
-    has_org_setting: boolean;
-    has_user_setting: boolean;
-    has_workstation_setting: boolean;
-}
 
 @Injectable({providedIn: 'root'})
 export class StoreService {
@@ -23,11 +21,6 @@ export class StoreService {
     // Note cookies shared with /eg/staff must be stored at "/"
     loginSessionBasePath = '/';
 
-    // Loaded on demand to avoid circular ref in compiler.
-    auth: any;
-
-    cache: {[key: string]: ServerSettingSummary};
-
     // Set of keys whose values should disappear at logout.
     loginSessionKeys: string[] = [
         'eg.auth.token',
@@ -37,10 +30,7 @@ export class StoreService {
     ];
 
     constructor(
-        private injector: Injector,
-        private cookieService: CookieService,
-        private net: NetService) {
-        this.cache = {};
+        private cookieService: CookieService) {
     }
 
     private parseJson(valJson: string): any {
@@ -62,32 +52,21 @@ export class StoreService {
         this.loginSessionKeys.push(key);
     }
 
-    setItem(key: string, val: any, isJson?: Boolean): Promise<void> {
-        // TODO: route keys appropriately
-        this.setLocalItem(key, val, isJson);
-        return Promise.resolve();
-    }
-
-    setLocalItem(key: string, val: any, isJson?: Boolean): void {
+    setLocalItem(key: string, val: any, isJson?: boolean): void {
         if (!isJson) {
             val = JSON.stringify(val);
         }
-        // console.debug(`${key} ${val}`);
         window.localStorage.setItem(key, val);
     }
 
-    setServerItem(key: string, val: any): Promise<void> {
-        return Promise.resolve();
-    }
-
-    setSessionItem(key: string, val: any, isJson?: Boolean): void {
+    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 {
+    setLoginSessionItem(key: string, val: any, isJson?: boolean): void {
         if (!isJson) {
             val = JSON.stringify(val);
         }
@@ -95,41 +74,10 @@ export class StoreService {
             {path : this.loginSessionBasePath, secure: true});
     }
 
-    getItem(key: string): Promise<any> {
-        // TODO: route keys appropriately
-        return Promise.resolve(this.getLocalItem(key));
-    }
-
     getLocalItem(key: string): any {
         return this.parseJson(window.localStorage.getItem(key));
     }
 
-    getServerItem(key: string): Promise<any> {
-
-        if (this.cache[key]) {
-            return Promise.resolve(this.cache[key].value);
-        }
-
-        if (!this.auth) {
-            this.auth = this.injector.get('AuthService');
-        }
-
-        if (this.auth.token()) {
-            return Promise.reject('auth token required for server settings');
-        }
-
-        return this.net.request(
-            'open-ils.actor',
-            'open-ils.actor.settings.retrieve',
-            [key], this.auth.token()
-        ).toPromise().then(
-            (summary: ServerSettingSummary) => {
-                this.cache[summary.name] = summary;
-                return summary.value;
-            }
-        );
-    }
-
     getSessionItem(key: string): any {
         return this.parseJson(window.sessionStorage.getItem(key));
     }
@@ -138,19 +86,10 @@ export class StoreService {
         return this.parseJson(this.cookieService.get(key));
     }
 
-    removeItem(key: string): Promise<any> {
-        // TODO: route keys appropriately
-        return Promise.resolve(this.removeLocalItem(key));
-    }
-
     removeLocalItem(key: string): void {
         window.localStorage.removeItem(key);
     }
 
-    removeServerItem(key: string): Promise<void> {
-        return Promise.resolve();
-    }
-
     removeSessionItem(key: string): void {
         window.sessionStorage.removeItem(key);
     }
index 6cd7b6b..48b9e39 100644 (file)
         <span class="ml-2" i18n>Manage Column Widths</span>
       </a>
       <a class="dropdown-item label-with-material-icon" 
-        (click)="saveColumns()">
+        (click)="saveGridConfig()">
         <span class="material-icons">save</span>
-        <span class="ml-2" i18n>Save Columns</span>
+        <span class="ml-2" i18n>Save Grid Settings</span>
       </a>
       <a class="dropdown-item label-with-material-icon" 
         (click)="gridContext.columnSet.reset()">
index fd15ba9..5c8b523 100644 (file)
@@ -25,12 +25,12 @@ export class GridToolbarComponent implements OnInit {
 
     ngOnInit() {}
 
-    saveColumns() {
+    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.saveColumns().then(
+        this.gridContext.saveGridConfig().then(
             // hide the with config after saving
             ok => this.colWidthConfig.isVisible = false,
             err => console.error(`Error saving columns: ${err}`)
index 3a6d073..db02e62 100644 (file)
@@ -3,7 +3,7 @@ import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
 import {Subscription} from 'rxjs/Subscription';
 import {IdlService} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
-import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
 
@@ -88,7 +88,7 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     constructor(
         private idl: IdlService,
         private org: OrgService,
-        private store: StoreService,
+        private store: ServerStoreService,
         private format: FormatService
     ) {
         this.context =
index b2af621..0c40a76 100644 (file)
@@ -6,7 +6,7 @@ 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 {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {Pager} from '@eg/share/util/pager';
 
@@ -280,10 +280,8 @@ export class GridColumnSet {
 
     compileSaveObject(): GridColumnPersistConf[] {
         // only store information about visible columns.
-        const conf = this.displayColumns();
-
-        // scrunch the data down to just the needed info
-        return conf.map(col => {
+        // 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); }
@@ -429,12 +427,14 @@ export class GridContext {
     // Services injected by our grid component
     idl: IdlService;
     org: OrgService;
-    store: StoreService;
+    store: ServerStoreService;
     format: FormatService;
 
     constructor(
-        idl: IdlService, org: OrgService,
-        store: StoreService, format: FormatService) {
+        idl: IdlService, 
+        org: OrgService,
+        store: ServerStoreService,
+        format: FormatService) {
 
         this.idl = idl;
         this.org = org;
@@ -459,7 +459,7 @@ export class GridContext {
 
     // Load initial settings and data.
     initData() {
-        this.applyColumnsConfig()
+        this.applyGridConfig()
         .then(ok => this.dataSource.requestPage(this.pager))
         .then(ok => this.listenToPager());
     }
@@ -468,9 +468,21 @@ export class GridContext {
         this.ignorePager();
     }
 
-    applyColumnsConfig(): Promise<void> {
-        return this.getColumnsConfig(this.persistKey)
-        .then(conf => this.columnSet.applyColumnSettings(conf));
+    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() {
@@ -834,24 +846,24 @@ export class GridContext {
         });
     }
 
-
-    saveColumns(): Promise<any> {
+    saveGridConfig(): Promise<any> {
         if (!this.persistKey) {
             throw new Error('Grid persistKey required to save columns');
         }
-        const compiled: GridColumnPersistConf[] =
-            this.columnSet.compileSaveObject();
-        return this.store.setItem('eg.grid.' + this.persistKey, compiled);
-    }
+        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: saveColumnsAsOrgSetting(...)
+    // TODO: saveGridConfigAsOrgSetting(...)
 
-    getColumnsConfig(persistKey: string): Promise<GridColumnPersistConf[]> {
-        if (!persistKey) { return Promise.resolve([]); }
+    getGridConfig(persistKey: string): Promise<GridPersistConf> {
+        if (!persistKey) { return Promise.resolve(null); }
         return this.store.getItem('eg.grid.' + persistKey);
     }
-
 }
 
 export class GridColumnPersistConf {
@@ -861,6 +873,12 @@ export class GridColumnPersistConf {
     align?: string;
 }
 
+export class GridPersistConf {
+    version: number;
+    limit: number;
+    columns: GridColumnPersistConf[];
+}
+
 
 // Actions apply to specific rows
 export class GridToolbarAction {
index 9e5b9a9..3f3320a 100644 (file)
@@ -17,7 +17,7 @@
  * workstation settings.
  */
 import {Injectable, EventEmitter} from '@angular/core';
-import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
 const AUDIO_BASE_URL = '/audio/notifications/';
 
 @Injectable()
@@ -26,7 +26,7 @@ export class AudioService {
     // map of requested audio path to resolved path
     private urlCache: {[path: string]: string} = {};
 
-    constructor(private store: StoreService) {}
+    constructor(private store: ServerStoreService) {}
 
     play(path: string): void {
         if (path) {
index 2c204e5..9fb0d7e 100644 (file)
@@ -50,20 +50,15 @@ export class WorkstationsComponent implements OnInit {
     ) {}
 
     ngOnInit() {
-        this.store.getItem('eg.workstation.all')
-        .then(list => this.workstations = list || [])
-        .then(noop => this.store.getItem('eg.workstation.default'))
-        .then(defWs => {
-            this.defaultName = defWs;
-            this.selectedName = this.auth.workstation() || defWs;
-        })
-        .then(noop => {
-            const rm = this.route.snapshot.paramMap.get('remove');
-            if (rm) {
-                this.removeSelected(this.removeWorkstation = rm);
-            }
-        });
+        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
@@ -90,7 +85,7 @@ export class WorkstationsComponent implements OnInit {
     setDefault(): void {
       if (this.selected()) {
             this.defaultName = this.selected().name;
-            this.store.setItem('eg.workstation.default', this.defaultName);
+            this.store.setLocalItem('eg.workstation.default', this.defaultName);
         }
     }
 
@@ -100,11 +95,11 @@ export class WorkstationsComponent implements OnInit {
         }
 
         this.workstations = this.workstations.filter(w => w.name !== name);
-        this.store.setItem('eg.workstation.all', this.workstations);
+        this.store.setLocalItem('eg.workstation.all', this.workstations);
 
         if (this.defaultName === name) {
             this.defaultName = null;
-            this.store.removeItem('eg.workstation.default');
+            this.store.removeLocalItem('eg.workstation.default');
         }
     }
 
@@ -177,16 +172,14 @@ export class WorkstationsComponent implements OnInit {
         };
 
         this.workstations.push(ws);
-        this.store.setItem('eg.workstation.all', this.workstations)
-        .then(ok => {
-            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();
-            }
-        });
+        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();
+        }
     }
 }
 
index f91e1fd..729beea 100644 (file)
@@ -3,7 +3,7 @@ import {Observable} from 'rxjs/Observable';
 import {Observer} from 'rxjs/Observer';
 import {Router, Resolve, RouterStateSnapshot,
         ActivatedRouteSnapshot} from '@angular/router';
-import {StoreService} from '@eg/core/store.service';
+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';
@@ -16,7 +16,7 @@ export class CatalogResolver implements Resolve<Promise<any[]>> {
 
     constructor(
         private router: Router,
-        private store: StoreService,
+        private store: ServerStoreService,
         private org: OrgService,
         private net: NetService,
         private auth: AuthService,
index 0083c32..7d663bf 100644 (file)
@@ -35,11 +35,10 @@ export class StaffLoginComponent implements OnInit {
         // Focus username
         this.renderer.selectRootElement('#username').focus();
 
-        this.store.getItem('eg.workstation.all')
-        .then(list => this.workstations = list || [])
-        .then(list => this.store.getItem('eg.workstation.default'))
-        .then(defWs => this.args.workstation = defWs)
-        .then(noOp => this.applyWorkstation());
+        this.workstations = this.store.getLocalItem('eg.workstation.all');
+        this.args.workstation = 
+            this.store.getLocalItem('eg.workstation.default');
+        this.applyWorkstation();
     }
 
     applyWorkstation() {