LP#1779158 Angular vandelay service / startup data
authorBill Erickson <berickxx@gmail.com>
Fri, 29 Jun 2018 17:30:18 +0000 (13:30 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 11 Oct 2018 18:56:30 +0000 (14:56 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.html
Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts
Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts [new file with mode: 0644]

index 839454b..07db35f 100644 (file)
@@ -1,5 +1,5 @@
 
-<ul class="nav nav-pills nav-fill pt-3 pb-3">
+<ul class="nav nav-pills nav-fill p-3">
   <li class="nav-item">
     <a class="nav-link" [ngClass]="{active: tab=='export'}" 
       routerLink="/staff/cat/vandelay/export" i18n>Export</a>
   </li>
   <li class="nav-item">
     <a class="nav-link" disabled [ngClass]="{active: tab=='display_attrs'}" 
-      routerLink="/staff/cat/vandelay/display_attrs" i18n>Record Display Attributes</a>
+      routerLink="/staff/cat/vandelay/display_attrs" 
+        i18n>Record Display Attributes</a>
   </li>
   <li class="nav-item">
     <a class="nav-link" disabled [ngClass]="{active: tab=='merge_profiles'}" 
-      routerLink="/staff/cat/vandelay/merge_profiles" i18n>Merge / Overlay Profiles</a>
+      routerLink="/staff/cat/vandelay/merge_profiles" 
+        i18n>Merge / Overlay Profiles</a>
   </li>
   <li class="nav-item">
     <a class="nav-link" disabled [ngClass]="{active: tab=='match_sets'}" 
-      routerLink="/staff/cat/vandelay/match_sets" i18n>Record Match Sets</a>
+      routerLink="/staff/cat/vandelay/match_sets" 
+        i18n>Record Match Sets</a>
   </li>
   <li class="nav-item">
     <a class="nav-link" disabled [ngClass]="{active: tab=='holdings_profiles'}" 
-      routerLink="/staff/cat/vandelay/holdings_profiles" i18n>Holdings Import Profiles</a>
+      routerLink="/staff/cat/vandelay/holdings_profiles" 
+      i18n>Holdings Import Profiles</a>
   </li>
 </ul>
 
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+
 <router-outlet></router-outlet>
index 4c537b6..9239f11 100644 (file)
@@ -1,18 +1,34 @@
-import {Component, OnInit} from '@angular/core';
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, NavigationEnd} from "@angular/router";
 import {take} from 'rxjs/operators/take';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {VandelayService} from './vandelay.service';
+import {IdlObject} from '@eg/core/idl.service';
 
 @Component({
   templateUrl: 'vandelay.component.html'
 })
-export class VandelayComponent implements OnInit {
+export class VandelayComponent implements OnInit, AfterViewInit {
 
     tab: string;
 
+    attrDefs = {bib: [], auth: []};
+    allQueues = {bib: [], auth: []};
+    activeQueues = {bib: [], auth: []}; // complete === 'f'
+    bibSources: IdlObject[];
+    bibBuckets: IdlObject[];
+    bibTrashGroups: IdlObject[];
+    copyStatuses: IdlObject[];
+    importItemDefs: IdlObject[];
+    matchSets: {[stype: string]: IdlObject[]};
+
+    @ViewChild('progressDialog')
+        private progressDialog: ProgressDialogComponent;
+
     constructor(
         private router: Router,
-        private route: ActivatedRoute
-    ) {
+        private route: ActivatedRoute,
+        private vandelay: VandelayService) {
 
         // As the parent component of the vandelay route tree, our
         // activated route never changes.  Instead, listen for global
@@ -24,10 +40,68 @@ export class VandelayComponent implements OnInit {
                 .subscribe(segments => this.tab = segments[0].path);
             }
         });
+
+        this.attachDataListeners();
     }
 
     ngOnInit() {
     }
 
+    // Manage the shared data here so it persists across tabs.
+    // Load the shared data after view init so the page can initialize
+    // first and display the loading message.
+    ngAfterViewInit() {
+
+        // loading startup data causes data in this component to change
+        // inside its own lifecycle hook, which is a no-no.  Run after
+        // the hook.
+        setTimeout(() => {
+
+            this.vandelay.loadStartupData(this.progressDialog).then(ok => {
+
+                // Pull data that does not change during a vandelay 
+                // UI directly from the service.
+                this.bibSources = this.vandelay.bibSources;
+                this.bibBuckets = this.vandelay.bibBuckets;
+                this.bibTrashGroups = this.vandelay.bibTrashGroups;
+                this.copyStatuses = this.vandelay.copyStatuses;
+            });
+        });
+    }
+
+
+    // Listen for global data changes, store data locally so it 
+    // may be propagated to child components.
+    attachDataListeners() {
+
+        this.vandelay.onAttrDefsUpdate$.subscribe(collection => {
+            const dtype = collection.dtype;
+            this.attrDefs[dtype] = collection.defs;
+        });
+
+        // Bib/auth queue list
+        this.vandelay.onQueueListUpdate$.subscribe(collection => {
+            const qtype = collection.qtype;
+            collection.queues.forEach(q => {
+                this.allQueues[qtype] = q;
+                if (q.complete() === 'f') {
+                    this.activeQueues[qtype] = q;
+                }
+            });
+        });
+
+        this.vandelay.onItemImportDefsUpdate$.subscribe(
+            defs => this.importItemDefs = defs
+        );
+
+        this.vandelay.onMatchSetsUpdate$.subscribe(sets => {
+            sets.forEach(s => {
+                if (!this.matchSets[s.mtype()]) {
+                    this.matchSets[s.mtype()] = [];
+                }
+                this.matchSets[s.mtype()].push(s);
+            });
+        });
+    }
 }
 
index 2b9c65b..c65a44f 100644 (file)
@@ -1,6 +1,7 @@
 import {NgModule} from '@angular/core';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {VandelayRoutingModule} from './routing.module';
+import {VandelayService} from './vandelay.service';
 import {VandelayComponent} from './vandelay.component';
 import {ImportComponent} from './import.component';
 import {ExportComponent} from './export.component';
@@ -18,6 +19,7 @@ import {GridModule} from '@eg/share/grid/grid.module';
     GridModule
   ],
   providers: [
+    VandelayService
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts
new file mode 100644 (file)
index 0000000..cf8e7fb
--- /dev/null
@@ -0,0 +1,190 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {tap} from 'rxjs/operators/tap';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {PermService} from '@eg/core/perm.service';
+import {EventService} from '@eg/core/event.service';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+
+@Injectable()
+export class VandelayService {
+
+    // Data that may change within the vandelay UI is propagated
+    // via EventEmitters
+    onQueueListUpdate$: EventEmitter<any>;
+    onItemImportDefsUpdate$: EventEmitter<IdlObject[]>;
+    onMatchSetsUpdate$: EventEmitter<IdlObject[]>;
+    onAttrDefsUpdate$: EventEmitter<any>;
+
+    // Data that persists during a vandelay session needs no emitters.
+    bibSources: IdlObject[];
+    bibBuckets: IdlObject[];
+    bibTrashGroups: IdlObject[];
+    copyStatuses: IdlObject[];
+
+    // Settings to retrieve (and cache) at load time.
+    orgSettings = ['vandelay.default_match_set'];
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private perm: PermService
+    ) {
+        this.onQueueListUpdate$ = new EventEmitter<any>();
+        this.onItemImportDefsUpdate$ = new EventEmitter<IdlObject[]>();
+        this.onMatchSetsUpdate$ = new EventEmitter<IdlObject[]>();
+        this.onAttrDefsUpdate$ = new EventEmitter<any>();
+    }
+
+    loadStartupData(dialog: ProgressDialogComponent): Promise<any> {
+
+        dialog.open();
+
+        const promises = [
+            this.getAttrDefs('bib').toPromise(),
+            this.getAttrDefs('auth').toPromise(),
+            this.getQueueList('bib').toPromise(),
+            this.getQueueList('auth').toPromise(),
+            this.getItemImportDefs().toPromise(),
+            this.getMatchSets().toPromise(),
+            this.getBibBuckets(),
+            this.getBibSources(),
+            this.getBibTrashGroups(),
+            this.getCopyStatuses(),
+            this.org.settings(this.orgSettings)
+        ];
+
+        dialog.update({value: 0, max: promises.length});
+        promises.forEach(p => p.then(_ => dialog.increment()));
+
+        return Promise.all(promises).then(ok => dialog.close());
+    }
+
+    getAttrDefs(dtype: string): Observable<IdlObject> {
+        const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad';
+        const defs = [];
+
+        const orderBy = {};
+        orderBy[cls] = 'id'
+
+        return this.pcrud.retrieveAll(cls, {order_by: orderBy})
+        .pipe(tap(
+            def => defs.push(def),
+            err => {},
+            ()  => {
+                console.debug(`Fetched ${defs.length} ${dtype} attr def(s)`);
+                this.onAttrDefsUpdate$.emit({dtype: dtype, defs: defs});
+            }
+        ));
+    }
+
+    // Returns a promise resolved with the list of queues.
+    // Also emits the onQueueListUpdate event so listeners
+    // can detect queue content changes.
+    getQueueList(qtype: string, filters?: any): Observable<IdlObject> {
+        const qt = (qtype === 'bib') ? qtype : 'authority';
+        const queues = [];
+
+        return this.net.request(
+            'open-ils.vandelay',
+            `open-ils.vandelay.${qt}_queue.owner.retrieve`,
+            this.auth.token(), null, filters
+        ).pipe(tap(
+            queue => {
+                const evt = this.evt.parse(queue);
+                if (evt) { return alert(evt); } // should not happen
+                queues.push(queue);
+            },
+            err => {console.error(err)},
+            ()  => {
+                console.debug(`Fetched ${queues.length} "${qtype}" queue(s)`);
+                this.onQueueListUpdate$.emit({qtype: qtype, queues: queues});
+            }
+        ));
+    }
+
+    getBibSources(): Promise<any> {
+        return this.pcrud.retrieveAll('cbs', 
+          {order_by: {cbs: 'id'}}, 
+          {atomic: true}
+        ).toPromise().then(sources => {
+            console.debug(`Fetched ${sources.length} bib source(s)`);
+            this.bibSources = sources;
+        });
+    }
+
+    getItemImportDefs(): Observable<IdlObject> {
+        const defs = [];
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+        return this.pcrud.search('viiad', {owner: owners})
+        .pipe(tap(
+            def => defs.push(def),
+            err => {},
+            ()  => {
+                console.debug(`Fetched ${defs.length} import item def(s)`);
+                this.onItemImportDefsUpdate$.emit(defs);
+            }
+        ));
+    }
+
+    getMatchSets(): Observable<IdlObject[]> {
+        const sets = [];
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+        return this.pcrud.search('vms', {owner: owners})
+        .pipe(tap(
+            s   => sets.push(s),
+            err => {},
+            ()  => {
+                console.debug(`Fetched ${sets.length} match set(s)`);
+                this.onMatchSetsUpdate$.emit(sets);
+            }
+        ));
+    }
+
+    getBibBuckets(): Promise<any> {
+        this.bibBuckets = [];
+
+        return this.net.request(
+            'open-ils.actor', 
+            'open-ils.actor.container.retrieve_by_class',
+            this.auth.token(), this.auth.user().id(), 'biblio', 'staff_client'
+        ).pipe(tap(
+            bkt => this.bibBuckets.push(bkt),
+            err => {},
+            ()  => console.debug(`Fetched ${this.bibBuckets.length} bucket(s)`)
+        )).toPromise();
+    }
+
+    getCopyStatuses(): Promise<any> {
+        return this.pcrud.retrieveAll('ccs', {}, {atomic: true})
+        .toPromise().then(stats => {
+            console.debug(`Fetched ${stats.length} copy status(es)`);
+            this.copyStatuses = stats;
+        });
+    }
+
+    getBibTrashGroups(): Promise<any> {
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+        return this.pcrud.search('vibtg', 
+            {always_apply : 'f', owner: owners}, 
+            {vibtg : ['label']},
+            {atomic: true}
+        ).toPromise().then(groups => {
+            console.debug(`Fetched ${groups.length} bib trash field group(s)`);
+            this.bibTrashGroups = groups;
+        });
+    }
+
+
+}
+