[open-ils-commits] [GIT] Evergreen ILS branch master updated. c9c9c2c3413e6640222556dfa20d9afbbb5dac57

Evergreen Git git at git.evergreen-ils.org
Tue Feb 19 18:16:40 EST 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  c9c9c2c3413e6640222556dfa20d9afbbb5dac57 (commit)
       via  dc79692240236a916b23f647993a489e7657e48f (commit)
       via  a3d3e73a752612d435c54a0ca8a8dca5c8fe40a6 (commit)
       via  1b5985fc636be0748504201f275bf548b376ceeb (commit)
       via  65eee056f8165ccc97070e9ba3b16bab054e8834 (commit)
       via  667b1c6ddea74f66406eff7abdc184a559a27a99 (commit)
       via  a3bae111c13dfbae6d8555db0356e277344b8a3b (commit)
       via  03268d65d2699b7b402aff0037e48e6566a331f8 (commit)
       via  169cd66fe4589ff742511aa8e2bbc353f6b1c253 (commit)
       via  48576fe0d0a648bee37de68fc7beba3585cbe7dc (commit)
       via  f2504b21de1eb5e3eae3047c2de0dc24b8999583 (commit)
       via  e6ce65d3b8b0188b4c30bc31abce9e2cbba40336 (commit)
       via  bcc72fa70921a842d5c876c72278e8ab355e28a2 (commit)
       via  8777877cd9ccdc45fbc8286e4fcc272931d3b1a9 (commit)
      from  142655ae2b4d808fc43e34e4223dbbf0c04e7d44 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit c9c9c2c3413e6640222556dfa20d9afbbb5dac57
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Tue Feb 19 18:14:49 2019 -0500

    LP#1779158 Stamping upgrade scripts for Angular Vandelay
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1150.data.vandelay-grid-settings.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1150.data.vandelay-grid-settings.sql
index 35726a4867..9b45201e10 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1150.data.vandelay-grid-settings.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
---SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1150', :eg_version);
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1151.data.vandelay-template-settings.sql
similarity index 82%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1151.data.vandelay-template-settings.sql
index bd104ef32d..304c03a238 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1151.data.vandelay-template-settings.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
-SELECT evergreen.upgrade_deps_block_check('YYYY', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1151', :eg_version);
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (

commit dc79692240236a916b23f647993a489e7657e48f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jan 31 12:08:37 2019 -0500

    LP1779158 Angular7 and ng-lint updates
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/package-lock.json b/Open-ILS/src/eg2/package-lock.json
index cea6fe8505..fe0147bdc3 100644
--- a/Open-ILS/src/eg2/package-lock.json
+++ b/Open-ILS/src/eg2/package-lock.json
@@ -3760,6 +3760,11 @@
         "schema-utils": "^1.0.0"
       }
     },
+    "file-saver": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz",
+      "integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g=="
+    },
     "filename-regex": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json
index 7d179ee954..0c57e46406 100644
--- a/Open-ILS/src/eg2/package.json
+++ b/Open-ILS/src/eg2/package.json
@@ -28,6 +28,7 @@
     "@nguniversal/express-engine": "^7.1.0",
     "bootstrap-css-only": "^4.2.1",
     "core-js": "^2.6.3",
+    "file-saver": "^2.0.0",
     "ngx-cookie": "^4.1.2",
     "rxjs": "^6.4.0",
     "zone.js": "^0.8.29"
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
index 6cb13afd96..bbd6daa198 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
@@ -1,5 +1,5 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 
 @Component({
@@ -13,7 +13,7 @@ export class DisplayAttrsComponent {
         private router: Router,
         private route: ActivatedRoute) {
 
-        this.route.paramMap.subscribe((params: ParamMap) => {                  
+        this.route.paramMap.subscribe((params: ParamMap) => {
             this.attrType = params.get('atype');
         });
     }
@@ -26,7 +26,7 @@ export class DisplayAttrsComponent {
         // prevent tab changing until after route navigation
         evt.preventDefault();
 
-        const url = 
+        const url =
           `/staff/cat/vandelay/display_attrs/${this.attrType}`;
 
         this.router.navigate([url]);
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
index 253cfcb454..d980e02bd2 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
@@ -2,7 +2,7 @@ import {Component, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
 import {NgbPanelChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
-import {saveAs} from 'file-saver/FileSaver';
+import {saveAs} from 'file-saver';
 import {AuthService} from '@eg/core/auth.service';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
@@ -26,7 +26,7 @@ export class ExportComponent implements AfterViewInit {
     isExporting: boolean;
 
     @ViewChild('fileSelector') private fileSelector;
-    @ViewChild('exportProgress') 
+    @ViewChild('exportProgress')
         private exportProgress: ProgressInlineComponent;
 
     constructor(
@@ -54,12 +54,12 @@ export class ExportComponent implements AfterViewInit {
             setTimeout(() => {
                 this.renderer.selectRootElement(
                     `#${this.recordSource}-input`).focus();
-            })
+            });
         }
     }
 
     fileSelected($event) {
-       this.selectedFile = $event.target.files[0]; 
+       this.selectedFile = $event.target.files[0];
     }
 
     hasNeededData(): boolean {
@@ -86,20 +86,20 @@ export class ExportComponent implements AfterViewInit {
         switch (this.recordSource) {
 
             case 'csv':
-                formData.append('idcolumn', ''+this.fieldNumber);
-                formData.append('idfile', 
+                formData.append('idcolumn', '' + this.fieldNumber);
+                formData.append('idfile',
                     this.selectedFile, this.selectedFile.name);
                 break;
 
             case 'record-id':
-                formData.append('id', ''+this.recordId);
+                formData.append('id', '' + this.recordId);
                 break;
 
             case 'bucket-id':
-                formData.append('containerid', ''+this.bucketId);
+                formData.append('containerid', '' + this.bucketId);
                 break;
         }
-        
+
         this.sendExportRequest(formData);
     }
 
@@ -108,19 +108,19 @@ export class ExportComponent implements AfterViewInit {
         const fileName = `export.${this.recordType}.` +
             `${this.recordEncoding}.${this.recordFormat}`;
 
-        const req = new HttpRequest('POST', VANDELAY_EXPORT_PATH, 
+        const req = new HttpRequest('POST', VANDELAY_EXPORT_PATH,
             formData, {reportProgress: true, responseType: 'text'});
 
         this.http.request(req).subscribe(
             evt => {
-                console.log(evt);
+                console.debug(evt);
                 if (evt.type === HttpEventType.DownloadProgress) {
                     // File size not reported by server in advance.
                     this.exportProgress.update({value: evt.loaded});
 
                 } else if (evt instanceof HttpResponse) {
 
-                    saveAs(new Blob([evt.body], 
+                    saveAs(new Blob([evt.body as Blob],
                         {type: 'application/octet-stream'}), fileName);
 
                     this.isExporting = false;
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index 9732e5abd6..15b5640fff 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -1,12 +1,14 @@
-import {Component, OnInit, AfterViewInit, Input, ViewChild, OnDestroy} from '@angular/core';
-import {tap} from 'rxjs/operators/tap';
+import {Component, OnInit, AfterViewInit, Input,
+    ViewChild, OnDestroy} from '@angular/core';
+import {Subject} from 'rxjs';
+import {tap} from 'rxjs/operators';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {ToastService} from '@eg/share/toast/toast.service';
-import {ComboboxComponent, 
+import {ComboboxComponent,
     ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {VandelayService, VandelayImportSelection,
   VANDELAY_UPLOAD_PATH} from './vandelay.service';
@@ -14,7 +16,6 @@ import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
-import {Subject} from 'rxjs/Subject';
 import {ServerStoreService} from '@eg/core/server-store.service';
 
 const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
@@ -46,7 +47,7 @@ interface ImportOptions {
     merge_profile?: any;
     fall_through_merge_profile?: any;
     strip_field_groups?: number[];
-    match_quality_ratio: number,
+    match_quality_ratio: number;
     exit_early: boolean;
 }
 
@@ -60,7 +61,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
 
     // used for applying a default queue ID value when we have
     // a load-time queue before the queue combobox entries exist.
-    startQueueId: number; 
+    startQueueId: number;
 
     bibTrashGroups: IdlObject[];
     selectedTrashGroups: number[];
@@ -104,15 +105,15 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     newTemplateName: string;
 
     @ViewChild('fileSelector') private fileSelector;
-    @ViewChild('uploadProgress') 
+    @ViewChild('uploadProgress')
         private uploadProgress: ProgressInlineComponent;
-    @ViewChild('enqueueProgress') 
+    @ViewChild('enqueueProgress')
         private enqueueProgress: ProgressInlineComponent;
-    @ViewChild('importProgress') 
+    @ViewChild('importProgress')
         private importProgress: ProgressInlineComponent;
 
     // Need these refs so values can be applied via external stimuli
-    @ViewChild('formTemplateSelector') 
+    @ViewChild('formTemplateSelector')
         private formTemplateSelector: ComboboxComponent;
     @ViewChild('recordTypeSelector')
         private recordTypeSelector: ComboboxComponent;
@@ -244,7 +245,9 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         switch (etype) {
             case 'bibSources':
                 return (this.vandelay.bibSources || []).map(
-                    s => { return {id: s.id(), label: s.source()}; });
+                    s => {
+                        return {id: s.id(), label: s.source()};
+                    });
 
             case 'bibBuckets':
                 list = this.vandelay.bibBuckets;
@@ -279,7 +282,8 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         switch (etype) {
             case 'recordType':
                 this.recordType = id;
-              
+                break;
+
             case 'bibSources':
                 this.selectedBibSource = id;
                 break;
@@ -307,7 +311,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     }
 
     fileSelected($event) {
-       this.selectedFile = $event.target.files[0]; 
+       this.selectedFile = $event.target.files[0];
     }
 
     // Required form data varies depending on context.
@@ -315,8 +319,8 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         if (this.vandelay.importSelection) {
             return this.importActionSelected();
         } else {
-            return this.selectedQueue 
-                && Boolean(this.recordType) && Boolean(this.selectedFile)
+            return this.selectedQueue &&
+                Boolean(this.recordType) && Boolean(this.selectedFile);
         }
     }
 
@@ -394,7 +398,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
                         } else {
                             alert(evt); // server error
                         }
-                    } 
+                    }
 
                     return Promise.reject('Queue Create Failed');
                 }
@@ -410,18 +414,18 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             // Nothing to upload when processing pre-queued records.
             return Promise.resolve();
         }
-        
+
         const formData: FormData = new FormData();
 
         formData.append('ses', this.auth.token());
-        formData.append('marc_upload', 
+        formData.append('marc_upload',
             this.selectedFile, this.selectedFile.name);
 
         if (this.selectedBibSource) {
-            formData.append('bib_source', ''+this.selectedBibSource);
+            formData.append('bib_source', '' + this.selectedBibSource);
         }
 
-        const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData, 
+        const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
             {reportProgress: true, responseType: 'text'});
 
         return this.http.request(req).pipe(tap(
@@ -433,7 +437,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
                 } else if (evt instanceof HttpResponse) {
                     this.sessionKey = evt.body as string;
                     console.log(
-                        'Vandelay file uploaded OK with key '+this.sessionKey);
+                        'Vandelay file uploaded OK with key ' + this.sessionKey);
                 }
             },
 
@@ -450,16 +454,19 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             // Nothing to enqueue when processing pre-queued records
             return Promise.resolve();
         }
-        var spoolType = this.recordType;
-        if (this.recordType == 'authority') spoolType = 'auth'
+
+        let spoolType = this.recordType;
+        if (this.recordType === 'authority') {
+            spoolType = 'auth';
+        }
 
         const method = `open-ils.vandelay.${spoolType}.process_spool`;
 
         return new Promise((resolve, reject) => {
             this.net.request(
-                'open-ils.vandelay', method, 
+                'open-ils.vandelay', method,
                 this.auth.token(), this.sessionKey, this.activeQueueId,
-                null, null, this.selectedBibSource, 
+                null, null, this.selectedBibSource,
                 (this.sessionName || null), true
             ).subscribe(
                 tracker => {
@@ -472,7 +479,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
                         trkr => {
                             this.enqueueProgress.update({
                                 // enqueue API only tracks actions performed
-                                max: null, 
+                                max: null,
                                 value: trkr.actions_performed()
                             });
                         },
@@ -515,7 +522,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         }
 
         return new Promise((resolve, reject) => {
-            this.net.request('open-ils.vandelay', 
+            this.net.request('open-ils.vandelay',
                 method, this.auth.token(), target, options)
             .subscribe(
                 tracker => {
@@ -579,14 +586,14 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         const template = {};
         TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
 
-        console.debug("Saving import profile", template);
+        console.debug('Saving import profile', template);
 
         this.formTemplates[this.selectedTemplate] = template;
         return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
     }
 
     markTemplateDefault() {
-        
+
         Object.keys(this.formTemplates).forEach(
             name => delete this.formTemplates.default
         );
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
index 991206853d..f02ab214ba 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
@@ -54,7 +54,7 @@ export class MatchSetExpressionComponent implements OnInit {
         if (!this.matchSet_) { return Promise.resolve(); }
 
         return this.pcrud.search('vmsp',
-            {match_set: this.matchSet_.id()}, {}, 
+            {match_set: this.matchSet_.id()}, {},
             {atomic: true, authoritative: true}
         ).toPromise().then(points => this.ingestMatchPoints(points));
     }
@@ -101,7 +101,7 @@ export class MatchSetExpressionComponent implements OnInit {
 
     getPointLabel(point: IdlObject, showmatch?: boolean): Promise<string> {
         return this.strings.interpolate(
-            'staff.cat.vandelay.matchpoint.label', 
+            'staff.cat.vandelay.matchpoint.label',
             {point: point, showmatch: showmatch}
         );
     }
@@ -110,7 +110,7 @@ export class MatchSetExpressionComponent implements OnInit {
 
     deleteNode() {
         this.changesMade = true;
-        const node = this.tree.selectedNode()
+        const node = this.tree.selectedNode();
         this.tree.removeNode(node);
     }
 
@@ -144,13 +144,13 @@ export class MatchSetExpressionComponent implements OnInit {
 
         } else {
 
-            if (ptype == 'attr') {
+            if (ptype === 'attr') {
                 point.svf(this.newPoint.values.recordAttr);
 
-            } else if (ptype == 'marc') {
+            } else if (ptype === 'marc') {
                 point.tag(this.newPoint.values.marcTag);
                 point.subfield(this.newPoint.values.marcSf);
-            } else if (ptype == 'heading') {
+            } else if (ptype === 'heading') {
                 point.heading(true);
             }
 
@@ -159,7 +159,7 @@ export class MatchSetExpressionComponent implements OnInit {
         }
 
         const node: TreeNode = new TreeNode({
-            id: point.id(), 
+            id: point.id(),
             callerData: {point: point}
         });
 
@@ -175,13 +175,13 @@ export class MatchSetExpressionComponent implements OnInit {
 
             if (node.children.length) {
                 return '(' + node.children.map(renderNode).join(
-                    ' ' + node.callerData.slimLabel + ' ') + ')'
+                    ' ' + node.callerData.slimLabel + ' ') + ')';
             } else if (!node.callerData.point.bool_op()) {
                 return node.callerData.slimLabel;
             } else {
                 return '()';
             }
-        }
+        };
 
         return renderNode(this.tree.rootNode);
     }
@@ -211,7 +211,7 @@ export class MatchSetExpressionComponent implements OnInit {
             'open-ils.vandelay.match_set.update',
             this.auth.token(), this.matchSet_.id(), rootPoint
         ).toPromise().then(
-            ok =>this.refreshTree(),
+            ok => this.refreshTree(),
             err => console.error(err)
         );
     }
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
index e20c954be2..0afc01d6fb 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
@@ -1,6 +1,6 @@
 import {Component, AfterViewInit, ViewChild} from '@angular/core';
-import {Router} from '@angular/router';              
-import {Pager} from '@eg/share/util/pager';                                    
+import {Router} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {OrgService} from '@eg/core/org.service';
@@ -32,12 +32,12 @@ export class MatchSetListComponent implements AfterViewInit {
 
         this.gridSource.getRows = (pager: Pager) => {
             const orgs = this.org.ancestors(this.contextOrg, true);
-            return this.pcrud.search('vms', {owner: orgs}, {   
+            return this.pcrud.search('vms', {owner: orgs}, {
                 order_by: {vms: ['name']},
                 limit: pager.limit,
                 offset: pager.offset
             });
-        }
+        };
 
         this.createNew = () => {
             this.editDialog.mode = 'create';
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
index 6298981cff..ff5d9a0cf6 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
@@ -31,7 +31,7 @@ export class MatchSetNewPointComponent implements OnInit {
     @Input() isForQuality: boolean;
 
     // biblio, authority, quality
-    @Input() set pointType(type_: string) { 
+    @Input() set pointType(type_: string) {
         this.values.pointType = type_;
         this.values.recordAttr = '';
         this.values.matchScore = 1;
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
index b2409c196c..7409ef9dd9 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
@@ -1,6 +1,5 @@
 import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
+import {Observable, of} from 'rxjs';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {NetService} from '@eg/core/net.service';
@@ -21,8 +20,8 @@ export class MatchSetQualityComponent implements OnInit {
     matchSet_: IdlObject;
     @Input() set matchSet(ms: IdlObject) {
         this.matchSet_ = ms;
-        if (ms) { 
-            this.matchSetType = ms.mtype(); 
+        if (ms) {
+            this.matchSetType = ms.mtype();
             if (this.grid) {
                 this.grid.reload();
             }
@@ -48,7 +47,7 @@ export class MatchSetQualityComponent implements OnInit {
         this.dataSource.getRows = (pager: Pager, sort: any[]) => {
 
             if (!this.matchSet_) {
-                return Observable.of();
+                return of();
             }
 
             const orderBy: any = {};
@@ -64,7 +63,7 @@ export class MatchSetQualityComponent implements OnInit {
 
             const search = {match_set: this.matchSet_.id()};
             return this.pcrud.search('vmsq', search, searchOps);
-        }
+        };
 
         this.deleteSelected = (rows: any[]) => {
             this.pcrud.remove(rows).subscribe(
@@ -78,7 +77,7 @@ export class MatchSetQualityComponent implements OnInit {
     ngOnInit() {}
 
     addQuality() {
-        const quality = this.idl.create('vmsq');  
+        const quality = this.idl.create('vmsq');
         const values = this.newPoint.values;
 
         quality.match_set(this.matchSet_.id());
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
index 15a19aaa42..e57f34fae2 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
@@ -1,5 +1,5 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {IdlObject} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -20,7 +20,7 @@ export class MatchSetComponent implements OnInit {
         private pcrud: PcrudService,
         private org: OrgService
     ) {
-        this.route.paramMap.subscribe((params: ParamMap) => {                  
+        this.route.paramMap.subscribe((params: ParamMap) => {
             this.matchSetId = +params.get('id');
             this.matchSetTab = params.get('matchSetTab');
         });
@@ -42,7 +42,7 @@ export class MatchSetComponent implements OnInit {
         // prevent tab changing until after route navigation
         evt.preventDefault();
 
-        const url = 
+        const url =
           `/staff/cat/vandelay/match_sets/${this.matchSetId}/${this.matchSetTab}`;
 
         this.router.navigate([url]);
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
index d72a81bfa8..7340e3d719 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
@@ -1,9 +1,8 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
-import {map} from 'rxjs/operators/map';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
-import {Pager} from '@eg/share/util/pager';                                    
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
@@ -31,7 +30,7 @@ export class QueueItemsComponent {
         private auth: AuthService,
         private vandelay: VandelayService) {
 
-        this.route.paramMap.subscribe((params: ParamMap) => {                  
+        this.route.paramMap.subscribe((params: ParamMap) => {
             this.queueId = +params.get('id');
             this.queueType = params.get('qtype');
         });
@@ -54,7 +53,7 @@ export class QueueItemsComponent {
         this.limitToImportErrors = (checked: boolean) => {
             this.filterImportErrors = checked;
             this.itemsGrid.reload();
-        }
+        };
     }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
index 888c8a57f4..5edde1e026 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
@@ -1,9 +1,8 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
-import {map} from 'rxjs/operators/map';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
-import {Pager} from '@eg/share/util/pager';                                    
+import {Observable, of} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
@@ -42,7 +41,7 @@ export class QueueListComponent {
         // queue API does not support sorting
         this.queueSource.getRows = (pager: Pager) => {
             return this.loadQueues(pager);
-        }
+        };
 
         this.deleteSelected = (queues: IdlObject[]) => {
 
@@ -50,25 +49,26 @@ export class QueueListComponent {
             // because they can be bulky calls
             const qtype = this.queueType;
             const method = `open-ils.vandelay.${qtype}_queue.delete`;
+            const selected = queues.slice(0); // clone to be nice
 
-            const deleteNext = (queues: IdlObject[], idx: number) => {
-                const queue = queues[idx];
-                if (!queue) { 
+            const deleteNext = (idx: number) => {
+                const queue = selected[idx];
+                if (!queue) {
                     this.currentGrid().reload();
-                    return Promise.resolve(); 
+                    return Promise.resolve();
                 }
-    
-                return this.net.request('open-ils.vandelay', 
+
+                return this.net.request('open-ils.vandelay',
                     method, this.auth.token(), queue.id()
-                ).toPromise().then(() => deleteNext(queues, ++idx));
-            }
+                ).toPromise().then(() => deleteNext(++idx));
+            };
 
-            deleteNext(queues, 0);
+            deleteNext(0);
         };
     }
 
     currentGrid(): GridComponent {
-        // The active grid changes along with the queue type.  
+        // The active grid changes along with the queue type.
         // The inactive grid will be set to null.
         return this.bibQueueGrid || this.authQueueGrid;
     }
@@ -87,13 +87,13 @@ export class QueueListComponent {
     loadQueues(pager: Pager): Observable<any> {
 
         if (!this.queueType) {
-            return Observable.of();
+            return of();
         }
 
         const qtype = this.queueType.match(/bib/) ? 'bib' : 'authority';
         const method = `open-ils.vandelay.${qtype}_queue.owner.retrieve`;
 
-        return this.net.request('open-ils.vandelay', 
+        return this.net.request('open-ils.vandelay',
             method, this.auth.token(), null, null,
             {offset: pager.offset, limit: pager.limit}
         );
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
index 1b75f563ce..fbc62ba783 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
@@ -1,10 +1,8 @@
 import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
-import {map} from 'rxjs/operators/map';
-import {filter} from 'rxjs/operators/filter';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
-import {Pager} from '@eg/share/util/pager';                                    
+import {Observable} from 'rxjs';
+import {map, filter} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {EventService} from '@eg/core/event.service';
 import {NetService} from '@eg/core/net.service';
@@ -52,7 +50,7 @@ export class QueueComponent implements OnInit, AfterViewInit {
         private auth: AuthService,
         private vandelay: VandelayService) {
 
-        this.route.paramMap.subscribe((params: ParamMap) => {                  
+        this.route.paramMap.subscribe((params: ParamMap) => {
             this.queueType = params.get('qtype');
             this.queueId = +params.get('id');
         });
@@ -87,8 +85,8 @@ export class QueueComponent implements OnInit, AfterViewInit {
     }
 
     ngAfterViewInit() {
-        if (this.queueType) { 
-            this.applyQueueType(); 
+        if (this.queueType) {
+            this.applyQueueType();
             if (this.queueId) {
                 this.loadQueueSummary();
             }
@@ -96,10 +94,10 @@ export class QueueComponent implements OnInit, AfterViewInit {
     }
 
     openRecord(row: any) {
-        if (this.queueType == 'auth') {
+        if (this.queueType === 'auth') {
             this.queueType = 'authority';
         }
-        const url = 
+        const url =
           `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}/record/${row.id}/marc`;
         this.router.navigate([url]);
     }
@@ -126,11 +124,11 @@ export class QueueComponent implements OnInit, AfterViewInit {
     }
 
     qtypeShort(): string {
-        return this.queueType === 'bib' ? 'bib' : 'auth';    
+        return this.queueType === 'bib' ? 'bib' : 'auth';
     }
 
     loadQueueSummary(): Promise<any> {
-        const method = 
+        const method =
             `open-ils.vandelay.${this.qtypeShort()}_queue.summary.retrieve`;
 
         return this.net.request(
@@ -143,11 +141,11 @@ export class QueueComponent implements OnInit, AfterViewInit {
         const options = {
             clear_marc: true,
             offset: pager.offset,
-            limit: pager.limit, 
+            limit: pager.limit,
             flesh_import_items: true,
             non_imported: this.filters.nonImported,
             with_import_error: this.filters.withErrors
-        }
+        };
 
         return this.vandelay.getQueuedRecords(
             this.queueId, this.queueType, options, this.filters.matches).pipe(
@@ -158,7 +156,7 @@ export class QueueComponent implements OnInit, AfterViewInit {
             const e = this.evt.parse(rec);
             if (e) { console.error(e); return false; }
             return true;
-        }), 
+        }),
         map(rec => {
             const recHash: any = {
                 id: rec.id(),
@@ -176,10 +174,10 @@ export class QueueComponent implements OnInit, AfterViewInit {
                 recHash.error_items = rec.import_items().filter(i => i.import_error());
             }
 
-            // Link the record attribute values to the root record 
+            // Link the record attribute values to the root record
             // object so the grid can find them.
             rec.attributes().forEach(attr => {
-                const def = 
+                const def =
                     this.attrDefs.filter(d => d.id() === attr.field())[0];
                 recHash[def.code()] = attr.attr_value();
             });
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
index 74e70f1f9e..f50c48266d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
@@ -1,8 +1,7 @@
 import {Component, Input, OnInit, ViewChild} from '@angular/core';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
-import {Observable} from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
-import {map} from 'rxjs/operators/map';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, of} from 'rxjs';
+import {map} from 'rxjs/operators';
 import {Pager} from '@eg/share/util/pager';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
@@ -47,7 +46,7 @@ export class QueuedRecordMatchesComponent implements OnInit {
 
         this.bibDataSource.getRows = (pager: Pager) => {
             return this.getBibMatchRows(pager);
-        }
+        };
 
         /* TODO
         this.authDataSource.getRows = (pager: Pager) => {
@@ -57,7 +56,7 @@ export class QueuedRecordMatchesComponent implements OnInit {
         // Mark or un-mark as row as the merge target on row click
         this.matchRowClick = (row: any) => {
             this.toggleMergeTarget(row.id);
-        }
+        };
     }
 
     toggleMergeTarget(matchId: number) {
@@ -126,7 +125,7 @@ export class QueuedRecordMatchesComponent implements OnInit {
                                     vqbr_quality: this.queuedRecord.quality(),
                                     match_score: match.match_score(),
                                     bib_summary: bibSummaries[match.eg_record()]
-                                }
+                                };
 
                                 observer.next(row);
                             });
@@ -143,7 +142,7 @@ export class QueuedRecordMatchesComponent implements OnInit {
         if (this.queuedRecord) {
             return Promise.resolve('');
         }
-        let idlClass = this.queueType === 'bib' ? 'vqbr' : 'vqar';
+        const idlClass = this.queueType === 'bib' ? 'vqbr' : 'vqar';
         const flesh = {flesh: 1, flesh_fields: {}};
         flesh.flesh_fields[idlClass] = ['matches'];
         return this.pcrud.retrieve(idlClass, this.recordId, flesh)
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
index 3a37be74c8..7bbf5a8d8e 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
@@ -1,5 +1,5 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
-import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 
 @Component({
@@ -16,7 +16,7 @@ export class QueuedRecordComponent {
         private router: Router,
         private route: ActivatedRoute) {
 
-        this.route.paramMap.subscribe((params: ParamMap) => {                  
+        this.route.paramMap.subscribe((params: ParamMap) => {
             this.queueId = +params.get('id');
             this.recordId = +params.get('recordId');
             this.queueType = params.get('qtype');
@@ -32,7 +32,7 @@ export class QueuedRecordComponent {
         // prevent tab changing until after route navigation
         evt.preventDefault();
 
-        const url = 
+        const url =
           `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}` +
           `/record/${this.recordId}/${this.recordTab}`;
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
index ad7b0588e0..e8b1f735bb 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
@@ -71,10 +71,10 @@ export class RecentImportsComponent implements OnInit {
         this.pcrud.search('vst', query, {order_by: {vst: 'create_time'}})
         .subscribe(
             tracker => {
-                // The screen flickers less if the tracker array is 
+                // The screen flickers less if the tracker array is
                 // updated inline instead of rebuilt every time.
 
-                const existing = 
+                const existing =
                     this.trackers.filter(t => t.id() === tracker.id())[0];
 
                 if (existing) {
@@ -116,7 +116,7 @@ export class RecentImportsComponent implements OnInit {
             },
             err => {},
             ()  => {
-                const active = 
+                const active =
                     this.trackers.filter(t => t.state() === 'active');
 
                 // Continue updating the display with updated tracker
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
index 9852a640e8..d42f55d56d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
@@ -1,5 +1,5 @@
 import {Component, Input, ViewChild} from '@angular/core';
-import {Pager} from '@eg/share/util/pager';                                    
+import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -29,7 +29,7 @@ export class RecordItemsComponent {
 
         // queue API does not support sorting
         this.gridSource.getRows = (pager: Pager) => {
-            return this.pcrud.search('vii', 
+            return this.pcrud.search('vii',
                 {record: this.recordId}, {order_by: {vii: ['id']}});
         };
     }
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
index 0bfad42d5c..7a6b24d2d5 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
@@ -1,6 +1,6 @@
 import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
-import {Router, ActivatedRoute, NavigationEnd} from "@angular/router";
-import {take} from 'rxjs/operators/take';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {take} from 'rxjs/operators';
 import {VandelayService} from './vandelay.service';
 import {IdlObject} from '@eg/core/idl.service';
 
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
index 3f4a1973b9..cd52d44e1f 100644
--- 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
@@ -1,9 +1,8 @@
 import {Injectable} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import {tap} from 'rxjs/operators/tap';
-import {map} from 'rxjs/operators/map';
+import {Observable} from 'rxjs';
+import {tap, map} from 'rxjs/operators';
 import {HttpClient} from '@angular/common/http';
-import {saveAs} from 'file-saver/FileSaver';
+import {saveAs} from 'file-saver';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
@@ -73,8 +72,8 @@ export class VandelayService {
         }
         const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad';
         const orderBy = {};
-        orderBy[cls] = 'id'
-        return this.pcrud.retrieveAll(cls, 
+        orderBy[cls] = 'id';
+        return this.pcrud.retrieveAll(cls,
             {order_by: orderBy}, {atomic: true}).toPromise()
         .then(list => {
             this.attrDefs[dtype] = list;
@@ -88,7 +87,7 @@ export class VandelayService {
         }
 
         const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
-        return this.pcrud.search('vmp', 
+        return this.pcrud.search('vmp',
             {owner: owners}, {order_by: {vmp: ['name']}}, {atomic: true})
         .toPromise().then(profiles => {
             this.mergeProfiles = profiles;
@@ -119,8 +118,8 @@ export class VandelayService {
             return Promise.resolve(this.bibSources);
         }
 
-        return this.pcrud.retrieveAll('cbs', 
-          {order_by: {cbs: 'id'}}, 
+        return this.pcrud.retrieveAll('cbs',
+          {order_by: {cbs: 'id'}},
           {atomic: true}
         ).toPromise().then(sources => {
             this.bibSources = sources;
@@ -143,7 +142,7 @@ export class VandelayService {
 
     // todo: differentiate between biblio and authority a la queue api
     getMatchSets(mtype: string): Promise<IdlObject[]> {
-    
+
         const mstype = mtype.match(/bib/) ? 'biblio' : 'authority';
 
         if (this.matchSets[mtype]) {
@@ -154,7 +153,7 @@ export class VandelayService {
 
         const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
 
-        return this.pcrud.search('vms', 
+        return this.pcrud.search('vms',
             {owner: owners, mtype: mstype}, {}, {atomic: true})
         .toPromise().then(sets => {
             this.matchSets[mtype] = sets;
@@ -167,12 +166,10 @@ export class VandelayService {
             return Promise.resolve(this.bibBuckets);
         }
 
-        const bkts = [];
         return this.net.request(
-            'open-ils.actor', 
+            'open-ils.actor',
             'open-ils.actor.container.retrieve_by_class',
             this.auth.token(), this.auth.user().id(), 'biblio', 'staff_client'
-        //).pipe(tap(bkt => bkts.push(bkt))).toPromise().then(() => bkts);
         ).toPromise().then(bkts => {
             this.bibBuckets = bkts;
             return bkts;
@@ -197,8 +194,8 @@ export class VandelayService {
 
         const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
 
-        return this.pcrud.search('vibtg', 
-            {always_apply : 'f', owner: owners}, 
+        return this.pcrud.search('vibtg',
+            {always_apply : 'f', owner: owners},
             {vibtg : ['label']},
             {atomic: true}
         ).toPromise().then(groups => {
@@ -210,31 +207,31 @@ export class VandelayService {
 
     // Create a queue and return the ID of the new queue via promise.
     createQueue(
-        queueName: string, 
-        recordType: string, 
-        importDefId: number, 
-        matchSet: number, 
+        queueName: string,
+        recordType: string,
+        importDefId: number,
+        matchSet: number,
         matchBucket: number): Promise<number> {
 
         const method = `open-ils.vandelay.${recordType}_queue.create`;
 
         let qType = recordType;
         if (recordType.match(/bib_acq/)) {
-            let qType = 'acq';
+            qType = 'acq';
         }
 
         return new Promise((resolve, reject) => {
             this.net.request(
-                'open-ils.vandelay', method, 
-                this.auth.token(), queueName, null, qType, 
+                'open-ils.vandelay', method,
+                this.auth.token(), queueName, null, qType,
                 matchSet, importDefId, matchBucket
             ).subscribe(queue => {
                 const e = this.evt.parse(queue);
-                if (e) { 
+                if (e) {
                     reject(e);
                 } else {
                     // createQueue is always called after queues have
-                    // been fetched and cached.  
+                    // been fetched and cached.
                     this.allQueues[qType].push(queue);
                     resolve(queue.id());
                 }
@@ -242,20 +239,20 @@ export class VandelayService {
         });
     }
 
-    getQueuedRecords(queueId: number, queueType: string, 
+    getQueuedRecords(queueId: number, queueType: string,
       options?: any, limitToMatches?: boolean): Observable<any> {
 
         const qtype = queueType.match(/bib/) ? 'bib' : 'auth';
 
-        let method = 
+        let method =
           `open-ils.vandelay.${qtype}_queue.records.retrieve`;
 
         if (limitToMatches) {
-            method = 
+            method =
               `open-ils.vandelay.${qtype}_queue.records.matches.retrieve`;
         }
 
-        return this.net.request('open-ils.vandelay', 
+        return this.net.request('open-ils.vandelay',
             method, this.auth.token(), queueId, options);
     }
 
@@ -264,11 +261,11 @@ export class VandelayService {
 
         const etype = queue.queue_type().match(/auth/) ? 'auth' : 'bib';
 
-        let url = 
-          `${VANDELAY_EXPORT_PATH}?type=${etype}&queueid=${queue.id()}`
+        let url =
+          `${VANDELAY_EXPORT_PATH}?type=${etype}&queueid=${queue.id()}`;
 
         let saveName = queue.name();
-           
+
         if (nonImported) {
             url += '&nonimported=1';
             saveName += '_nonimported';
@@ -289,7 +286,7 @@ export class VandelayService {
         );
     }
 
-    // Poll every 2 seconds for session tracker updates so long 
+    // Poll every 2 seconds for session tracker updates so long
     // as the session tracker is active.
     // Returns an Observable of tracker objects.
     pollSessionTracker(id: number): Observable<IdlObject> {
@@ -300,13 +297,13 @@ export class VandelayService {
 
     getNextSessionTracker(id: number, observer: any) {
 
-		// No need for this to be an authoritative call.
+        // No need for this to be an authoritative call.
         // It will complete eventually regardless.
         this.pcrud.retrieve('vst', id).subscribe(
             tracker => {
                 if (tracker && tracker.state() === 'active') {
                     observer.next(tracker);
-                    setTimeout(() => 
+                    setTimeout(() =>
                         this.getNextSessionTracker(id, observer), 2000);
                 } else {
                     console.debug(
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
index eb99aec3be..2270081148 100644
--- 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
@@ -49,7 +49,7 @@ export class RecordBucketDialogComponent
     }
 
     ngOnInit() {
-        
+
         if (this.qId) {
             this.bucketType = 'vandelay_queue';
         } else {

commit a3d3e73a752612d435c54a0ca8a8dca5c8fe40a6
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Jan 8 10:57:53 2019 -0500

    LP1779158 Import field id/class mismatch fix
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
index 5f7352f8b8..c7f8c17ab1 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
@@ -202,8 +202,8 @@
       </label>
     </div>
     <div class="col-lg-3">
-      <input id="form-check-input" type="checkbox" 
-        name="auto-overlay-acq-copies"
+      <input class="form-check-input" type="checkbox" 
+        id="auto-overlay-acq-copies"
         [disabled]="recordType == 'authority'"
         [(ngModel)]="autoOverlayAcqCopies">
     </div>

commit 1b5985fc636be0748504201f275bf548b376ceeb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jan 7 16:35:15 2019 -0500

    LP1779158 Always clear import selection
    
    Clear the import selection (e.g. Queue => Import All Items) from the
    import UI when the user navigates away, regardless of whether the import
    succeeded.  Otherwise, the import UI gets stuck in selection mode
    requiring the user to manually clear it.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index e3c2d3ded3..9732e5abd6 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -180,11 +180,9 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     }
 
     ngOnDestroy() {
-        // If we successfully completed the most recent 
-        // upload/import assume the importSelection can be cleared.
-        if (this.uploadComplete) {
-            this.clearSelection();
-        }
+        // Always clear the import selection when navigating away from
+        // the import page.
+        this.clearSelection();
     }
 
     importSelection(): VandelayImportSelection {

commit 65eee056f8165ccc97070e9ba3b16bab054e8834
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jan 7 16:23:12 2019 -0500

    LP1779158 Vandelay workstation setting repairs/additions
    
    * Fix some workstation setting names that were missing the 'eg.grid.*'
      prefix
    * Add workstation setting types for merge profile and attr definition
      admin pages.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index c10e34fe62..ab3568eaee 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -19436,47 +19436,68 @@ VALUES (
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.match_set.list', 'gui', 'object',
+    'eg.grid.cat.vandelay.match_set.list', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.match_set.list',
+        'eg.grid.cat.vandelay.match_set.list',
         'Grid Config: Vandelay Match Sets',
         'cwst', 'label'
     )
 ), (
-    'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+    'eg.grid.cat.vandelay.match_set.quality', 'gui', 'object',
     oils_i18n_gettext(
-        'staff.cat.vandelay.match_set.quality',
+        'eg.grid.cat.vandelay.match_set.quality',
         'Grid Config: Vandelay Match Quality Metrics',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.items', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.items', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.items',
+        'eg.grid.cat.vandelay.queue.items',
         'Grid Config: Vandelay Queue Import Items',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.list.bib', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.list.bib', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.list.bib',
+        'eg.grid.cat.vandelay.queue.list.bib',
         'Grid Config: Vandelay Bib Queue List',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.bib.items', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.bib.items', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.bib.items',
+        'eg.grid.cat.vandelay.queue.bib.items',
         'Grid Config: Vandelay Bib Items',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.list.auth', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.list.auth', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.list.auth',
+        'eg.grid.cat.vandelay.queue.list.auth',
         'Grid Config: Vandelay Authority Queue List',
         'cwst', 'label'
     )
+), (
+    'eg.grid.admin.vandelay.merge_profile', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.merge_profile',
+        'Grid Config: Vandelay Merge Profiles',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.vandelay.bib_attr_definition', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.bib_attr_definition',
+        'Grid Config: Vandelay Bib Record Attributes',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.vandelay.import_item_attr_definition', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.import_item_attr_definition',
+        'Grid Config: Vandelay Import Item Attributes',
+        'cwst', 'label'
+    )
 );
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
index 4eb2206201..35726a4867 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
@@ -11,56 +11,78 @@ VALUES (
         'cwst', 'label'
     )
 ), (
-    'eg.grid.cat.vandelay.queue.auth', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.authority', 'gui', 'object',
     oils_i18n_gettext(
-        'eg.grid.cat.vandelay.queue.auth',
+        'eg.grid.cat.vandelay.queue.authority',
         'Grid Config: Vandelay Authority Queue',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.match_set.list', 'gui', 'object',
+    'eg.grid.cat.vandelay.match_set.list', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.match_set.list',
+        'eg.grid.cat.vandelay.match_set.list',
         'Grid Config: Vandelay Match Sets',
         'cwst', 'label'
     )
 ), (
-    'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+    'eg.grid.cat.vandelay.match_set.quality', 'gui', 'object',
     oils_i18n_gettext(
-        'staff.cat.vandelay.match_set.quality',
+        'eg.grid.cat.vandelay.match_set.quality',
         'Grid Config: Vandelay Match Quality Metrics',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.items', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.items', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.items',
+        'eg.grid.cat.vandelay.queue.items',
         'Grid Config: Vandelay Queue Import Items',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.list.bib', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.list.bib', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.list.bib',
+        'eg.grid.cat.vandelay.queue.list.bib',
         'Grid Config: Vandelay Bib Queue List',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.bib.items', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.bib.items', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.bib.items',
+        'eg.grid.cat.vandelay.queue.bib.items',
         'Grid Config: Vandelay Bib Items',
         'cwst', 'label'
     )
 ), (
-    'cat.vandelay.queue.list.auth', 'gui', 'object',
+    'eg.grid.cat.vandelay.queue.list.auth', 'gui', 'object',
     oils_i18n_gettext(
-        'cat.vandelay.queue.list.auth',
+        'eg.grid.cat.vandelay.queue.list.auth',
         'Grid Config: Vandelay Authority Queue List',
         'cwst', 'label'
     )
+), (
+    'eg.grid.admin.vandelay.merge_profile', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.merge_profile',
+        'Grid Config: Vandelay Merge Profiles',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.vandelay.bib_attr_definition', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.bib_attr_definition',
+        'Grid Config: Vandelay Bib Record Attributes',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.vandelay.import_item_attr_definition', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.vandelay.import_item_attr_definition',
+        'Grid Config: Vandelay Import Item Attributes',
+        'cwst', 'label'
+    )
 );
 
+
 COMMIT;
 
 

commit 667b1c6ddea74f66406eff7abdc184a559a27a99
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jan 7 16:04:41 2019 -0500

    LP1779158 Import non-matching saved template repair
    
    Fix issue in Vandelay import form templates where template variable name
    did not match the form variable name.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index e9eb3f8e17..e3c2d3ded3 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -24,7 +24,7 @@ const TEMPLATE_ATTRS = [
     'selectedBibSource',
     'selectedMatchSet',
     'mergeOnExact',
-    'importNonMatch',
+    'importNonMatching',
     'mergeOnBestMatch',
     'mergeOnSingleMatch',
     'autoOverlayAcqCopies',

commit a3bae111c13dfbae6d8555db0356e277344b8a3b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jan 4 11:07:31 2019 -0500

    LP1779158 Cache new queues / display active queues
    
    * Only list active (non-complete) queues in the Vandelay queue selector
      combobox.
    * Display an alert message when the user attempts to create a queue
      whose name collides with an existing queue for the user.
    * Be sure newly created queues are added to the local cache of queues in
      the Vandelay service.
    * Remove unused activeQueue list from vandelay.service to avoid having
      to maintain 2 separate queue caches.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
index a00b35fcff..5f7352f8b8 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
@@ -8,6 +8,10 @@
   </div>
 </div>
 
+<eg-alert-dialog #dupeQueueAlert i18n-dialogBody 
+  dialogBody="A queue with the requested name already exists.">
+</eg-alert-dialog>
+
 <h2 i18n>MARC File Upload</h2>
 <div class="common-form striped-odd form-validated ml-3 mr-3">
   <div class="row">
@@ -74,7 +78,7 @@
       <label for="queue-select" i18n>Select or Create a Queue</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('allQueues')"
+      <eg-combobox [entries]="formatEntries('activeQueues')"
         id="queue-select"
         [startId]="startQueueId"
         [startIdFiresOnChange]="true"
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index 185d88bd67..e9eb3f8e17 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -13,6 +13,7 @@ import {VandelayService, VandelayImportSelection,
 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
 import {Subject} from 'rxjs/Subject';
 import {ServerStoreService} from '@eg/core/server-store.service';
 
@@ -126,6 +127,9 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     @ViewChild('fallThruMergeProfileSelector')
         private fallThruMergeProfileSelector: ComboboxComponent;
 
+    @ViewChild('dupeQueueAlert')
+        private dupeQueueAlert: AlertDialogComponent;
+
     constructor(
         private http: HttpClient,
         private toast: ToastService,
@@ -248,8 +252,9 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
                 list = this.vandelay.bibBuckets;
                 break;
 
-            case 'allQueues':
-                list = this.vandelay.allQueues[rtype];
+            case 'activeQueues':
+                list = (this.vandelay.allQueues[rtype] || [])
+                        .filter(q => q.complete() === 'f');
                 break;
 
             case 'matchSets':
@@ -372,9 +377,6 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     resolveQueue(): Promise<number> {
 
         if (this.selectedQueue.freetext) {
-        /*
-        if (this.selectedQueue && this.selectedQueue.freetext) {
-        */
             // Free text queue selector means create a new entry.
             // TODO: first check for name dupes
 
@@ -384,15 +386,23 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
                 this.selectedHoldingsProfile,
                 this.selectedMatchSet,
                 this.selectedBucket
-            );
+            ).then(
+                id => id,
+                err => {
+                    const evt = this.evt.parse(err);
+                    if (evt) {
+                        if (evt.textcode.match(/QUEUE_EXISTS/)) {
+                            this.dupeQueueAlert.open();
+                        } else {
+                            alert(evt); // server error
+                        }
+                    } 
 
+                    return Promise.reject('Queue Create Failed');
+                }
+            );
         } else {
             return Promise.resolve(this.selectedQueue.id);
-            /*
-            var queue_id = this.startQueueId;
-            if (this.selectedQueue) queue_id = this.selectedQueue.id;
-            return Promise.resolve(queue_id);
-            */
         }
     }
 
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
index 45d1de556d..3f4a1973b9 100644
--- 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
@@ -1,4 +1,4 @@
-import {Injectable, EventEmitter} from '@angular/core';
+import {Injectable} from '@angular/core';
 import {Observable} from 'rxjs/Observable';
 import {tap} from 'rxjs/operators/tap';
 import {map} from 'rxjs/operators/map';
@@ -32,7 +32,6 @@ export class VandelayImportSelection {
 export class VandelayService {
 
     allQueues: {[qtype: string]: IdlObject[]};
-    activeQueues: {[qtype: string]: IdlObject[]}; 
     attrDefs: {[atype: string]: IdlObject[]};
     bibSources: IdlObject[];
     bibBuckets: IdlObject[];
@@ -62,7 +61,6 @@ export class VandelayService {
         private perm: PermService
     ) {
         this.attrDefs = {};
-        this.activeQueues = {};
         this.allQueues = {};
         this.matchSets = {};
         this.importSelection = null;
@@ -99,8 +97,6 @@ export class VandelayService {
     }
 
     // Returns a promise resolved with the list of queues.
-    // Also emits the onQueueListUpdate event so listeners
-    // can detect queue content changes.
     getAllQueues(qtype: string): Promise<IdlObject[]> {
         if (this.allQueues[qtype]) {
             return Promise.resolve(this.allQueues[qtype]);
@@ -118,27 +114,6 @@ export class VandelayService {
         )).toPromise().then(() => this.allQueues[qtype]);
     }
 
-
-    // Returns a promise resolved with the list of queues.
-    // Also emits the onQueueListUpdate event so listeners
-    // can detect queue content changes.
-    getActiveQueues(qtype: string): Promise<IdlObject[]> {
-        if (this.activeQueues[qtype]) {
-            return Promise.resolve(this.activeQueues[qtype]);
-        } else {
-            this.activeQueues[qtype] = [];
-        }
-
-        // could be a big list, invoke in streaming mode
-        return this.net.request(
-            'open-ils.vandelay',
-            `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
-            this.auth.token(), null, {complete: 'f'}
-        ).pipe(tap(
-            queue => this.activeQueues[qtype].push(queue)
-        )).toPromise().then(() => this.activeQueues[qtype]);
-    }
-
     getBibSources(): Promise<IdlObject[]> {
         if (this.bibSources) {
             return Promise.resolve(this.bibSources);
@@ -256,9 +231,11 @@ export class VandelayService {
             ).subscribe(queue => {
                 const e = this.evt.parse(queue);
                 if (e) { 
-                    alert(e);
                     reject(e);
                 } else {
+                    // createQueue is always called after queues have
+                    // been fetched and cached.  
+                    this.allQueues[qType].push(queue);
                     resolve(queue.id());
                 }
             });

commit 03268d65d2699b7b402aff0037e48e6566a331f8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jan 4 11:05:17 2019 -0500

    LP1779158 Angular alert dialog component
    
    Similar to the PromptDialogComponent except it displays a simple message
    (formatted as 'danger') and the only option available to the user is to
    close the dialog.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
index c83ad392bc..9361042074 100644
--- a/Open-ILS/src/eg2/src/app/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -19,6 +19,7 @@ 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 {AlertDialogComponent} from '@eg/share/dialog/alert.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';
@@ -28,6 +29,7 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
   declarations: [
     PrintComponent,
     DialogComponent,
+    AlertDialogComponent,
     ConfirmDialogComponent,
     PromptDialogComponent,
     ProgressInlineComponent,
@@ -46,6 +48,7 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
     FormsModule,
     PrintComponent,
     DialogComponent,
+    AlertDialogComponent,
     ConfirmDialogComponent,
     PromptDialogComponent,
     ProgressInlineComponent,
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html
new file mode 100644
index 0000000000..e81f697c6e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html
@@ -0,0 +1,9 @@
+<ng-template #dialogContent>
+  <div class="modal-body">
+    <div class="alert alert-danger">{{dialogBody}}</div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close()" i18n>OK</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts
new file mode 100644
index 0000000000..a09e972541
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts
@@ -0,0 +1,18 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+ at Component({
+  selector: 'eg-alert-dialog',
+  templateUrl: './alert.component.html'
+})
+
+/**
+ * Alertation dialog that requests user input.
+ */
+export class AlertDialogComponent extends DialogComponent {
+
+    // What are we warning the user with?
+    @Input() public dialogBody: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index cd51da658d..185d88bd67 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -371,7 +371,10 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     // Extract selected queue ID or create a new queue when requested.
     resolveQueue(): Promise<number> {
 
+        if (this.selectedQueue.freetext) {
+        /*
         if (this.selectedQueue && this.selectedQueue.freetext) {
+        */
             // Free text queue selector means create a new entry.
             // TODO: first check for name dupes
 
@@ -384,9 +387,12 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             );
 
         } else {
+            return Promise.resolve(this.selectedQueue.id);
+            /*
             var queue_id = this.startQueueId;
             if (this.selectedQueue) queue_id = this.selectedQueue.id;
             return Promise.resolve(queue_id);
+            */
         }
     }
 

commit 169cd66fe4589ff742511aa8e2bbc353f6b1c253
Author: Kyle Huckins <khuckins at catalyte.io>
Date:   Tue Dec 18 22:57:21 2018 +0000

    lp1779158 Inspect Queue Improvements
    
    - Add "Delete selected" option to Authority Queues
    - Change Queuetype to 'authority' from 'auth' when selecting single record to view
    in auth record queue.
    - Ensure "Imported As" link correctly navigates to Auth record view for Auth Queues
    
    Signed-off-by: Kyle Huckins <khuckins at catalyte.io>
    
     Changes to be committed:
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
index 6aface5418..badc6df9e4 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
@@ -31,6 +31,8 @@
   persistKey="cat.vandelay.queue.list.auth"
   (onRowActivate)="rowActivated($event)"
   idlClass="vaq" [dataSource]="queueSource">
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    [action]="deleteSelected"></eg-grid-toolbar-action>
 </eg-grid>
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
index bd991070ef..5584700eed 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
@@ -111,9 +111,13 @@
 </ng-template>
 
 <ng-template #importedAsTmpl let-row="row">
-  <a routerLink="/staff/catalog/record/{{row.imported_as}}">
+  <a *ngIf="queueType=='bib'" routerLink="/staff/catalog/record/{{row.imported_as}}">
     {{row.imported_as}}
   </a>
+  <a *ngIf="queueType=='auth'" href="/eg/staff/cat/catalog/authority/{{row.imported_as}}/marc_edit">
+    {{row.imported_as}}
+  </a>
+  
 </ng-template>
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
index 034f39acb5..1b75f563ce 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
@@ -96,6 +96,9 @@ export class QueueComponent implements OnInit, AfterViewInit {
     }
 
     openRecord(row: any) {
+        if (this.queueType == 'auth') {
+            this.queueType = 'authority';
+        }
         const url = 
           `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}/record/${row.id}/marc`;
         this.router.navigate([url]);

commit 48576fe0d0a648bee37de68fc7beba3585cbe7dc
Author: Kyle Huckins <khuckins at catalyte.io>
Date:   Tue Nov 27 21:07:35 2018 +0000

    lp1779158 Vandelay Authority Import Tweaks
    
    - Check for "Auth" string
    - Dummy rec.import_items when record is not bib
    - Teach resolveQueue() to assume startQueueId unless a queue is selected already
    
    Signed-off-by: Kyle Huckins <khuckins at catalyte.io>
    
     Changes to be committed:
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
            modified:   Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index 74341b929a..cd51da658d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -371,7 +371,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     // Extract selected queue ID or create a new queue when requested.
     resolveQueue(): Promise<number> {
 
-        if (this.selectedQueue.freetext) {
+        if (this.selectedQueue && this.selectedQueue.freetext) {
             // Free text queue selector means create a new entry.
             // TODO: first check for name dupes
 
@@ -384,7 +384,9 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             );
 
         } else {
-            return Promise.resolve(this.selectedQueue.id);
+            var queue_id = this.startQueueId;
+            if (this.selectedQueue) queue_id = this.selectedQueue.id;
+            return Promise.resolve(queue_id);
         }
     }
 
@@ -434,8 +436,10 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             // Nothing to enqueue when processing pre-queued records
             return Promise.resolve();
         }
+        var spoolType = this.recordType;
+        if (this.recordType == 'authority') spoolType = 'auth'
 
-        const method = `open-ils.vandelay.${this.recordType}.process_spool`;
+        const method = `open-ils.vandelay.${spoolType}.process_spool`;
 
         return new Promise((resolve, reject) => {
             this.net.request(
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
index a6f67c34d1..034f39acb5 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
@@ -163,11 +163,16 @@ export class QueueComponent implements OnInit, AfterViewInit {
                 error_detail: rec.error_detail(),
                 import_time: rec.import_time(),
                 imported_as: rec.imported_as(),
-                import_items: rec.import_items(),
-                error_items: rec.import_items().filter(i => i.import_error()),
+                import_items: [],
+                error_items: [],
                 matches: rec.matches()
             };
 
+            if (this.queueType === 'bib') {
+                recHash.import_items = rec.import_items();
+                recHash.error_items = rec.import_items().filter(i => i.import_error());
+            }
+
             // Link the record attribute values to the root record 
             // object so the grid can find them.
             rec.attributes().forEach(attr => {
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
index 7a6d6405e4..45d1de556d 100644
--- 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
@@ -244,7 +244,7 @@ export class VandelayService {
         const method = `open-ils.vandelay.${recordType}_queue.create`;
 
         let qType = recordType;
-        if (recordType.match(/acq/)) {
+        if (recordType.match(/bib_acq/)) {
             let qType = 'acq';
         }
 

commit f2504b21de1eb5e3eae3047c2de0dc24b8999583
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Dec 4 14:13:42 2018 -0500

    LP#1779158 Vandelay UI accessibilty/usability improvements
    
    * Associate <input>'s with their labels in the import and export forms
    * Hide the MARC removal groups selector when record type is authority.
    * Disable ACQ copy overlay option when record type is authority.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
index 020e09748b..53f36e6813 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
@@ -10,7 +10,7 @@
           <ng-template ngbPanelContent>
             <div class="row">
               <div class="col-lg-6">
-                <label i18n>Use Field Number</label>
+                <label for="csv-input" i18n>Use Field Number</label>
               </div>
               <div class="col-lg-6">
                 <input id='csv-input' type="number" class="form-control" 
@@ -20,11 +20,11 @@
             </div>
             <div class="row">
               <div class="col-lg-6">
-                <label i18n>From CSV file</label>
+                <label for="use-csv-file" i18n>From CSV file</label>
               </div>
               <div class="col-lg-6">
                 <input #fileSelector (change)="fileSelected($event)" 
-                  class="form-control" type="file"/>
+                  id="use-csv-file" class="form-control" type="file"/>
               </div>
             </div>
           </ng-template>
@@ -33,7 +33,7 @@
           <ng-template ngbPanelContent>
             <div class="row">
               <div class="col-lg-6">
-                <label i18n>Record ID</label>
+                <label for="record-id-input" i18n>Record ID</label>
               </div>
               <div class="col-lg-6">
                 <input id='record-id-input' type="number" 
@@ -46,7 +46,7 @@
           <ng-template ngbPanelContent>
             <div class="row">
               <div class="col-lg-6">
-                <label i18n>Bucket ID</label>
+                <label for="bucket-id-input" i18n>Bucket ID</label>
               </div>
               <div class="col-lg-6">
                 <input id='bucket-id-input' type="number" 
@@ -60,10 +60,11 @@
     <div class="col-lg-6">
       <div class="row">
         <div class="col-lg-6">
-          <label i18n>Record Type</label>
+          <label for="record-type" i18n>Record Type</label>
         </div>
         <div class="col-lg-6">
-          <select class="form-control" [(ngModel)]="recordType">
+          <select class="form-control" 
+            [(ngModel)]="recordType" id="record-type">
             <option i18n value="biblio">Bibliographic Records</option>
             <option i18n value="authority">Authority Records</option>
           </select>
@@ -71,10 +72,11 @@
       </div>
       <div class="row">
         <div class="col-lg-6">
-          <label i18n>Record Format</label>
+          <label for="record-format" i18n>Record Format</label>
         </div>
         <div class="col-lg-6">
-          <select class="form-control" [(ngModel)]="recordFormat">
+          <select class="form-control" 
+            [(ngModel)]="recordFormat" id="record-format">
             <option i18n value="USMARC">MARC21</option>
             <option i18n value="UNIMARC">UNIMARC</option>
             <option i18n value="XML">MARC XML</option>
@@ -84,10 +86,11 @@
       </div>
       <div class="row">
         <div class="col-lg-6">
-          <label i18n>Record Encoding</label>
+          <label for="record-encoding" i18n>Record Encoding</label>
         </div>
         <div class="col-lg-6">
-          <select class="form-control" [(ngModel)]="recordEncoding">
+          <select class="form-control" 
+            [(ngModel)]="recordEncoding" id="record-encoding">
             <option i18n value="UTF-8">UTF-8</option>
             <option i18n value="MARC8">MARC8</option>
           </select>
@@ -95,10 +98,13 @@
       </div>
       <div class="row">
         <div class="col-lg-6">
-          <label i18n>Include holdings in Bibliographic Records</label>
+          <label for="include-holdings" i18n>
+            Include holdings in Bibliographic Records
+          </label>
         </div>
         <div class="col-lg-6">
-          <input class="form-check-input" type="checkbox" [(ngModel)]="includeHoldings">
+          <input class="form-check-input" type="checkbox" 
+            [(ngModel)]="includeHoldings" id="include-holdings">
         </div>
       </div>
       <div class="row">
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
index 58b3bb015d..a00b35fcff 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
@@ -12,10 +12,11 @@
 <div class="common-form striped-odd form-validated ml-3 mr-3">
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Apply/Create Form Template</label>
+      <label for="template-select" i18n>Apply/Create Form Template</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox #formTemplateSelector
+        id="template-select"
         (onChange)="templateSelectorChange($event)"
         [allowFreeText]="true"
         [startId]="selectedTemplate"
@@ -39,10 +40,11 @@
 
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Record Type</label>
+      <label for="type-select" i18n>Record Type</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox #recordTypeSelector
+        id="type-select"
         (onChange)="selectEntry($event, 'recordType')"
         [disabled]="importSelection()" [required]="true"
         [startId]="recordType" placeholder="Record Type..." i18n-placeholder>
@@ -55,10 +57,11 @@
       </eg-combobox>
     </div>
     <div class="col-lg-3">
-      <label i18n>Select a Record Source</label>
+      <label for="source-select" i18n>Select a Record Source</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox #bibSourceSelector
+        id="source-select"
         [entries]="formatEntries('bibSources')" 
         (onChange)="selectEntry($event, 'bibSources')"
         [startId]="selectedBibSource"
@@ -68,10 +71,11 @@
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Select or Create a Qeueue</label>
+      <label for="queue-select" i18n>Select or Create a Queue</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox [entries]="formatEntries('allQueues')"
+        id="queue-select"
         [startId]="startQueueId"
         [startIdFiresOnChange]="true"
         [disabled]="startQueueId"
@@ -81,10 +85,11 @@
       </eg-combobox>
     </div>
     <div class="col-lg-3">
-      <label i18n>Limit Matches to Bucket</label>
+      <label for="bucket-select" i18n>Limit Matches to Bucket</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox [entries]="formatEntries('bibBuckets')" 
+        id="bucket-select"
         [startId]="selectedBucket"
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         (onChange)="selectEntry($event, 'bibBuckets')"
@@ -93,28 +98,33 @@
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Record Match Set</label>
+      <label for="match-set-select" i18n>Record Match Set</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox #matchSetSelector
+        id="match-set-select"
         [entries]="formatEntries('matchSets')" 
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         [startId]="selectedMatchSet || defaultMatchSet"
         (onChange)="selectEntry($event, 'matchSets')"
         placeholder="Match Set..." i18n-placeholder></eg-combobox>
     </div>
-    <div class="col-lg-3"><label i18n>Import Non-Matching Records</label></div>
+    <div class="col-lg-3">
+        <label for="import-non-matching" i18n>Import Non-Matching Records</label>
+    </div>
     <div class="col-lg-3">
       <input class="form-check-input" type="checkbox" 
+        id="import-non-matching"
         [(ngModel)]="importNonMatching">
     </div>
  </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Holdings Import Profile</label>
+      <label for="item-import-def" i18n>Holdings Import Profile</label>
     </div>
     <div class="col-lg-3"> <!-- TODO disable for authority -->
       <eg-combobox #holdingsProfileSelector
+        id="item-import-def"
         [entries]="formatEntries('importItemDefs')"
         [startId]="selectedHoldingsProfile"
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
@@ -122,88 +132,109 @@
         placeholder="Holdings Import Profile..." i18n-placeholder>
       </eg-combobox>
     </div>
-    <div class="col-lg-3"><label i18n>Merge On Exact Match (901c)</label></div>
+    <div class="col-lg-3">
+        <label for="merge-on-exact" i18n>Merge On Exact Match (901c)</label>
+    </div>
     <div class="col-lg-3">
       <input class="form-check-input" type="checkbox" 
-        [(ngModel)]="mergeOnExact">
+        id="merge-on-exact" [(ngModel)]="mergeOnExact">
     </div>
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Merge Profile</label>
+      <label for="merge-profiles" i18n>Merge Profile</label>
     </div>
     <div class="col-lg-3">
       <eg-combobox #mergeProfileSelector
+        id="merge-profiles"
         [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'mergeProfiles')"
         placeholder="Merge Profile..." i18n-placeholder>
       </eg-combobox>
     </div>
-    <div class="col-lg-3"><label i18n>Merge On Single Match</label></div>
+    <div class="col-lg-3">
+        <label for="merge-on-single" i18n>Merge On Single Match</label>
+    </div>
     <div class="col-lg-3">
       <input class="form-check-input" type="checkbox" 
-        [(ngModel)]="mergeOnSingleMatch">
+        id="merge-on-single" [(ngModel)]="mergeOnSingleMatch">
     </div>
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Insufficient Quality Fall-Through Profile</label></div>
+      <label for="insuff-merge-profiles" i18n>
+        Insufficient Quality Fall-Through Profile
+    </label>
+    </div>
     <div class="col-lg-3">
       <eg-combobox #fallThruMergeProfileSelector
+        id="insuff-merge-profiles"
         [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'FallThruMergeProfile')"
         placeholder="Fall-Through Merge Profile..." i18n-placeholder>
       </eg-combobox>
     </div>
-    <div class="col-lg-3"><label i18n>Merge On Best Match</label></div>
+    <div class="col-lg-3">
+      <label for="merge-on-best" i18n>Merge On Best Match</label>
+    </div>
     <div class="col-lg-3">
       <input class="form-check-input" type="checkbox" 
-        [(ngModel)]="mergeOnBestMatch">
+        id="merge-on-best" [(ngModel)]="mergeOnBestMatch">
     </div>
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Best/Single Match Minimum Quality Ratio</label></div>
+      <label for="min-quality-ratio" i18n>
+        Best/Single Match Minimum Quality Ratio
+      </label>
+    </div>
     <div class="col-lg-3">
-      <input type="number" step="0.1" 
+      <input type="number" step="0.1" id="min-quality-ratio" 
         class="form-control" [(ngModel)]="minQualityRatio">
     </div>
     <div class="col-lg-3">
-      <label i18n>Auto-overlay In-process Acquisitions Copies</label></div>
+      <label for="auto-overlay-acq-copies" i18n>
+        Auto-overlay In-process Acquisitions Copies
+      </label>
+    </div>
     <div class="col-lg-3">
-      <input class="form-check-input" type="checkbox" 
+      <input id="form-check-input" type="checkbox" 
+        name="auto-overlay-acq-copies"
+        [disabled]="recordType == 'authority'"
         [(ngModel)]="autoOverlayAcqCopies">
     </div>
   </div>
   <div class="row">
     <div class="col-lg-3">
-      <label i18n>Optional Session Name:</label>
+      <label for="session-name" i18n>Optional Session Name:</label>
     </div>
     <div class="col-lg-3">
       <input [(ngModel)]="sessionName" class="form-control" type="text"
-        i18n-placeholder placeholder="Session Name..."/>
+        name="session-name" i18n-placeholder placeholder="Session Name..."/>
     </div>
     <div class="col-lg-3">
-      <label i18n>Remove MARC Field Groups</label>
+      <label for="marc-remove-groups" i18n>Remove MARC Field Groups</label>
     </div>
-    <div class="col-lg-3" *ngIf="bibTrashGroups.length == 0">
+    <ng-container *ngIf="recordType != 'authority'">
+      <div class="col-lg-3" *ngIf="bibTrashGroups.length == 0">
         <span i18n class="font-italic">No Groups Configured</span>
-    </div>
-    <div class="col-lg-3" *ngIf="bibTrashGroups.length">
-      <select multiple [(ngModel)]="selectedTrashGroups" 
-        class="form-control" size="3">
-        <option *ngFor="let grp of bibTrashGroups" 
-          value="{{grp.id()}}">{{grp.label()}}</option>
-      </select>
-    </div>
+      </div>
+      <div class="col-lg-3" *ngIf="bibTrashGroups.length">
+        <select multiple [(ngModel)]="selectedTrashGroups" 
+          id="marc-remove-groups" class="form-control" size="3">
+          <option *ngFor="let grp of bibTrashGroups" 
+            value="{{grp.id()}}">{{grp.label()}}</option>
+        </select>
+      </div>
+    </ng-container>
   </div>
   <div class="row" *ngIf="!importSelection()">
     <div class="col-lg-3">
-      <label i18n>File to Upload:</label>
+      <label for="upload-file" i18n>File to Upload:</label>
     </div>
     <div class="col-lg-3">
       <input #fileSelector (change)="fileSelected($event)" 
-        required class="form-control" type="file"/>
+        id="upload-file" required class="form-control" type="file"/>
     </div>
   </div>
   <div class="row" *ngIf="importSelection()">

commit e6ce65d3b8b0188b4c30bc31abce9e2cbba40336
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Oct 29 12:31:26 2018 -0400

    LP#1800481 Vandelay import form templates
    
    Support saving MARC Import form values as named templates.  Values are
    stored as (by defualt) workstation settings.  A template may be selected
    as the default and templates may be deleted.
    
    Includes release notes update angular vandelay.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

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
index e8adff329a..323623c3a0 100644
--- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
@@ -132,6 +132,12 @@ export class ComboboxComponent implements OnInit {
         this.applySelection();
     }
 
+    // Manually set the selected value by ID.
+    // This does NOT fire the onChange handler.
+    applyEntryId(entryId: any) {
+        this.selected = this.entrylist.filter(e => e.id === entryId)[0];
+    }
+
     onBlur() {
         // When the selected value is a string it means we have either
         // no value (user cleared the input) or a free-text value.
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
index c85233254c..58b3bb015d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
@@ -12,12 +12,39 @@
 <div class="common-form striped-odd form-validated ml-3 mr-3">
   <div class="row">
     <div class="col-lg-3">
+      <label i18n>Apply/Create Form Template</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox #formTemplateSelector
+        (onChange)="templateSelectorChange($event)"
+        [allowFreeText]="true"
+        [startId]="selectedTemplate"
+        [startIdFiresOnChange]="true"
+        [entries]="formatTemplateEntries()"
+        placeholder="Apply or Create Form Template..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-6">
+      <button class="btn btn-success"
+        [disabled]="!selectedTemplate"
+        (click)="saveTemplate()" i18n>Save As New Template</button>
+      <button class="btn btn-outline-primary ml-3"
+        [disabled]="!selectedTemplate"
+        (click)="markTemplateDefault()" i18n>Mark Template as Default</button>
+      <button class="btn btn-danger ml-3"
+        [disabled]="!selectedTemplate"
+        (click)="deleteTemplate()" i18n>Delete Template</button>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="col-lg-3">
       <label i18n>Record Type</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox (onChange)="selectEntry($event, 'recordType')"
-        [disabled]="importSelection()"
-        [required]="true"
+      <eg-combobox #recordTypeSelector
+        (onChange)="selectEntry($event, 'recordType')"
+        [disabled]="importSelection()" [required]="true"
         [startId]="recordType" placeholder="Record Type..." i18n-placeholder>
         <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records" 
           i18n-entryLabel></eg-combobox-entry>
@@ -31,7 +58,8 @@
       <label i18n>Select a Record Source</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('bibSources')" 
+      <eg-combobox #bibSourceSelector
+        [entries]="formatEntries('bibSources')" 
         (onChange)="selectEntry($event, 'bibSources')"
         [startId]="selectedBibSource"
         placeholder="Record Source..." i18n-placeholder>
@@ -68,7 +96,8 @@
       <label i18n>Record Match Set</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('matchSets')" 
+      <eg-combobox #matchSetSelector
+        [entries]="formatEntries('matchSets')" 
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         [startId]="selectedMatchSet || defaultMatchSet"
         (onChange)="selectEntry($event, 'matchSets')"
@@ -85,7 +114,8 @@
       <label i18n>Holdings Import Profile</label>
     </div>
     <div class="col-lg-3"> <!-- TODO disable for authority -->
-      <eg-combobox [entries]="formatEntries('importItemDefs')"
+      <eg-combobox #holdingsProfileSelector
+        [entries]="formatEntries('importItemDefs')"
         [startId]="selectedHoldingsProfile"
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         (onChange)="selectEntry($event, 'importItemDefs')"
@@ -103,7 +133,8 @@
       <label i18n>Merge Profile</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+      <eg-combobox #mergeProfileSelector
+        [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'mergeProfiles')"
         placeholder="Merge Profile..." i18n-placeholder>
       </eg-combobox>
@@ -118,7 +149,8 @@
     <div class="col-lg-3">
       <label i18n>Insufficient Quality Fall-Through Profile</label></div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+      <eg-combobox #fallThruMergeProfileSelector
+        [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'FallThruMergeProfile')"
         placeholder="Fall-Through Merge Profile..." i18n-placeholder>
       </eg-combobox>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
index 3b36f6a341..74341b929a 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -6,13 +6,33 @@ import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {ToastService} from '@eg/share/toast/toast.service';
-import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ComboboxComponent, 
+    ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {VandelayService, VandelayImportSelection,
   VANDELAY_UPLOAD_PATH} from './vandelay.service';
 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
 import {Subject} from 'rxjs/Subject';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
+
+const TEMPLATE_ATTRS = [
+    'recordType',
+    'selectedBibSource',
+    'selectedMatchSet',
+    'mergeOnExact',
+    'importNonMatch',
+    'mergeOnBestMatch',
+    'mergeOnSingleMatch',
+    'autoOverlayAcqCopies',
+    'selectedHoldingsProfile',
+    'selectedMergeProfile',
+    'selectedFallThruMergeProfile',
+    'selectedTrashGroups',
+    'minQualityRatio'
+];
 
 interface ImportOptions {
     session_key: string;
@@ -25,6 +45,7 @@ interface ImportOptions {
     merge_profile?: any;
     fall_through_merge_profile?: any;
     strip_field_groups?: number[];
+    match_quality_ratio: number,
     exit_early: boolean;
 }
 
@@ -77,6 +98,10 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     // Optional enqueue/import tracker session name.
     sessionName: string;
 
+    selectedTemplate: string;
+    formTemplates: {[name: string]: any};
+    newTemplateName: string;
+
     @ViewChild('fileSelector') private fileSelector;
     @ViewChild('uploadProgress') 
         private uploadProgress: ProgressInlineComponent;
@@ -85,6 +110,22 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     @ViewChild('importProgress') 
         private importProgress: ProgressInlineComponent;
 
+    // Need these refs so values can be applied via external stimuli
+    @ViewChild('formTemplateSelector') 
+        private formTemplateSelector: ComboboxComponent;
+    @ViewChild('recordTypeSelector')
+        private recordTypeSelector: ComboboxComponent;
+    @ViewChild('bibSourceSelector')
+        private bibSourceSelector: ComboboxComponent;
+    @ViewChild('matchSetSelector')
+        private matchSetSelector: ComboboxComponent;
+    @ViewChild('holdingsProfileSelector')
+        private holdingsProfileSelector: ComboboxComponent;
+    @ViewChild('mergeProfileSelector')
+        private mergeProfileSelector: ComboboxComponent;
+    @ViewChild('fallThruMergeProfileSelector')
+        private fallThruMergeProfileSelector: ComboboxComponent;
+
     constructor(
         private http: HttpClient,
         private toast: ToastService,
@@ -92,6 +133,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         private net: NetService,
         private auth: AuthService,
         private org: OrgService,
+        private store: ServerStoreService,
         private vandelay: VandelayService
     ) {
         this.applyDefaults();
@@ -102,6 +144,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         this.selectedBibSource = 1; // default to system local
         this.recordType = 'bib';
         this.bibTrashGroups = [];
+        this.formTemplates = {};
 
         if (this.vandelay.importSelection) {
 
@@ -161,12 +204,36 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             this.vandelay.getBibTrashGroups().then(
                 groups => this.bibTrashGroups = groups),
             this.org.settings(['vandelay.default_match_set']).then(
-                s => this.defaultMatchSet = s['vandelay.default_match_set'])
+                s => this.defaultMatchSet = s['vandelay.default_match_set']),
+            this.loadTemplates()
         ];
 
         return Promise.all(promises);
     }
 
+    loadTemplates() {
+        this.store.getItem(TEMPLATE_SETTING_NAME).then(
+            templates => {
+                this.formTemplates = templates || {};
+
+                Object.keys(this.formTemplates).forEach(name => {
+                    if (this.formTemplates[name].default) {
+                        this.selectedTemplate = name;
+                    }
+                });
+            }
+        );
+    }
+
+    formatTemplateEntries(): ComboboxEntry[] {
+        const entries = [];
+
+        Object.keys(this.formTemplates || {}).forEach(
+            name => entries.push({id: name, label: name}));
+
+        return entries;
+    }
+
     // Format typeahead data sets
     formatEntries(etype: string): ComboboxEntry[] {
         const rtype = this.recordType;
@@ -469,6 +536,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             merge_profile: this.selectedMergeProfile,
             fall_through_merge_profile: this.selectedFallThruMergeProfile,
             strip_field_groups: this.selectedTrashGroups,
+            match_quality_ratio: this.minQualityRatio,
             exit_early: true
         };
 
@@ -487,5 +555,67 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     openQueue() {
         console.log('opening queue ' + this.activeQueueId);
     }
+
+    saveTemplate() {
+
+        const template = {};
+        TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
+
+        console.debug("Saving import profile", template);
+
+        this.formTemplates[this.selectedTemplate] = template;
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
+
+    markTemplateDefault() {
+        
+        Object.keys(this.formTemplates).forEach(
+            name => delete this.formTemplates.default
+        );
+
+        this.formTemplates[this.selectedTemplate].default = true;
+
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
+
+    templateSelectorChange(entry: ComboboxEntry) {
+
+        if (!entry) {
+            this.selectedTemplate = '';
+            return;
+        }
+
+        this.selectedTemplate = entry.label; // label == name
+
+        if (entry.freetext) {
+            // User is entering a new template name.
+            // Nothing to apply.
+            return;
+        }
+
+        // User selected an existing template, apply it to the form.
+
+        const template = this.formTemplates[entry.id];
+
+        // Copy the template values into "this"
+        TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
+
+        // Some values must be manually passed to the combobox'es
+
+        this.recordTypeSelector.applyEntryId(this.recordType);
+        this.bibSourceSelector.applyEntryId(this.selectedBibSource);
+        this.matchSetSelector.applyEntryId(this.selectedMatchSet);
+        this.holdingsProfileSelector
+            .applyEntryId(this.selectedHoldingsProfile);
+        this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
+        this.fallThruMergeProfileSelector
+            .applyEntryId(this.selectedFallThruMergeProfile);
+    }
+
+    deleteTemplate() {
+        delete this.formTemplates[this.selectedTemplate];
+        this.formTemplateSelector.selected = null;
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
 }
 
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index e8ef9ed3f6..c10e34fe62 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -19520,6 +19520,16 @@ VALUES (
     'bool'
 );
 
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.cat.vandelay.import.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'eg.cat.vandelay.import.templates',
+        'Vandelay Import Form Templates',
+        'cwst', 'label'
+    )
+);
+
 
 INSERT into config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql
new file mode 100644
index 0000000000..bd104ef32d
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql
@@ -0,0 +1,15 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('YYYY', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.cat.vandelay.import.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'eg.cat.vandelay.import.templates',
+        'Vandelay Import Form Templates',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;
diff --git a/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc b/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc
index 6b5173b044..2baa53b3c5 100644
--- a/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc
@@ -6,6 +6,13 @@ Angular(6) instead of Dojo.  The functionality is consistent with the
 previous version of the interface, with minor UI adjustments to match
 the Angular style, plus one new interface called 'Recent Imports'
 
+Import Templates
+++++++++++++++++
+
+Users may now saves sets of import attributes from the MARC import form as 
+named templates.  Users may select a default template, applied on page load 
+by default, and users may delete existing templates.
+
 Recent Imports Tab
 ++++++++++++++++++
 

commit bcc72fa70921a842d5c876c72278e8ab355e28a2
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Oct 11 15:47:58 2018 -0400

    LP#1779158 Ang6 Vandelay Release Notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc b/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc
new file mode 100644
index 0000000000..6b5173b044
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc
@@ -0,0 +1,21 @@
+MARC Import/Export Interface Update (Angular Port)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This MARC Import/Export (AKA Vandelay) interface is now built on
+Angular(6) instead of Dojo.  The functionality is consistent with the
+previous version of the interface, with minor UI adjustments to match
+the Angular style, plus one new interface called 'Recent Imports'
+
+Recent Imports Tab
+++++++++++++++++++
+
+This is a new interface which allows users to see active and recent
+Vandelay sesssions originating from the same workstation or logged in
+user account.  Active sessions include real-time progress information so
+the user may track the progress without refreshing the page.
+
+This interface makes it possible to exit the main import tab or the
+Vandelay interface altogether and return at a later time to check on
+import progress.  It also allows users to kick-off multiple imports at
+the same time and follow the status of each in one interace.
+

commit 8777877cd9ccdc45fbc8286e4fcc272931d3b1a9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jun 28 18:23:39 2018 -0400

    LP#1779158 Ang6 Vandelay UI Port
    
    Port of the MARC Import/Export UI from a Dojo-driven interface to a
    Angular(6) interface.
    
    Includes an additional UI called "Recent Imports" which displays
    Vandelay session tracker information for both active sessions and those
    within the selected time frame.  Active sessions are updated regularly
    to display progress to the user.
    
    Includes grid persist key workstation settings.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
index 6701941046..37bb188c72 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -396,7 +396,7 @@ export class GridRowSelector {
 
 export interface GridRowFlairEntry {
     icon: string;   // name of material icon
-    title: string;  // tooltip string
+    title?: string;  // tooltip string
 }
 
 export class GridColumnPersistConf {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts
new file mode 100644
index 0000000000..a923b46822
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'vandelay',
+    loadChildren: '@eg/staff/cat/vandelay/vandelay.module#VandelayModule'
+  }
+];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CatRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.html
new file mode 100644
index 0000000000..78a86ed34c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.html
@@ -0,0 +1,17 @@
+
+<ngb-tabset #tabs [activeId]="attrType" (tabChange)="onTabChange($event)">
+	<ngb-tab title="Bibliographic Attributes" i18n-title id="bib">
+		<ng-template ngbTabContent>
+      <div class="mt-3">
+        <eg-admin-page idlClass="vqbrad"></eg-admin-page>
+      </div>
+		</ng-template>
+	</ngb-tab>
+	<ngb-tab title="Authority Attributes" i18n-title id="authority">
+		<ng-template ngbTabContent>
+      <div class="mt-3">
+        <eg-admin-page idlClass="vqarad"></eg-admin-page>
+      </div>
+		</ng-template>
+	</ngb-tab>
+</ngb-tabset>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
new file mode 100644
index 0000000000..6cb13afd96
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
@@ -0,0 +1,35 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  templateUrl: 'display-attrs.component.html'
+})
+export class DisplayAttrsComponent {
+
+    attrType: string;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute) {
+
+        this.route.paramMap.subscribe((params: ParamMap) => {                  
+            this.attrType = params.get('atype');
+        });
+    }
+
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    onTabChange(evt: NgbTabChangeEvent) {
+        this.attrType = evt.nextId;
+
+        // prevent tab changing until after route navigation
+        evt.preventDefault();
+
+        const url = 
+          `/staff/cat/vandelay/display_attrs/${this.attrType}`;
+
+        this.router.navigate([url]);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
new file mode 100644
index 0000000000..020e09748b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
@@ -0,0 +1,119 @@
+<h2 i18n>Export Records</h2>
+
+<div class="common-form striped-even form-validated">
+  <div class="row">
+    <div class="col-lg-6">
+      <div class="row"><label>Select a Record Source</label></div>
+      <ngb-accordion [closeOthers]="true" activeIds="csv" 
+        (panelChange)="sourceChange($event)">
+        <ngb-panel id="csv" title="CSV File">
+          <ng-template ngbPanelContent>
+            <div class="row">
+              <div class="col-lg-6">
+                <label i18n>Use Field Number</label>
+              </div>
+              <div class="col-lg-6">
+                <input id='csv-input' type="number" class="form-control" 
+                  [(ngModel)]="fieldNumber"
+                  i18n-placeholder placeholder="Starts at 0..."/>
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-6">
+                <label i18n>From CSV file</label>
+              </div>
+              <div class="col-lg-6">
+                <input #fileSelector (change)="fileSelected($event)" 
+                  class="form-control" type="file"/>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-panel>
+        <ngb-panel id="record-id" title="Record ID">
+          <ng-template ngbPanelContent>
+            <div class="row">
+              <div class="col-lg-6">
+                <label i18n>Record ID</label>
+              </div>
+              <div class="col-lg-6">
+                <input id='record-id-input' type="number" 
+                  class="form-control" [(ngModel)]="recordId"/>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-panel>
+        <ngb-panel id="bucket-id" title="Bucket">
+          <ng-template ngbPanelContent>
+            <div class="row">
+              <div class="col-lg-6">
+                <label i18n>Bucket ID</label>
+              </div>
+              <div class="col-lg-6">
+                <input id='bucket-id-input' type="number" 
+                  class="form-control" [(ngModel)]="bucketId"/>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-panel>
+      </ngb-accordion>
+    </div><!-- col -->
+    <div class="col-lg-6">
+      <div class="row">
+        <div class="col-lg-6">
+          <label i18n>Record Type</label>
+        </div>
+        <div class="col-lg-6">
+          <select class="form-control" [(ngModel)]="recordType">
+            <option i18n value="biblio">Bibliographic Records</option>
+            <option i18n value="authority">Authority Records</option>
+          </select>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-lg-6">
+          <label i18n>Record Format</label>
+        </div>
+        <div class="col-lg-6">
+          <select class="form-control" [(ngModel)]="recordFormat">
+            <option i18n value="USMARC">MARC21</option>
+            <option i18n value="UNIMARC">UNIMARC</option>
+            <option i18n value="XML">MARC XML</option>
+            <option i18n value="BRE">Evergreen Record Entry</option>
+          </select>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-lg-6">
+          <label i18n>Record Encoding</label>
+        </div>
+        <div class="col-lg-6">
+          <select class="form-control" [(ngModel)]="recordEncoding">
+            <option i18n value="UTF-8">UTF-8</option>
+            <option i18n value="MARC8">MARC8</option>
+          </select>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-lg-6">
+          <label i18n>Include holdings in Bibliographic Records</label>
+        </div>
+        <div class="col-lg-6">
+          <input class="form-check-input" type="checkbox" [(ngModel)]="includeHoldings">
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-lg-10 offset-lg-1">
+          <button class="btn btn-success btn-lg btn-block font-weight-bold"
+            [disabled]="isExporting || !hasNeededData()" 
+            (click)="exportRecords()" i18n>Export</button>
+        </div>
+      </div>
+      <div class="row" [hidden]="!isExporting">
+        <div class="col-lg-10 offset-lg-1">
+          <eg-progress-inline #exportProgress></eg-progress-inline>
+        </div>
+      </div>
+    </div><!-- left col -->
+  </div><!-- row -->
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
new file mode 100644
index 0000000000..253cfcb454
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
@@ -0,0 +1,138 @@
+import {Component, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
+import {NgbPanelChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
+import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
+import {saveAs} from 'file-saver/FileSaver';
+import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {VandelayService, VANDELAY_EXPORT_PATH} from './vandelay.service';
+
+
+ at Component({
+  templateUrl: 'export.component.html'
+})
+export class ExportComponent implements AfterViewInit {
+
+    recordSource: string;
+    fieldNumber: number;
+    selectedFile: File;
+    recordId: number;
+    bucketId: number;
+    recordType: string;
+    recordFormat: string;
+    recordEncoding: string;
+    includeHoldings: boolean;
+    isExporting: boolean;
+
+    @ViewChild('fileSelector') private fileSelector;
+    @ViewChild('exportProgress') 
+        private exportProgress: ProgressInlineComponent;
+
+    constructor(
+        private renderer: Renderer2,
+        private http: HttpClient,
+        private toast: ToastService,
+        private auth: AuthService
+    ) {
+        this.recordType = 'biblio';
+        this.recordFormat = 'USMARC';
+        this.recordEncoding = 'UTF-8';
+        this.includeHoldings = false;
+    }
+
+    ngAfterViewInit() {
+        this.renderer.selectRootElement('#csv-input').focus();
+    }
+
+    sourceChange($event: NgbPanelChangeEvent) {
+        this.recordSource = $event.panelId;
+
+        if ($event.nextState) { // panel opened
+
+            // give the panel a chance to render before focusing input
+            setTimeout(() => {
+                this.renderer.selectRootElement(
+                    `#${this.recordSource}-input`).focus();
+            })
+        }
+    }
+
+    fileSelected($event) {
+       this.selectedFile = $event.target.files[0]; 
+    }
+
+    hasNeededData(): boolean {
+        return Boolean(
+            this.selectedFile || this.recordId || this.bucketId
+        );
+    }
+
+    exportRecords() {
+        this.isExporting = true;
+        this.exportProgress.update({value: 0});
+
+        const formData: FormData = new FormData();
+
+        formData.append('ses', this.auth.token());
+        formData.append('rectype', this.recordType);
+        formData.append('encoding', this.recordEncoding);
+        formData.append('format', this.recordFormat);
+
+        if (this.includeHoldings) {
+            formData.append('holdings', '1');
+        }
+
+        switch (this.recordSource) {
+
+            case 'csv':
+                formData.append('idcolumn', ''+this.fieldNumber);
+                formData.append('idfile', 
+                    this.selectedFile, this.selectedFile.name);
+                break;
+
+            case 'record-id':
+                formData.append('id', ''+this.recordId);
+                break;
+
+            case 'bucket-id':
+                formData.append('containerid', ''+this.bucketId);
+                break;
+        }
+        
+        this.sendExportRequest(formData);
+    }
+
+    sendExportRequest(formData: FormData) {
+
+        const fileName = `export.${this.recordType}.` +
+            `${this.recordEncoding}.${this.recordFormat}`;
+
+        const req = new HttpRequest('POST', VANDELAY_EXPORT_PATH, 
+            formData, {reportProgress: true, responseType: 'text'});
+
+        this.http.request(req).subscribe(
+            evt => {
+                console.log(evt);
+                if (evt.type === HttpEventType.DownloadProgress) {
+                    // File size not reported by server in advance.
+                    this.exportProgress.update({value: evt.loaded});
+
+                } else if (evt instanceof HttpResponse) {
+
+                    saveAs(new Blob([evt.body], 
+                        {type: 'application/octet-stream'}), fileName);
+
+                    this.isExporting = false;
+                }
+            },
+
+            (err: HttpErrorResponse) => {
+                console.error(err);
+                this.toast.danger(err.error);
+                this.isExporting = false;
+            }
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/holdings-profiles.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/holdings-profiles.component.ts
new file mode 100644
index 0000000000..3a342ddc1b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/holdings-profiles.component.ts
@@ -0,0 +1,9 @@
+import {Component} from '@angular/core';
+
+ at Component({
+    template: `<eg-admin-page idlClass="viiad"></eg-admin-page>`
+})
+export class HoldingsProfilesComponent {
+    constructor() {}
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
new file mode 100644
index 0000000000..c85233254c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
@@ -0,0 +1,234 @@
+<div class="row mb-3" *ngIf="importSelection()">
+  <div class="col-lg-2" *ngIf="selectedQueue">
+    <button class="btn btn-info label-with-material-icon"
+      routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{selectedQueue.id}}">
+      <span class="material-icons">arrow_back</span>
+      <span i18n>Return to Queue</span>
+    </button>
+  </div>
+</div>
+
+<h2 i18n>MARC File Upload</h2>
+<div class="common-form striped-odd form-validated ml-3 mr-3">
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Record Type</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox (onChange)="selectEntry($event, 'recordType')"
+        [disabled]="importSelection()"
+        [required]="true"
+        [startId]="recordType" placeholder="Record Type..." i18n-placeholder>
+        <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records" 
+          i18n-entryLabel></eg-combobox-entry>
+        <eg-combobox-entry entryId="authority" entryLabel="Authority Records" 
+          i18n-entryLabel></eg-combobox-entry>
+        <eg-combobox-entry entryId="bib-acq" entryLabel="Acquisitions Records" 
+          i18n-entryLabel></eg-combobox-entry>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-3">
+      <label i18n>Select a Record Source</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('bibSources')" 
+        (onChange)="selectEntry($event, 'bibSources')"
+        [startId]="selectedBibSource"
+        placeholder="Record Source..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Select or Create a Qeueue</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('allQueues')"
+        [startId]="startQueueId"
+        [startIdFiresOnChange]="true"
+        [disabled]="startQueueId"
+        (onChange)="selectedQueue=$event" i18n-placeholder
+        [required]="true"
+        [allowFreeText]="true" placeholder="Select or Create a Queue...">
+      </eg-combobox>
+    </div>
+    <div class="col-lg-3">
+      <label i18n>Limit Matches to Bucket</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('bibBuckets')" 
+        [startId]="selectedBucket"
+        [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+        (onChange)="selectEntry($event, 'bibBuckets')"
+        placeholder="Buckets..." i18n-placeholder></eg-combobox>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Record Match Set</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('matchSets')" 
+        [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+        [startId]="selectedMatchSet || defaultMatchSet"
+        (onChange)="selectEntry($event, 'matchSets')"
+        placeholder="Match Set..." i18n-placeholder></eg-combobox>
+    </div>
+    <div class="col-lg-3"><label i18n>Import Non-Matching Records</label></div>
+    <div class="col-lg-3">
+      <input class="form-check-input" type="checkbox" 
+        [(ngModel)]="importNonMatching">
+    </div>
+ </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Holdings Import Profile</label>
+    </div>
+    <div class="col-lg-3"> <!-- TODO disable for authority -->
+      <eg-combobox [entries]="formatEntries('importItemDefs')"
+        [startId]="selectedHoldingsProfile"
+        [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+        (onChange)="selectEntry($event, 'importItemDefs')"
+        placeholder="Holdings Import Profile..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-3"><label i18n>Merge On Exact Match (901c)</label></div>
+    <div class="col-lg-3">
+      <input class="form-check-input" type="checkbox" 
+        [(ngModel)]="mergeOnExact">
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Merge Profile</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+        (onChange)="selectEntry($event, 'mergeProfiles')"
+        placeholder="Merge Profile..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-3"><label i18n>Merge On Single Match</label></div>
+    <div class="col-lg-3">
+      <input class="form-check-input" type="checkbox" 
+        [(ngModel)]="mergeOnSingleMatch">
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Insufficient Quality Fall-Through Profile</label></div>
+    <div class="col-lg-3">
+      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+        (onChange)="selectEntry($event, 'FallThruMergeProfile')"
+        placeholder="Fall-Through Merge Profile..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-3"><label i18n>Merge On Best Match</label></div>
+    <div class="col-lg-3">
+      <input class="form-check-input" type="checkbox" 
+        [(ngModel)]="mergeOnBestMatch">
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Best/Single Match Minimum Quality Ratio</label></div>
+    <div class="col-lg-3">
+      <input type="number" step="0.1" 
+        class="form-control" [(ngModel)]="minQualityRatio">
+    </div>
+    <div class="col-lg-3">
+      <label i18n>Auto-overlay In-process Acquisitions Copies</label></div>
+    <div class="col-lg-3">
+      <input class="form-check-input" type="checkbox" 
+        [(ngModel)]="autoOverlayAcqCopies">
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-3">
+      <label i18n>Optional Session Name:</label>
+    </div>
+    <div class="col-lg-3">
+      <input [(ngModel)]="sessionName" class="form-control" type="text"
+        i18n-placeholder placeholder="Session Name..."/>
+    </div>
+    <div class="col-lg-3">
+      <label i18n>Remove MARC Field Groups</label>
+    </div>
+    <div class="col-lg-3" *ngIf="bibTrashGroups.length == 0">
+        <span i18n class="font-italic">No Groups Configured</span>
+    </div>
+    <div class="col-lg-3" *ngIf="bibTrashGroups.length">
+      <select multiple [(ngModel)]="selectedTrashGroups" 
+        class="form-control" size="3">
+        <option *ngFor="let grp of bibTrashGroups" 
+          value="{{grp.id()}}">{{grp.label()}}</option>
+      </select>
+    </div>
+  </div>
+  <div class="row" *ngIf="!importSelection()">
+    <div class="col-lg-3">
+      <label i18n>File to Upload:</label>
+    </div>
+    <div class="col-lg-3">
+      <input #fileSelector (change)="fileSelected($event)" 
+        required class="form-control" type="file"/>
+    </div>
+  </div>
+  <div class="row" *ngIf="importSelection()">
+    <div class="col-lg-3">
+      <label>Import Selected</label>
+    </div>
+    <div class="col-lg-3">
+      <span *ngIf="!importSelection().importQueue" i18n>
+        Importing {{importSelection().recordIds.length}} Record(s)</span>
+      <span *ngIf="importSelection().importQueue" i18n>
+        Importing Queue {{importSelection().queue.name()}}</span>
+    </div>
+    <div class="col-lg-3">
+      <button class="btn btn-outline-info ml-2" (click)="clearSelection()" i18n>
+        Clear Selection
+      </button>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-6 offset-lg-3">
+      <button class="btn btn-success btn-lg btn-block font-weight-bold"
+        [disabled]="isUploading || !hasNeededData()" 
+        (click)="upload()" i18n>Upload</button>
+    </div>
+  </div>
+  <!-- hide instead of *ngIf so ViewChild can find the progress bars -->
+  <div class="row" [hidden]="!showProgress || importSelection()">
+    <div class="col-lg-3">
+      <label i18n>Upload Progress</label>
+    </div>
+    <div class="col-lg-6">
+      <eg-progress-inline #uploadProgress></eg-progress-inline>
+    </div>
+  </div>
+  <div class="row" [hidden]="!showProgress || importSelection()">
+    <div class="col-lg-3">
+      <label i18n>Enqueue Progress</label>
+    </div>
+    <div class="col-lg-6">
+      <eg-progress-inline #enqueueProgress></eg-progress-inline>
+    </div>
+  </div>
+  <div class="row" [hidden]="!showProgress">
+    <div class="col-lg-3">
+      <label i18n>Import Progress</label>
+    </div>
+    <div class="col-lg-6">
+      <eg-progress-inline #importProgress></eg-progress-inline>
+    </div>
+  </div>
+  <div class="row" [hidden]="!uploadComplete">
+    <div class="col-lg-6 offset-lg-3">
+      <button class="btn btn-info btn-lg btn-block font-weight-bold"
+        routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{activeQueueId}}"
+        i18n>Go To Queue</button>
+    </div>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
new file mode 100644
index 0000000000..3b36f6a341
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
@@ -0,0 +1,491 @@
+import {Component, OnInit, AfterViewInit, Input, ViewChild, OnDestroy} from '@angular/core';
+import {tap} from 'rxjs/operators/tap';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {VandelayService, VandelayImportSelection,
+  VANDELAY_UPLOAD_PATH} from './vandelay.service';
+import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
+import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {Subject} from 'rxjs/Subject';
+
+interface ImportOptions {
+    session_key: string;
+    overlay_map?: {[qrId: number]: /* breId */ number};
+    import_no_match?: boolean;
+    auto_overlay_exact?: boolean;
+    auto_overlay_best_match?: boolean;
+    auto_overlay_1match?: boolean;
+    opp_acq_copy_overlay?: boolean;
+    merge_profile?: any;
+    fall_through_merge_profile?: any;
+    strip_field_groups?: number[];
+    exit_early: boolean;
+}
+
+ at Component({
+  templateUrl: 'import.component.html'
+})
+export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
+
+    recordType: string;
+    selectedQueue: ComboboxEntry; // freetext enabled
+
+    // used for applying a default queue ID value when we have
+    // a load-time queue before the queue combobox entries exist.
+    startQueueId: number; 
+
+    bibTrashGroups: IdlObject[];
+    selectedTrashGroups: number[];
+
+    activeQueueId: number;
+    selectedBucket: number;
+    selectedBibSource: number;
+    selectedMatchSet: number;
+    selectedHoldingsProfile: number;
+    selectedMergeProfile: number;
+    selectedFallThruMergeProfile: number;
+    selectedFile: File;
+
+    defaultMatchSet: string;
+
+    importNonMatching: boolean;
+    mergeOnExact: boolean;
+    mergeOnSingleMatch: boolean;
+    mergeOnBestMatch: boolean;
+    minQualityRatio: number;
+    autoOverlayAcqCopies: boolean;
+
+    // True after the first upload, then remains true.
+    showProgress: boolean;
+
+    // Upload in progress.
+    isUploading: boolean;
+
+    // True only after successful upload
+    uploadComplete: boolean;
+
+    // Upload / processsing session key
+    // Generated by the server
+    sessionKey: string;
+
+    // Optional enqueue/import tracker session name.
+    sessionName: string;
+
+    @ViewChild('fileSelector') private fileSelector;
+    @ViewChild('uploadProgress') 
+        private uploadProgress: ProgressInlineComponent;
+    @ViewChild('enqueueProgress') 
+        private enqueueProgress: ProgressInlineComponent;
+    @ViewChild('importProgress') 
+        private importProgress: ProgressInlineComponent;
+
+    constructor(
+        private http: HttpClient,
+        private toast: ToastService,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private org: OrgService,
+        private vandelay: VandelayService
+    ) {
+        this.applyDefaults();
+    }
+
+    applyDefaults() {
+        this.minQualityRatio = 0;
+        this.selectedBibSource = 1; // default to system local
+        this.recordType = 'bib';
+        this.bibTrashGroups = [];
+
+        if (this.vandelay.importSelection) {
+
+            if (!this.vandelay.importSelection.queue) {
+                // Incomplete import selection, clear it.
+                this.vandelay.importSelection = null;
+                return;
+            }
+
+            const queue = this.vandelay.importSelection.queue;
+            this.recordType = queue.queue_type();
+            this.selectedMatchSet = queue.match_set();
+
+            // This will be propagated to selectedQueue as a combobox
+            // entry via the combobox
+            this.startQueueId = queue.id();
+
+            if (this.recordType === 'bib') {
+                this.selectedBucket = queue.match_bucket();
+                this.selectedHoldingsProfile = queue.item_attr_def();
+            }
+        }
+    }
+
+    ngOnInit() {}
+
+    ngAfterViewInit() {
+        this.loadStartupData();
+    }
+
+    ngOnDestroy() {
+        // If we successfully completed the most recent 
+        // upload/import assume the importSelection can be cleared.
+        if (this.uploadComplete) {
+            this.clearSelection();
+        }
+    }
+
+    importSelection(): VandelayImportSelection {
+        return this.vandelay.importSelection;
+    }
+
+    loadStartupData(): Promise<any> {
+        // Note displaying and manipulating a progress dialog inside
+        // the AfterViewInit cycle leads to errors because the child
+        // component is modifed after dirty checking.
+
+        const promises = [
+            this.vandelay.getMergeProfiles(),
+            this.vandelay.getAllQueues('bib'),
+            this.vandelay.getAllQueues('authority'),
+            this.vandelay.getMatchSets('bib'),
+            this.vandelay.getMatchSets('authority'),
+            this.vandelay.getBibBuckets(),
+            this.vandelay.getBibSources(),
+            this.vandelay.getItemImportDefs(),
+            this.vandelay.getBibTrashGroups().then(
+                groups => this.bibTrashGroups = groups),
+            this.org.settings(['vandelay.default_match_set']).then(
+                s => this.defaultMatchSet = s['vandelay.default_match_set'])
+        ];
+
+        return Promise.all(promises);
+    }
+
+    // Format typeahead data sets
+    formatEntries(etype: string): ComboboxEntry[] {
+        const rtype = this.recordType;
+        let list;
+
+        switch (etype) {
+            case 'bibSources':
+                return (this.vandelay.bibSources || []).map(
+                    s => { return {id: s.id(), label: s.source()}; });
+
+            case 'bibBuckets':
+                list = this.vandelay.bibBuckets;
+                break;
+
+            case 'allQueues':
+                list = this.vandelay.allQueues[rtype];
+                break;
+
+            case 'matchSets':
+                list = this.vandelay.matchSets[rtype];
+                break;
+
+            case 'importItemDefs':
+                list = this.vandelay.importItemAttrDefs;
+                break;
+
+            case 'mergeProfiles':
+                list = this.vandelay.mergeProfiles;
+                break;
+        }
+
+        return (list || []).map(item => {
+            return {id: item.id(), label: item.name()};
+        });
+    }
+
+    selectEntry($event: ComboboxEntry, etype: string) {
+        const id = $event ? $event.id : null;
+
+        switch (etype) {
+            case 'recordType':
+                this.recordType = id;
+              
+            case 'bibSources':
+                this.selectedBibSource = id;
+                break;
+
+            case 'bibBuckets':
+                this.selectedBucket = id;
+                break;
+
+            case 'matchSets':
+                this.selectedMatchSet = id;
+                break;
+
+            case 'importItemDefs':
+                this.selectedHoldingsProfile = id;
+                break;
+
+            case 'mergeProfiles':
+                this.selectedMergeProfile = id;
+                break;
+
+            case 'FallThruMergeProfile':
+                this.selectedFallThruMergeProfile = id;
+                break;
+        }
+    }
+
+    fileSelected($event) {
+       this.selectedFile = $event.target.files[0]; 
+    }
+
+    // Required form data varies depending on context.
+    hasNeededData(): boolean {
+        if (this.vandelay.importSelection) {
+            return this.importActionSelected();
+        } else {
+            return this.selectedQueue 
+                && Boolean(this.recordType) && Boolean(this.selectedFile)
+        }
+    }
+
+    importActionSelected(): boolean {
+        return this.importNonMatching
+            || this.mergeOnExact
+            || this.mergeOnSingleMatch
+            || this.mergeOnBestMatch;
+    }
+
+    // 1. create queue if necessary
+    // 2. upload MARC file
+    // 3. Enqueue MARC records
+    // 4. Import records
+    upload() {
+        this.sessionKey = null;
+        this.showProgress = true;
+        this.isUploading = true;
+        this.uploadComplete = false;
+        this.resetProgressBars();
+
+        this.resolveQueue()
+        .then(
+            queueId => {
+                this.activeQueueId = queueId;
+                return this.uploadFile();
+            },
+            err => Promise.reject('queue create failed')
+        ).then(
+            ok => this.processSpool(),
+            err => Promise.reject('process spool failed')
+        ).then(
+            ok => this.importRecords(),
+            err => Promise.reject('import records failed')
+        ).then(
+            ok => {
+                this.isUploading = false;
+                this.uploadComplete = true;
+            },
+            err => {
+                console.log('file upload failed: ', err);
+                this.isUploading = false;
+                this.resetProgressBars();
+
+            }
+        );
+    }
+
+    resetProgressBars() {
+        this.uploadProgress.update({value: 0, max: 1});
+        this.enqueueProgress.update({value: 0, max: 1});
+        this.importProgress.update({value: 0, max: 1});
+    }
+
+    // Extract selected queue ID or create a new queue when requested.
+    resolveQueue(): Promise<number> {
+
+        if (this.selectedQueue.freetext) {
+            // Free text queue selector means create a new entry.
+            // TODO: first check for name dupes
+
+            return this.vandelay.createQueue(
+                this.selectedQueue.label,
+                this.recordType,
+                this.selectedHoldingsProfile,
+                this.selectedMatchSet,
+                this.selectedBucket
+            );
+
+        } else {
+            return Promise.resolve(this.selectedQueue.id);
+        }
+    }
+
+    uploadFile(): Promise<any> {
+
+        if (this.vandelay.importSelection) {
+            // Nothing to upload when processing pre-queued records.
+            return Promise.resolve();
+        }
+        
+        const formData: FormData = new FormData();
+
+        formData.append('ses', this.auth.token());
+        formData.append('marc_upload', 
+            this.selectedFile, this.selectedFile.name);
+
+        if (this.selectedBibSource) {
+            formData.append('bib_source', ''+this.selectedBibSource);
+        }
+
+        const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData, 
+            {reportProgress: true, responseType: 'text'});
+
+        return this.http.request(req).pipe(tap(
+            evt => {
+                if (evt.type === HttpEventType.UploadProgress) {
+                    this.uploadProgress.update(
+                        {value: evt.loaded, max: evt.total});
+
+                } else if (evt instanceof HttpResponse) {
+                    this.sessionKey = evt.body as string;
+                    console.log(
+                        'Vandelay file uploaded OK with key '+this.sessionKey);
+                }
+            },
+
+            (err: HttpErrorResponse) => {
+                console.error(err);
+                this.toast.danger(err.error);
+            }
+        )).toPromise();
+    }
+
+    processSpool():  Promise<any> {
+
+        if (this.vandelay.importSelection) {
+            // Nothing to enqueue when processing pre-queued records
+            return Promise.resolve();
+        }
+
+        const method = `open-ils.vandelay.${this.recordType}.process_spool`;
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.vandelay', method, 
+                this.auth.token(), this.sessionKey, this.activeQueueId,
+                null, null, this.selectedBibSource, 
+                (this.sessionName || null), true
+            ).subscribe(
+                tracker => {
+                    const e = this.evt.parse(tracker);
+                    if (e) { console.error(e); return reject(); }
+
+                    // Spooling is in progress, track the results.
+                    this.vandelay.pollSessionTracker(tracker.id())
+                    .subscribe(
+                        trkr => {
+                            this.enqueueProgress.update({
+                                // enqueue API only tracks actions performed
+                                max: null, 
+                                value: trkr.actions_performed()
+                            });
+                        },
+                        err => { console.log(err); reject(); },
+                        () => {
+                            this.enqueueProgress.update({max: 1, value: 1});
+                            resolve();
+                        }
+                    );
+                }
+            );
+        });
+    }
+
+    importRecords(): Promise<any> {
+
+        if (!this.importActionSelected()) {
+            return Promise.resolve();
+        }
+
+        const selection = this.vandelay.importSelection;
+
+        if (selection && !selection.importQueue) {
+            return this.importRecordQueue(selection.recordIds);
+        } else {
+            return this.importRecordQueue();
+        }
+    }
+
+    importRecordQueue(recIds?: number[]): Promise<any> {
+        const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
+
+        let method = `open-ils.vandelay.${rtype}_queue.import`;
+        const options: ImportOptions = this.compileImportOptions();
+
+        let target: number | number[] = this.activeQueueId;
+        if (recIds && recIds.length) {
+            method = `open-ils.vandelay.${rtype}_record.list.import`;
+            target = recIds;
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request('open-ils.vandelay', 
+                method, this.auth.token(), target, options)
+            .subscribe(
+                tracker => {
+                    const e = this.evt.parse(tracker);
+                    if (e) { console.error(e); return reject(); }
+
+                    // Spooling is in progress, track the results.
+                    this.vandelay.pollSessionTracker(tracker.id())
+                    .subscribe(
+                        trkr => {
+                            this.importProgress.update({
+                                max: trkr.total_actions(),
+                                value: trkr.actions_performed()
+                            });
+                        },
+                        err => { console.log(err); reject(); },
+                        () => {
+                            this.importProgress.update({max: 1, value: 1});
+                            resolve();
+                        }
+                    );
+                }
+            );
+        });
+    }
+
+    compileImportOptions(): ImportOptions {
+
+        const options: ImportOptions = {
+            session_key: this.sessionKey,
+            import_no_match: this.importNonMatching,
+            auto_overlay_exact: this.mergeOnExact,
+            auto_overlay_best_match: this.mergeOnBestMatch,
+            auto_overlay_1match: this.mergeOnSingleMatch,
+            opp_acq_copy_overlay: this.autoOverlayAcqCopies,
+            merge_profile: this.selectedMergeProfile,
+            fall_through_merge_profile: this.selectedFallThruMergeProfile,
+            strip_field_groups: this.selectedTrashGroups,
+            exit_early: true
+        };
+
+        if (this.vandelay.importSelection) {
+            options.overlay_map = this.vandelay.importSelection.overlayMap;
+        }
+
+        return options;
+    }
+
+    clearSelection() {
+        this.vandelay.importSelection = null;
+        this.startQueueId = null;
+    }
+
+    openQueue() {
+        console.log('opening queue ' + this.activeQueueId);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.html
new file mode 100644
index 0000000000..fe7d8171c9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.html
@@ -0,0 +1,69 @@
+<ng-template #nodeStrTmpl let-point="point" let-showmatch="showmatch" i18n>
+  <ng-container *ngIf="point">
+    <span *ngIf="point.negate()">NOT </span>
+    <span *ngIf="point.heading()">Normalized Heading</span>
+    <span>{{point.bool_op()}}{{point.svf()}}{{point.tag()}}</span>
+    <span *ngIf="point.subfield()"> ‡{{point.subfield()}}</span>
+    <span *ngIf="showmatch && !point.bool_op()"> | Match score {{point.quality()}}</span>
+  </ng-container>
+</ng-template>
+<eg-string key="staff.cat.vandelay.matchpoint.label" 
+  [template]="nodeStrTmpl"></eg-string>
+
+<div class="row mt-2">
+  <div class="col-lg-7">
+    <div class="row ml-2">
+      <span class="text-white bg-dark p-2" i18n>
+        Your Expression: {{expressionAsString()}}
+      </span>
+    </div>
+    <div class="row ml-2 mt-4">
+      <span class="mr-2" i18n>Add New:</span>
+      <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='biblio'"
+        (click)="newPointType='attr'" i18n>Record Attribute</button>
+      <button class="btn btn-outline-dark mr-2" 
+        (click)="newPointType='marc'" i18n>MARC Tag and Subfield</button>
+      <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='authority'"
+        (click)="newPointType='heading'" i18n>Normalized Authority Heading</button>
+      <button class="btn btn-outline-dark mr-2" 
+        (click)="newPointType='bool'" i18n>Boolean Operator</button>
+    </div>
+    <eg-match-set-new-point #newPoint [pointType]="newPointType">
+    </eg-match-set-new-point>
+    <div class="row mt-2 ml-2" *ngIf="newPointType">
+      <button class="btn btn-success" (click)="addChildNode()" 
+        [disabled]="!selectedIsBool()" i18n>
+        Add To Selected Node
+      </button>
+    </div>
+    <div class="row mt-2 ml-2 font-italic" *ngIf="newPointType">
+      <ol i18n>
+        <li>Define a new match point using the above fields.</li>
+        <li>Select a boolean node in the tree.</li>
+        <li>Click the "Add..." button to add the new matchpoint
+          as a child of the selected node.</li>
+      </ol>
+    </div>
+  </div>
+  <div class="col-lg-5">
+    <ng-container *ngIf="tree">
+      <div class="d-flex">
+        <button class="btn btn-warning mr-1" (click)="deleteNode()" 
+          [disabled]="!hasSelectedNode()" i18n>
+          Remove Selected Node
+        </button>
+        <button class="btn btn-success mr-1" (click)="saveTree()"
+          [disabled]="!changesMade" i18n>
+          Save Changes 
+        </button>
+      </div>
+      <div class="pt-2">
+        <eg-tree
+          [tree]="tree" 
+          (nodeClicked)="nodeClicked($event)">
+        </eg-tree>
+      </div>
+    </ng-container>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
new file mode 100644
index 0000000000..991206853d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
@@ -0,0 +1,219 @@
+import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {Tree, TreeNode} from '@eg/share/tree/tree';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {StringService} from '@eg/share/string/string.service';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+
+ at Component({
+  selector: 'eg-match-set-expression',
+  templateUrl: 'match-set-expression.component.html'
+})
+export class MatchSetExpressionComponent implements OnInit {
+
+    // Match set arrives from parent async.
+    matchSet_: IdlObject;
+    @Input() set matchSet(ms: IdlObject) {
+        this.matchSet_ = ms;
+        if (ms && !this.initDone) {
+            this.matchSetType = ms.mtype();
+            this.initDone = true;
+            this.refreshTree();
+        }
+    }
+
+    tree: Tree;
+    initDone: boolean;
+    matchSetType: string;
+    changesMade: boolean;
+
+    // Current type of new match point
+    newPointType: string;
+    newId: number;
+
+    @ViewChild('newPoint') newPoint: MatchSetNewPointComponent;
+
+    constructor(
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private net: NetService,
+        private auth: AuthService,
+        private org: OrgService,
+        private strings: StringService
+    ) {
+        this.newId = -1;
+    }
+
+    ngOnInit() {}
+
+    refreshTree(): Promise<any> {
+        if (!this.matchSet_) { return Promise.resolve(); }
+
+        return this.pcrud.search('vmsp',
+            {match_set: this.matchSet_.id()}, {}, 
+            {atomic: true, authoritative: true}
+        ).toPromise().then(points => this.ingestMatchPoints(points));
+    }
+
+    ingestMatchPoints(points: IdlObject[]) {
+        const nodes = [];
+        const idmap: any = {};
+
+        // massage data, create tree nodes
+        points.forEach(point => {
+
+            point.negate(point.negate() === 't' ? true : false);
+            point.heading(point.heading() === 't' ? true : false);
+            point.children([]);
+
+            const node = new TreeNode({
+                id: point.id(),
+                expanded: true,
+                callerData: {point: point}
+            });
+            idmap[node.id + ''] = node;
+            this.setNodeLabel(node, point).then(() => nodes.push(node));
+        });
+
+        // apply the tree parent/child relationships
+        points.forEach(point => {
+            const node = idmap[point.id() + ''];
+            if (point.parent()) {
+                idmap[point.parent() + ''].children.push(node);
+            } else {
+                this.tree = new Tree(node);
+            }
+        });
+    }
+
+    setNodeLabel(node: TreeNode, point: IdlObject): Promise<any> {
+        if (node.label) { return Promise.resolve(null); }
+        return Promise.all([
+            this.getPointLabel(point, true).then(txt => node.label = txt),
+            this.getPointLabel(point, false).then(
+                txt => node.callerData.slimLabel = txt)
+        ]);
+    }
+
+    getPointLabel(point: IdlObject, showmatch?: boolean): Promise<string> {
+        return this.strings.interpolate(
+            'staff.cat.vandelay.matchpoint.label', 
+            {point: point, showmatch: showmatch}
+        );
+    }
+
+    nodeClicked(node: TreeNode) {}
+
+    deleteNode() {
+        this.changesMade = true;
+        const node = this.tree.selectedNode()
+        this.tree.removeNode(node);
+    }
+
+    hasSelectedNode(): boolean {
+        return Boolean(this.tree.selectedNode());
+    }
+
+    selectedIsBool(): boolean {
+        if (this.tree) {
+            const node = this.tree.selectedNode();
+            return node && node.callerData.point.bool_op();
+        }
+        return false;
+    }
+
+    addChildNode() {
+        this.changesMade = true;
+
+        const pnode = this.tree.selectedNode();
+        const point = this.idl.create('vmsp');
+        point.id(this.newId--);
+        point.isnew(true);
+        point.parent(pnode.id);
+        point.match_set(this.matchSet_.id());
+        point.children([]);
+
+        const ptype = this.newPoint.values.pointType;
+
+        if (ptype === 'bool') {
+            point.bool_op(this.newPoint.values.boolOp);
+
+        } else {
+
+            if (ptype == 'attr') {
+                point.svf(this.newPoint.values.recordAttr);
+
+            } else if (ptype == 'marc') {
+                point.tag(this.newPoint.values.marcTag);
+                point.subfield(this.newPoint.values.marcSf);
+            } else if (ptype == 'heading') {
+                point.heading(true);
+            }
+
+            point.negate(this.newPoint.values.negate);
+            point.quality(this.newPoint.values.matchScore);
+        }
+
+        const node: TreeNode = new TreeNode({
+            id: point.id(), 
+            callerData: {point: point}
+        });
+
+        // Match points are added to the DB only when the tree is saved.
+        this.setNodeLabel(node, point).then(() => pnode.children.push(node));
+    }
+
+    expressionAsString(): string {
+        if (!this.tree) { return ''; }
+
+        const renderNode = (node: TreeNode): string => {
+            if (!node) { return ''; }
+
+            if (node.children.length) {
+                return '(' + node.children.map(renderNode).join(
+                    ' ' + node.callerData.slimLabel + ' ') + ')'
+            } else if (!node.callerData.point.bool_op()) {
+                return node.callerData.slimLabel;
+            } else {
+                return '()';
+            }
+        }
+
+        return renderNode(this.tree.rootNode);
+    }
+
+    // Server API deletes and recreates the tree on update.
+    // It manages parent/child relationships via the children array.
+    // We only need send the current tree in a form the API recognizes.
+    saveTree(): Promise<any> {
+
+
+        const compileTree = (node?: TreeNode) => {
+
+            if (!node) { node = this.tree.rootNode; }
+
+            const point = node.callerData.point;
+
+            node.children.forEach(child =>
+                point.children().push(compileTree(child)));
+
+            return point;
+        };
+
+        const rootPoint: IdlObject = compileTree();
+
+        return this.net.request(
+            'open-ils.vandelay',
+            'open-ils.vandelay.match_set.update',
+            this.auth.token(), this.matchSet_.id(), rootPoint
+        ).toPromise().then(
+            ok =>this.refreshTree(),
+            err => console.error(err)
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.html
new file mode 100644
index 0000000000..7674be239d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.html
@@ -0,0 +1,37 @@
+
+<div class="d-flex mb-3">
+  <div>
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text">Owner</span>
+      </div>
+      <eg-org-select
+        [initialOrg]="contextOrg"
+        (onChange)="orgOnChange($event)">
+      </eg-org-select>
+    </div>
+  </div>
+</div>
+
+<ng-template #nameTmpl let-row="row">
+  <a routerLink="/staff/cat/vandelay/match_sets/{{row.id()}}/editor">
+    {{row.name()}}
+  </a> 
+</ng-template>
+
+<eg-grid #grid [dataSource]="gridSource"
+  persistKey="cat.vandelay.match_set.list"
+  idlClass="vms" [dataSource]="queueSource">
+  <eg-grid-toolbar-button label="New Match Set" i18n-label [action]="createNew">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    [action]="deleteSelected"></eg-grid-toolbar-action>
+  <eg-grid-column name="name" [cellTemplate]="nameTmpl">
+  </eg-grid-column>
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="vms">
+</eg-fm-record-editor>
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
new file mode 100644
index 0000000000..e20c954be2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
@@ -0,0 +1,78 @@
+import {Component, AfterViewInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';              
+import {Pager} from '@eg/share/util/pager';                                    
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+
+ at Component({
+  templateUrl: 'match-set-list.component.html'
+})
+export class MatchSetListComponent implements AfterViewInit {
+
+    contextOrg: IdlObject;
+    gridSource: GridDataSource;
+    deleteSelected: (rows: IdlObject[]) => void;
+    createNew: () => void;
+    @ViewChild('grid') grid: GridComponent;
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+
+    constructor(
+        private router: Router,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private org: OrgService) {
+
+        this.gridSource = new GridDataSource();
+        this.contextOrg = this.org.get(this.auth.user().ws_ou());
+
+        this.gridSource.getRows = (pager: Pager) => {
+            const orgs = this.org.ancestors(this.contextOrg, true);
+            return this.pcrud.search('vms', {owner: orgs}, {   
+                order_by: {vms: ['name']},
+                limit: pager.limit,
+                offset: pager.offset
+            });
+        }
+
+        this.createNew = () => {
+            this.editDialog.mode = 'create';
+            this.editDialog.open({size: 'lg'}).then(
+                ok => this.grid.reload(),
+                err => {}
+            );
+        };
+
+        this.deleteSelected = (matchSets: IdlObject[]) => {
+            matchSets.forEach(matchSet => matchSet.isdeleted(true));
+            this.pcrud.autoApply(matchSets).subscribe(
+                val => console.debug('deleted: ' + val),
+                err => {},
+                ()  => this.grid.reload()
+            );
+        };
+    }
+
+    ngAfterViewInit() {
+        this.grid.onRowActivate.subscribe(
+            (matchSet: IdlObject) => {
+                this.editDialog.mode = 'update';
+                this.editDialog.recId = matchSet.id();
+                this.editDialog.open({size: 'lg'}).then(
+                    ok => this.grid.reload(),
+                    err => {}
+                );
+            }
+        );
+    }
+
+    orgOnChange(org: IdlObject) {
+        this.contextOrg = org;
+        this.grid.reload();
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.html
new file mode 100644
index 0000000000..4ffa40c819
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.html
@@ -0,0 +1,77 @@
+<div class="row ml-2 mt-4 p-2 border border-secondary" *ngIf="values.pointType">
+  <div class="col-lg-12 common-form striped-odd form-validated">
+    <ng-container *ngIf="values.pointType=='attr'">
+      <div class="row mb-1">
+        <div class="col-lg-3" i18n>Record Attribute:</div>
+        <div class="col-lg-4">
+          <eg-combobox [entries]="bibAttrDefEntries"
+            [required]="true" 
+            (onChange)="values.recordAttr=$event ? $event.id : ''"
+            placeholder="Record Attribute..." i18n-placeholder>                       
+          </eg-combobox>  
+        </div>
+      </div>
+    </ng-container>
+    <ng-container *ngIf="values.pointType=='marc'">
+      <div class="row mb-1">
+        <div class="col-lg-3" i18n>Tag:</div>
+        <div class="col-lg-2">
+          <input required type="text" class="form-control" [(ngModel)]="values.marcTag"/>
+        </div>
+      </div>
+      <div class="row mb-1">
+        <div class="col-lg-3" i18n>Subfield ‡:</div>
+        <div class="col-lg-2">
+          <input required type="text" class="form-control" [(ngModel)]="values.marcSf"/>
+        </div>
+      </div>
+    </ng-container>
+    <ng-container *ngIf="values.pointType=='heading'">
+      <div class="row mb-1">
+        <div class="col-lg-3" i18n>Normalized Heading:</div>
+        <div class="col-lg-2">
+          <input type="checkbox" class="form-check-input" checked disabled/>
+        </div>
+      </div>
+    </ng-container>
+    <ng-container *ngIf="values.pointType!='bool'">
+      <div class="row mb-1">
+        <div class="col-lg-3">Match Score:</div>
+        <div class="col-lg-2">
+          <input required type="number" class="form-control" 
+            [(ngModel)]="values.matchScore" step="0.1"/>
+        </div>
+      </div>
+      <ng-container *ngIf="!isForQuality">
+        <div class="row mb-1">
+          <div class="col-lg-3">Negate:</div>
+          <div class="col-lg-2">
+            <input type="checkbox" 
+              class="form-check-input" [(ngModel)]="values.negate"/>
+          </div>
+        </div>
+      </ng-container>
+    </ng-container>
+    <ng-container *ngIf="values.pointType=='bool'">
+      <div class="row mb-1">
+        <div class="col-lg-3">Operator:</div>
+        <div class="col-lg-2">
+          <select class="form-control" [(ngModel)]="values.boolOp">
+            <option value='AND' i18n>AND</option>
+            <option value='OR' i18n>OR</option>
+          </select>
+        </div>
+      </div>
+    </ng-container>
+    <ng-container *ngIf="isForQuality">
+      <div class="row mb-1">
+        <div class="col-lg-3" i18n>Value:</div>
+        <div class="col-lg-2">
+          <input type="text" class="form-control" required 
+            [(ngModel)]="values.value"/>
+        </div>
+      </div>
+    </ng-container>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
new file mode 100644
index 0000000000..6298981cff
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
@@ -0,0 +1,65 @@
+import {Component, OnInit, ViewChild, Output, Input} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+// Can be used to create match_set_point's and match_set_quality's
+export class MatchSetPointValues {
+    pointType: string;
+    recordAttr: string;
+    matchScore: number;
+    negate: boolean;
+    marcTag: string;
+    marcSf: string;
+    heading: string;
+    boolOp: string;
+    value: string;
+}
+
+ at Component({
+  selector: 'eg-match-set-new-point',
+  templateUrl: 'match-set-new-point.component.html'
+})
+export class MatchSetNewPointComponent implements OnInit {
+
+    public values: MatchSetPointValues;
+
+    bibAttrDefs: IdlObject[];
+    bibAttrDefEntries: ComboboxEntry[];
+
+    // defining a new match_set_quality
+    @Input() isForQuality: boolean;
+
+    // biblio, authority, quality
+    @Input() set pointType(type_: string) { 
+        this.values.pointType = type_;
+        this.values.recordAttr = '';
+        this.values.matchScore = 1;
+        this.values.negate = false;
+        this.values.marcTag = '';
+        this.values.marcSf = '';
+        this.values.boolOp = 'AND';
+        this.values.value = '';
+    }
+
+    constructor(
+        private idl: IdlService,
+        private pcrud: PcrudService
+    ) {
+        this.values = new MatchSetPointValues();
+        this.bibAttrDefs = [];
+        this.bibAttrDefEntries = [];
+    }
+
+    ngOnInit() {
+        this.pcrud.retrieveAll('crad', {order_by: {crad: 'label'}})
+        .subscribe(attr => {
+            this.bibAttrDefs.push(attr);
+            this.bibAttrDefEntries.push({id: attr.name(), label: attr.label()});
+        });
+    }
+
+    setNewPointType(type_: string) {
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.html
new file mode 100644
index 0000000000..5229ddf3ac
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.html
@@ -0,0 +1,27 @@
+<div class="row mt-2">
+  <div class="col-lg-7">
+    <div class="row ml-2 mt-4">
+      <span class="mr-2" i18n>Add New:</span>
+      <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='biblio'"
+        (click)="newPointType='attr'" i18n>Record Attribute</button>
+      <button class="btn btn-outline-dark mr-2" 
+        (click)="newPointType='marc'" i18n>MARC Tag and Subfield</button>
+    </div>
+    <eg-match-set-new-point #newPoint 
+      [pointType]="newPointType" [isForQuality]="true">
+    </eg-match-set-new-point>
+    <div class="row mt-2 ml-2" *ngIf="newPointType">
+      <button class="btn btn-success mr-2" 
+        (click)="addQuality()" i18n>Add</button>
+      <button class="btn btn-warning"
+        (click)="newPointType=null" i18n>Cancel</button>
+    </div>
+  </div>
+</div>
+
+<eg-grid idlClass="vmsq" [dataSource]="dataSource" #grid
+  persistKey="staff.cat.vandelay.match_set.quality">
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    [action]="deleteSelected"></eg-grid-toolbar-action>
+</eg-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
new file mode 100644
index 0000000000..b2409c196c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
@@ -0,0 +1,105 @@
+import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+
+ at Component({
+  selector: 'eg-match-set-quality',
+  templateUrl: 'match-set-quality.component.html'
+})
+export class MatchSetQualityComponent implements OnInit {
+
+    // Match set arrives from parent async.
+    matchSet_: IdlObject;
+    @Input() set matchSet(ms: IdlObject) {
+        this.matchSet_ = ms;
+        if (ms) { 
+            this.matchSetType = ms.mtype(); 
+            if (this.grid) {
+                this.grid.reload();
+            }
+        }
+    }
+
+    newPointType: string;
+    matchSetType: string;
+    dataSource: GridDataSource;
+    @ViewChild('newPoint') newPoint: MatchSetNewPointComponent;
+    @ViewChild('grid') grid: GridComponent;
+    deleteSelected: (rows: IdlObject[]) => void;
+
+    constructor(
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private net: NetService,
+        private auth: AuthService,
+        private org: OrgService
+    ) {
+
+        this.dataSource = new GridDataSource();
+        this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+
+            if (!this.matchSet_) {
+                return Observable.of();
+            }
+
+            const orderBy: any = {};
+            if (sort.length) {
+                orderBy.vmsq = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            const searchOps = {
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            };
+
+            const search = {match_set: this.matchSet_.id()};
+            return this.pcrud.search('vmsq', search, searchOps);
+        }
+
+        this.deleteSelected = (rows: any[]) => {
+            this.pcrud.remove(rows).subscribe(
+                ok  => console.log('deleted ', ok),
+                err => console.error(err),
+                ()  => this.grid.reload()
+            );
+        };
+    }
+
+    ngOnInit() {}
+
+    addQuality() {
+        const quality = this.idl.create('vmsq');  
+        const values = this.newPoint.values;
+
+        quality.match_set(this.matchSet_.id());
+        quality.quality(values.matchScore);
+        quality.value(values.value);
+
+        if (values.recordAttr) {
+            quality.svf(values.recordAttr);
+        } else {
+            quality.tag(values.marcTag);
+            quality.subfield(values.marcSf);
+        }
+
+        this.pcrud.create(quality).subscribe(
+            ok  => console.debug('created ', ok),
+            err => console.error(err),
+            ()  => {
+                this.newPointType = null;
+                this.grid.reload();
+            }
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.html
new file mode 100644
index 0000000000..fd69cf93bd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.html
@@ -0,0 +1,36 @@
+<div class="row pb-2" *ngIf="matchSet">
+  <div class="col-lg-4">
+    <div class="card tight-card">
+      <h5 class="card-header" i18n>Match Set Summary</h5>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-lg-6" i18n>Match Set Name:</div>
+          <div class="col-lg-6">{{matchSet.name()}}</div>
+        </div>
+        <div class="row">
+          <div class="col-lg-6" i18n>Owning Library:</div>
+          <div class="col-lg-6">{{matchSet.owner().shortname()}}</div>
+        </div>
+        <div class="row">
+          <div class="col-lg-6" i18n>Type:</div>
+          <div class="col-lg-6">{{matchSet.mtype()}}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<ngb-tabset [activeId]="matchSetTab" (tabChange)="onTabChange($event)">
+	<ngb-tab title="Match Set Editor" i18n-title id="editor">
+		<ng-template ngbTabContent>
+      <eg-match-set-expression [matchSet]="matchSet">
+      </eg-match-set-expression>
+		</ng-template>
+	</ngb-tab>
+	<ngb-tab title="Match Set Quality Metrics" i18n-title id="quality">
+		<ng-template ngbTabContent>
+      <eg-match-set-quality [matchSet]="matchSet">
+      </eg-match-set-quality>
+		</ng-template>
+	</ngb-tab>
+</ngb-tabset>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
new file mode 100644
index 0000000000..15a19aaa42
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
@@ -0,0 +1,51 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+
+ at Component({
+  templateUrl: 'match-set.component.html'
+})
+export class MatchSetComponent implements OnInit {
+
+    matchSet: IdlObject;
+    matchSetId: number;
+    matchSetTab: string;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private org: OrgService
+    ) {
+        this.route.paramMap.subscribe((params: ParamMap) => {                  
+            this.matchSetId = +params.get('id');
+            this.matchSetTab = params.get('matchSetTab');
+        });
+    }
+
+    ngOnInit() {
+        this.pcrud.retrieve('vms', this.matchSetId)
+            .toPromise().then(ms => {
+                ms.owner(this.org.get(ms.owner()));
+                this.matchSet = ms;
+            });
+    }
+
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    onTabChange(evt: NgbTabChangeEvent) {
+        this.matchSetTab = evt.nextId;
+
+        // prevent tab changing until after route navigation
+        evt.preventDefault();
+
+        const url = 
+          `/staff/cat/vandelay/match_sets/${this.matchSetId}/${this.matchSetTab}`;
+
+        this.router.navigate([url]);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/merge-profiles.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/merge-profiles.component.ts
new file mode 100644
index 0000000000..2059b618cb
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/merge-profiles.component.ts
@@ -0,0 +1,9 @@
+import {Component} from '@angular/core';
+
+ at Component({
+    template: `<eg-admin-page idlClass="vmp"></eg-admin-page>`
+})
+export class MergeProfilesComponent {
+    constructor() {}
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
new file mode 100644
index 0000000000..8bc896f5b6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
@@ -0,0 +1,19 @@
+<div class="row mb-3">
+  <div class="col-lg-2">
+    <button class="btn btn-info label-with-material-icon"
+      routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}">
+      <span class="material-icons">arrow_back</span>
+      <span i18n>Return to Queue</span>
+    </button>
+  </div>
+</div>
+
+<eg-grid #itemsGrid 
+  showFields="record,import_error,imported_as,import_time,owning_lib,call_number,barcode"
+  persistKey="cat.vandelay.queue.items"
+  idlClass="vii" [dataSource]="gridSource">
+  <eg-grid-toolbar-checkbox [onChange]="limitToImportErrors"
+    i18n-label label="Limit to Import Failures"></eg-grid-toolbar-checkbox>
+
+</eg-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
new file mode 100644
index 0000000000..d72a81bfa8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
@@ -0,0 +1,60 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Pager} from '@eg/share/util/pager';                                    
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+ at Component({
+  templateUrl: 'queue-items.component.html'
+})
+export class QueueItemsComponent {
+
+    queueType: string;
+    queueId: number;
+    filterImportErrors: boolean;
+    limitToImportErrors: (checked: boolean) => void;
+
+    gridSource: GridDataSource;
+    @ViewChild('itemsGrid') itemsGrid: GridComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private vandelay: VandelayService) {
+
+        this.route.paramMap.subscribe((params: ParamMap) => {                  
+            this.queueId = +params.get('id');
+            this.queueType = params.get('qtype');
+        });
+
+        this.gridSource = new GridDataSource();
+
+        // queue API does not support sorting
+        this.gridSource.getRows = (pager: Pager) => {
+            return this.net.request(
+                'open-ils.vandelay',
+                'open-ils.vandelay.import_item.queue.retrieve',
+                this.auth.token(), this.queueId, {
+                    with_import_error: this.filterImportErrors,
+                    offset: pager.offset,
+                    limit: pager.limit
+                }
+            );
+        };
+
+        this.limitToImportErrors = (checked: boolean) => {
+            this.filterImportErrors = checked;
+            this.itemsGrid.reload();
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
new file mode 100644
index 0000000000..6aface5418
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
@@ -0,0 +1,36 @@
+<div class="import-form">
+  <h2 i18n>Select a Queue To Inspect</h2>
+  <div class="row flex">
+    <div>
+      <label i18n>Queue Type</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox (onChange)="queueTypeChanged($event)"
+        [startId]="queueType"
+        placeholder="Queue Type..." i18n-placeholder>
+        <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records" 
+          i18n-entryLabel></eg-combobox-entry>
+        <eg-combobox-entry entryId="auth" entryLabel="Authority Records" 
+          i18n-entryLabel></eg-combobox-entry>
+        <eg-combobox-entry entryId="bib-acq" entryLabel="Acquisitions Records" 
+          i18n-entryLabel></eg-combobox-entry>
+      </eg-combobox>
+    </div>
+  </div>
+</div>
+
+<eg-grid *ngIf="queueType=='bib'" #bibQueueGrid 
+  persistKey="cat.vandelay.queue.list.bib"
+  (onRowActivate)="rowActivated($event)"
+  idlClass="vbq" [dataSource]="queueSource">
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    [action]="deleteSelected"></eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-grid *ngIf="queueType=='auth'" #authQueueGrid 
+  persistKey="cat.vandelay.queue.list.auth"
+  (onRowActivate)="rowActivated($event)"
+  idlClass="vaq" [dataSource]="queueSource">
+</eg-grid>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
new file mode 100644
index 0000000000..888c8a57f4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
@@ -0,0 +1,102 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Pager} from '@eg/share/util/pager';                                    
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+ at Component({
+  templateUrl: 'queue-list.component.html'
+})
+export class QueueListComponent {
+
+    queueType: string; // bib / auth / bib-acq
+    queueSource: GridDataSource;
+    deleteSelected: (rows: IdlObject[]) => void;
+
+    // points to the currently active grid.
+    queueGrid: GridComponent;
+
+    @ViewChild('bibQueueGrid') bibQueueGrid: GridComponent;
+    @ViewChild('authQueueGrid') authQueueGrid: GridComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private vandelay: VandelayService) {
+
+        this.queueType = 'bib';
+        this.queueSource = new GridDataSource();
+
+        // Reset queue grid offset
+        this.vandelay.queuePageOffset = 0;
+
+        // queue API does not support sorting
+        this.queueSource.getRows = (pager: Pager) => {
+            return this.loadQueues(pager);
+        }
+
+        this.deleteSelected = (queues: IdlObject[]) => {
+
+            // Serialize the deletes, especially if there are many of them
+            // because they can be bulky calls
+            const qtype = this.queueType;
+            const method = `open-ils.vandelay.${qtype}_queue.delete`;
+
+            const deleteNext = (queues: IdlObject[], idx: number) => {
+                const queue = queues[idx];
+                if (!queue) { 
+                    this.currentGrid().reload();
+                    return Promise.resolve(); 
+                }
+    
+                return this.net.request('open-ils.vandelay', 
+                    method, this.auth.token(), queue.id()
+                ).toPromise().then(() => deleteNext(queues, ++idx));
+            }
+
+            deleteNext(queues, 0);
+        };
+    }
+
+    currentGrid(): GridComponent {
+        // The active grid changes along with the queue type.  
+        // The inactive grid will be set to null.
+        return this.bibQueueGrid || this.authQueueGrid;
+    }
+
+    rowActivated(queue) {
+        const url = `/staff/cat/vandelay/queue/${this.queueType}/${queue.id()}`;
+        this.router.navigate([url]);
+    }
+
+    queueTypeChanged($event) {
+        this.queueType = $event.id;
+        this.queueSource.reset();
+    }
+
+
+    loadQueues(pager: Pager): Observable<any> {
+
+        if (!this.queueType) {
+            return Observable.of();
+        }
+
+        const qtype = this.queueType.match(/bib/) ? 'bib' : 'authority';
+        const method = `open-ils.vandelay.${qtype}_queue.owner.retrieve`;
+
+        return this.net.request('open-ils.vandelay', 
+            method, this.auth.token(), null, null,
+            {offset: pager.offset, limit: pager.limit}
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
new file mode 100644
index 0000000000..bd991070ef
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
@@ -0,0 +1,152 @@
+
+<eg-progress-dialog #progressDlg
+  dialogTitle="Deleting Queue..." i18n-dialogTitle></eg-progress-dialog>
+
+<ng-container *ngIf="queueSummary && queueSummary.queue">
+
+  <eg-confirm-dialog 
+    #confirmDelDlg
+    i18n-dialogTitle i18n-dialogBody
+    dialogTitle="Confirm Delete"
+    dialogBody="Delete Queue {{queueSummary.queue.name()}}?">
+  </eg-confirm-dialog>
+
+  <h2 i18n>Queue {{queueSummary.queue.name()}}</h2>
+  <div class="row pb-2">
+    <div class="col-lg-6">
+      <div class="card tight-card">
+        <h5 class="card-header" i18n>Queue Summary</h5>
+        <ul class="list-group list-group-flush">
+          <li class="list-group-item">
+            <div class="d-flex">
+              <div class="flex-3" i18n>Records in Queue:</div>
+              <div class="flex-1">{{queueSummary.total}}</div>
+              <div class="flex-3" i18n>Items in Queue:</div>
+              <div class="flex-1">{{queueSummary.total_items}}</div>
+            </div>
+          </li>
+          <li class="list-group-item">
+            <div class="d-flex">
+              <div class="flex-3" i18n>Records Imported:</div>
+              <div class="flex-1">{{queueSummary.imported}}</div>
+              <div class="flex-3" i18n>Items Imported:</div>
+              <div class="flex-1">{{queueSummary.total_items_imported}}</div>
+            </div>
+          </li>
+          <li class="list-group-item">
+            <div class="d-flex">
+              <div class="flex-3" i18n>Records Import Failures:</div>
+              <div class="flex-1">{{queueSummary.rec_import_errors}}</div>
+              <div class="flex-3" i18n>Item Import Failures:</div>
+              <div class="flex-1">{{queueSummary.item_import_errors}}</div>
+            </div>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="col-lg-6">
+      <div class="card tight-card">
+        <h5 class="card-header" i18n>Queue Actions</h5>
+        <ul class="list-group list-group-flush">
+          <li class="list-group-item">
+            <div class="d-flex">
+              <div class="flex-1">
+                <a [routerLink]="" (click)="importSelected()" 
+                  i18n>Import Selected Records</a>
+              </div>
+              <div class="flex-1">
+                <a [routerLink]="" (click)="importAll()" i18n>Import All Records</a>
+              </div>
+            </div>
+          </li>
+          <li class="list-group-item">
+            <div class="d-flex">
+              <div class="flex-1">
+                <a i18n
+                  routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}/items">
+                    View Import Items
+                </a>
+              </div>
+              <div class="flex-1">
+                <a [routerLink]="" (click)="exportNonImported()" 
+                  i18n>Export Non-Imported Records</a>
+              </div>
+            </div>
+          </li>
+          <li class="list-group-item">
+            <div class="d-flex">
+              <eg-record-bucket-dialog #bucketDialog [queueId]="queueId">
+              </eg-record-bucket-dialog>
+              <div class="flex-1">
+                <a [routerLink]="" (click)="bucketDialog.open({size:'lg'})" i18n>
+                  Copy Queue To Bucket
+                </a>
+              </div>
+              <div class="flex-1">
+                <a [routerLink]="" (click)="deleteQueue()" i18n>Delete Queue</a>
+              </div>
+            </div>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</ng-container>
+
+<ng-template #matchesTmpl let-row="row">
+  <a i18n [ngClass]="{'font-weight-bold': hasOverlayTarget(row.id)}"
+    routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}/record/{{row.id}}/matches">
+      ({{row.matches.length}})
+      {{hasOverlayTarget(row.id) ? '*' : ''}}
+  </a>
+</ng-template>
+
+<ng-template #errorsTmpl let-row="row">
+  <div *ngIf="row.error_detail">
+    <b class="text-danger" title="{{row.error_detail}}">{{row.import_error}}</b>
+  </div>
+  <div *ngIf="row.error_items.length">
+    <b class="text-danger">Items ({{row.error_items.length}})</b>
+  </div>
+</ng-template>
+
+<ng-template #importedAsTmpl let-row="row">
+  <a routerLink="/staff/catalog/record/{{row.imported_as}}">
+    {{row.imported_as}}
+  </a>
+</ng-template>
+
+
+<!-- 
+Most columns are generated programmatically from queued record attribute
+definitions.  Hide a number of stock record attributes by default
+because there are a lot of them.
+-->
+
+<eg-grid #queueGrid [dataSource]="queueSource"
+  persistKey="cat.vandelay.queue.{{queueType}}"
+  (onRowActivate)="openRecord($event)"
+  [pageOffset]="queuePageOffset()"
+  hideFields="language,pagination,price,rec_identifier,eg_tcn_source,eg_identifier,item_barcode,zsource">
+
+  <eg-grid-toolbar-checkbox i18n-label label="Records With Matches"
+    [onChange]="limitToMatches"></eg-grid-toolbar-checkbox>
+
+  <eg-grid-toolbar-checkbox i18n-label label="Non-Imported Records"
+    [onChange]="limitToNonImported"></eg-grid-toolbar-checkbox>
+
+  <eg-grid-toolbar-checkbox i18n-label label="Records with Import Errors"
+    [onChange]="limitToImportErrors"></eg-grid-toolbar-checkbox>
+
+  <eg-grid-column name="id" [index]="true" 
+    [hidden]="true"></eg-grid-column>
+  <eg-grid-column i18n-label label="Matches" 
+    name="+matches" [cellTemplate]="matchesTmpl"></eg-grid-column>
+  <eg-grid-column name="import_error" i18n-label 
+    label="Import Errors" [cellTemplate]="errorsTmpl"></eg-grid-column>
+  <eg-grid-column name="import_time" i18n-label 
+    label="Import Date" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="imported_as" i18n-label 
+    label="Imported As" [cellTemplate]="importedAsTmpl"></eg-grid-column>
+</eg-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
new file mode 100644
index 0000000000..a6f67c34d1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
@@ -0,0 +1,250 @@
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {filter} from 'rxjs/operators/filter';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Pager} from '@eg/share/util/pager';                                    
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {VandelayService, VandelayImportSelection,
+    VANDELAY_EXPORT_PATH} from './vandelay.service';
+
+ at Component({
+  templateUrl: 'queue.component.html'
+})
+export class QueueComponent implements OnInit, AfterViewInit {
+
+    queueId: number;
+    queueType: string; // bib / authority
+    queueSource: GridDataSource;
+    queuedRecClass: string;
+    queueSummary: any;
+
+    filters = {
+        matches: false,
+        nonImported: false,
+        withErrors: false
+    };
+
+    limitToMatches: (checked: boolean) => void;
+    limitToNonImported: (checked: boolean) => void;
+    limitToImportErrors: (checked: boolean) => void;
+
+    // keep a local copy for convenience
+    attrDefs: IdlObject[];
+
+    @ViewChild('queueGrid') queueGrid: GridComponent;
+    @ViewChild('confirmDelDlg') confirmDelDlg: ConfirmDialogComponent;
+    @ViewChild('progressDlg') progressDlg: ProgressDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private vandelay: VandelayService) {
+
+        this.route.paramMap.subscribe((params: ParamMap) => {                  
+            this.queueType = params.get('qtype');
+            this.queueId = +params.get('id');
+        });
+
+        this.queueSource = new GridDataSource();
+        this.queueSource.getRows = (pager: Pager) => {
+            this.vandelay.queuePageOffset = pager.offset;
+            return this.loadQueueRecords(pager);
+        };
+
+        this.limitToMatches = (checked: boolean) => {
+            this.filters.matches = checked;
+            this.queueGrid.reload();
+        };
+
+        this.limitToNonImported = (checked: boolean) => {
+            this.filters.nonImported = checked;
+            this.queueGrid.reload();
+        };
+
+        this.limitToImportErrors = (checked: boolean) => {
+            this.filters.withErrors = checked;
+            this.queueGrid.reload();
+        };
+    }
+
+    ngOnInit() {
+    }
+
+    queuePageOffset(): number {
+        return this.vandelay.queuePageOffset;
+    }
+
+    ngAfterViewInit() {
+        if (this.queueType) { 
+            this.applyQueueType(); 
+            if (this.queueId) {
+                this.loadQueueSummary();
+            }
+        }
+    }
+
+    openRecord(row: any) {
+        const url = 
+          `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}/record/${row.id}/marc`;
+        this.router.navigate([url]);
+    }
+
+    applyQueueType() {
+        this.queuedRecClass = this.queueType.match(/bib/) ? 'vqbr' : 'vqar';
+        this.vandelay.getAttrDefs(this.queueType).then(
+            attrs => {
+                this.attrDefs = attrs;
+                // Add grid columns for record attributes
+                attrs.forEach(attr => {
+                    const col = new GridColumn();
+                    col.name = attr.code(),
+                    col.label = attr.description(),
+                    col.datatype = 'string';
+                    this.queueGrid.context.columnSet.add(col);
+                });
+
+                // Reapply the grid configuration now that we've
+                // dynamically added columns.
+                this.queueGrid.context.applyGridConfig();
+            }
+        );
+    }
+
+    qtypeShort(): string {
+        return this.queueType === 'bib' ? 'bib' : 'auth';    
+    }
+
+    loadQueueSummary(): Promise<any> {
+        const method = 
+            `open-ils.vandelay.${this.qtypeShort()}_queue.summary.retrieve`;
+
+        return this.net.request(
+            'open-ils.vandelay', method, this.auth.token(), this.queueId)
+        .toPromise().then(sum => this.queueSummary = sum);
+    }
+
+    loadQueueRecords(pager: Pager): Observable<any> {
+
+        const options = {
+            clear_marc: true,
+            offset: pager.offset,
+            limit: pager.limit, 
+            flesh_import_items: true,
+            non_imported: this.filters.nonImported,
+            with_import_error: this.filters.withErrors
+        }
+
+        return this.vandelay.getQueuedRecords(
+            this.queueId, this.queueType, options, this.filters.matches).pipe(
+        filter(rec => {
+            // avoid sending mishapen data to the grid
+            // this happens (among other reasons) when the grid
+            // no longer exists
+            const e = this.evt.parse(rec);
+            if (e) { console.error(e); return false; }
+            return true;
+        }), 
+        map(rec => {
+            const recHash: any = {
+                id: rec.id(),
+                import_error: rec.import_error(),
+                error_detail: rec.error_detail(),
+                import_time: rec.import_time(),
+                imported_as: rec.imported_as(),
+                import_items: rec.import_items(),
+                error_items: rec.import_items().filter(i => i.import_error()),
+                matches: rec.matches()
+            };
+
+            // Link the record attribute values to the root record 
+            // object so the grid can find them.
+            rec.attributes().forEach(attr => {
+                const def = 
+                    this.attrDefs.filter(d => d.id() === attr.field())[0];
+                recHash[def.code()] = attr.attr_value();
+            });
+
+            return recHash;
+        }));
+    }
+
+    findOrCreateImportSelection() {
+        let selection = this.vandelay.importSelection;
+        if (!selection) {
+            selection = new VandelayImportSelection();
+            this.vandelay.importSelection = selection;
+        }
+        selection.queue = this.queueSummary.queue;
+        return selection;
+    }
+
+    hasOverlayTarget(rid: number): boolean {
+        return this.vandelay.importSelection &&
+            Boolean(this.vandelay.importSelection.overlayMap[rid]);
+    }
+
+    importSelected() {
+        const rows = this.queueGrid.context.getSelectedRows();
+        if (rows.length) {
+            const selection = this.findOrCreateImportSelection();
+            selection.recordIds = rows.map(row => row.id);
+            console.log('importing: ', this.vandelay.importSelection);
+            this.router.navigate(['/staff/cat/vandelay/import']);
+        }
+    }
+
+    importAll() {
+        const selection = this.findOrCreateImportSelection();
+        selection.importQueue = true;
+        this.router.navigate(['/staff/cat/vandelay/import']);
+    }
+
+    deleteQueue() {
+        this.confirmDelDlg.open().then(
+            yes => {
+                this.progressDlg.open();
+                return this.net.request(
+                    'open-ils.vandelay',
+                    `open-ils.vandelay.${this.qtypeShort()}_queue.delete`,
+                    this.auth.token(), this.queueId
+                ).toPromise();
+            },
+            no => {
+                this.progressDlg.close();
+                return Promise.reject('delete failed');
+            }
+        ).then(
+            resp => {
+                this.progressDlg.close();
+                const e = this.evt.parse(resp);
+                if (e) {
+                    console.error(e);
+                    alert(e);
+                } else {
+                    // Jump back to the main queue page.
+                    this.router.navigate(['/staff/cat/vandelay/queue']);
+                }
+            },
+            err => {
+                this.progressDlg.close();
+            }
+        );
+    }
+
+    exportNonImported() {
+        this.vandelay.exportQueue(this.queueSummary.queue, true);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.html
new file mode 100644
index 0000000000..db72a9aa24
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.html
@@ -0,0 +1,96 @@
+
+<ng-template #bibIdTemplate let-row="row">
+  <a routerLink="/staff/catalog/record/{{row.eg_record}}/marc_view" i18n>
+    {{row.eg_record}}
+  </a>
+</ng-template>
+
+<ng-template #targetTemplate let-row="row">
+  <ng-container *ngIf="isOverlayTarget(row.id)">
+    <span i18n-title title="Selected Merge Target" 
+      class="material-icons">check_circle</span>
+  </ng-container>
+</ng-template>
+
+<ng-container *ngIf="queueType == 'bib'">
+  <eg-grid #bibGrid [dataSource]="bibDataSource" 
+    (onRowClick)="matchRowClick($event)"
+    [disableMultiSelect]="true">
+    <!--
+    <eg-grid-toolbar-action i18n-label label="Mark As Overlay Target"
+      [action]="markOverlayTarget">
+    </eg-grid-toolbar-action>
+    -->
+    <eg-grid-column name="id" [index]="true" [hidden]="true" 
+      i18n-label label="Match ID">
+    </eg-grid-column>
+    <eg-grid-column name="selected" i18n-label label="Merge Target"
+      [cellTemplate]="targetTemplate">
+    </eg-grid-column>
+    <eg-grid-column name="eg_record" i18n-label label="Record ID"
+      [cellTemplate]="bibIdTemplate">
+    </eg-grid-column>
+    <eg-grid-column name="match_score" i18n-label label="Match Score">
+    </eg-grid-column>
+    <eg-grid-column name="bre_quality" i18n-label label="Matched Record Quality">
+    </eg-grid-column>
+    <eg-grid-column name="vqbr_quality" i18n-label label="Queued Record Quality">
+    </eg-grid-column>
+    <eg-grid-column path="bib_summary.display.title" i18n-label label="Title">
+    </eg-grid-column>
+    <eg-grid-column path="bib_summary.record.creator.usrname" 
+      i18n-label label="Creator">
+    </eg-grid-column>
+    <eg-grid-column path="bib_summary.record.create_date" datatype="timestamp"
+      i18n-label label="Create Date">
+    </eg-grid-column>
+    <eg-grid-column path="bib_summary.record.editor.usrname" 
+      i18n-label label="Editor">
+    </eg-grid-column>
+    <eg-grid-column path="bib_summary.record.edit_date" datatype="timestamp"
+      i18n-label label="Edit Date">
+    </eg-grid-column>
+  </eg-grid>
+</ng-container>
+<ng-container *ngIf="queueType == 'authority'">
+  <eg-grid #authGrid [dataSource]="authDataSource">
+    <eg-grid-column name="id" [index]="true" [hidden]="true" 
+      i18n-label label="Match ID">
+    </eg-grid-column>
+  </eg-grid>
+</ng-container>
+
+
+<!--
+{
+name: '[% l('Merge Target') %]',
+get: vlGetOverlayTargetSelector,
+formatter : vlFormatOverlayTargetSelector,
+},
+{name: '[% l('ID') %]', field:'id'},
+{   name: '[% l('View MARC') %]',
+get: vlGetViewMARC,
+formatter : vlFormatViewMatchMARC
+},
+{name: '[% l('Match Score') %]', field:'match_score'},
+{name: '[% l('Queued Record Quality') %]', field:'rec_quality'},
+{name: '[% l('Matched Record Quality') %]', field:'match_quality'},
+{name: '[% l('Creator') %]', get: vlGetCreator},
+{name: '[% l('Create Date') %]', field:'create_date', get: vlGetDateTimeField},
+{name: '[% l('Last Edit Date') %]', field:'edit_date', get: vlGetDateTimeField},
+{name: '[% l('Source') %]', field:'source'},
+]]
+}];
+
+if (recordType == 'auth') {
+vlMatchGridLayout[0].cells[0].push(
+{name: '[% l("Heading") %]', field:'heading'}
+);
+} else {
+vlMatchGridLayout[0].cells[0].push(
+{name: '[% l('TCN Source') %]', field:'tcn_source'},
+{name: '[% l('TCN Value') %]', field:'tcn_value'}
+);
+}
+-->
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
new file mode 100644
index 0000000000..74e70f1f9e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
@@ -0,0 +1,153 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {VandelayService, VandelayImportSelection} from './vandelay.service';
+
+ at Component({
+  selector: 'eg-queued-record-matches',
+  templateUrl: 'queued-record-matches.component.html'
+})
+export class QueuedRecordMatchesComponent implements OnInit {
+
+    @Input() queueType: string;
+    @Input() recordId: number;
+    @ViewChild('bibGrid') bibGrid: GridComponent;
+    @ViewChild('authGrid') authGrid: GridComponent;
+
+    queuedRecord: IdlObject;
+    bibDataSource: GridDataSource;
+    authDataSource: GridDataSource;
+    markOverlayTarget: (rows: any[]) => any;
+    matchRowClick: (row: any) => void;
+    matchMap: {[id: number]: IdlObject};
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private bib: BibRecordService,
+        private vandelay: VandelayService) {
+
+        this.bibDataSource = new GridDataSource();
+        this.authDataSource = new GridDataSource();
+
+        this.bibDataSource.getRows = (pager: Pager) => {
+            return this.getBibMatchRows(pager);
+        }
+
+        /* TODO
+        this.authDataSource.getRows = (pager: Pager) => {
+        }
+        */
+
+        // Mark or un-mark as row as the merge target on row click
+        this.matchRowClick = (row: any) => {
+            this.toggleMergeTarget(row.id);
+        }
+    }
+
+    toggleMergeTarget(matchId: number) {
+
+        if (this.isOverlayTarget(matchId)) {
+
+            // clear selection on secondary click;
+            delete this.vandelay.importSelection.overlayMap[this.recordId];
+
+        } else {
+            // Add to selection.
+            // Start a new one if necessary, which will be adopted
+            // and completed by the queue UI before import.
+
+            let selection = this.vandelay.importSelection;
+            if (!selection) {
+                selection = new VandelayImportSelection();
+                this.vandelay.importSelection = selection;
+            }
+            const match = this.matchMap[matchId];
+            selection.overlayMap[this.recordId] = match.eg_record();
+        }
+    }
+
+    isOverlayTarget(matchId: number): boolean {
+        const selection = this.vandelay.importSelection;
+        if (selection) {
+            const match = this.matchMap[matchId];
+            return selection.overlayMap[this.recordId] === match.eg_record();
+        }
+        return false;
+    }
+
+    ngOnInit() {}
+
+    // This thing is a nesty beast -- clean it up
+    getBibMatchRows(pager: Pager): Observable<any> {
+
+        return new Observable(observer => {
+
+            this.getQueuedRecord().then(() => {
+
+                const matches = this.queuedRecord.matches();
+                const recIds = [];
+                this.matchMap = {};
+                matches.forEach(m => {
+                    this.matchMap[m.id()] = m;
+                    if (!recIds.includes(m.eg_record())) {
+                        recIds.push(m.eg_record());
+                    }
+                });
+
+                const bibSummaries: {[id: number]: BibRecordSummary} = {};
+                this.bib.getBibSummary(recIds).subscribe(
+                    summary => bibSummaries[summary.id] = summary,
+                    err => {},
+                    ()  => {
+                        this.bib.fleshBibUsers(
+                            Object.values(bibSummaries).map(sum => sum.record)
+                        ).then(() => {
+                            matches.forEach(match => {
+                                const row = {
+                                    id: match.id(),
+                                    eg_record: match.eg_record(),
+                                    bre_quality: match.quality(),
+                                    vqbr_quality: this.queuedRecord.quality(),
+                                    match_score: match.match_score(),
+                                    bib_summary: bibSummaries[match.eg_record()]
+                                }
+
+                                observer.next(row);
+                            });
+
+                            observer.complete();
+                        });
+                    }
+                );
+            });
+        });
+    }
+
+    getQueuedRecord(): Promise<any> {
+        if (this.queuedRecord) {
+            return Promise.resolve('');
+        }
+        let idlClass = this.queueType === 'bib' ? 'vqbr' : 'vqar';
+        const flesh = {flesh: 1, flesh_fields: {}};
+        flesh.flesh_fields[idlClass] = ['matches'];
+        return this.pcrud.retrieve(idlClass, this.recordId, flesh)
+            .toPromise().then(rec => this.queuedRecord = rec);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.html
new file mode 100644
index 0000000000..d9e85347e0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.html
@@ -0,0 +1,31 @@
+
+<div class="row mb-3">
+  <div class="col-lg-2">
+    <button class="btn btn-info label-with-material-icon"
+      routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}">
+      <span class="material-icons">arrow_back</span>
+      <span i18n>Return to Queue</span>
+    </button>
+  </div>
+</div>
+
+<ngb-tabset #recordTabs [activeId]="recordTab" (tabChange)="onTabChange($event)">
+	<ngb-tab title="Queued Record MARC" i18n-title id="marc">
+		<ng-template ngbTabContent>
+      <eg-marc-html [recordId]="recordId" [recordType]="'vandelay-'+queueType">
+      </eg-marc-html>
+		</ng-template>
+	</ngb-tab>
+	<ngb-tab title="Record Matches" i18n-title id="matches">
+		<ng-template ngbTabContent>
+      <eg-queued-record-matches [recordId]="recordId" [queueType]="queueType">
+      </eg-queued-record-matches>
+		</ng-template>
+	</ngb-tab>
+	<ngb-tab title="Import Items" i18n-title id="items">
+		<ng-template ngbTabContent>
+      <eg-queued-record-items [recordId]="recordId">
+      </eg-queued-record-items>
+		</ng-template>
+	</ngb-tab>
+</ngb-tabset>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
new file mode 100644
index 0000000000..3a37be74c8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
@@ -0,0 +1,42 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';              
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  templateUrl: 'queued-record.component.html'
+})
+export class QueuedRecordComponent {
+
+    queueId: number;
+    queueType: string;
+    recordId: number;
+    recordTab: string;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute) {
+
+        this.route.paramMap.subscribe((params: ParamMap) => {                  
+            this.queueId = +params.get('id');
+            this.recordId = +params.get('recordId');
+            this.queueType = params.get('qtype');
+            this.recordTab = params.get('recordTab');
+        });
+    }
+
+    // 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();
+
+        const url = 
+          `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}` +
+          `/record/${this.recordId}/${this.recordTab}`;
+
+        this.router.navigate([url]);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.html
new file mode 100644
index 0000000000..6654ac49a6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.html
@@ -0,0 +1,67 @@
+<div class="row mb-2">
+  <div class="col-lg-6">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text" i18n>Show Sessions Since: </span>
+      </div>
+      <eg-date-select 
+        [initialIso]="sinceDate"
+        (onChangeAsIso)="dateFilterChange($event)">
+      </eg-date-select>
+    </div>
+  </div>
+</div>
+
+<div *ngIf="trackers.length == 0">
+    <div class="row">
+        <div class="col-lg-6">
+            <div class="alert alert-info">
+                <span i18n>No Import Sessions To Display</span>
+            </div>
+        </div>
+    </div>
+</div>
+
+  <div class="row mb-4" *ngFor="let tracker of trackers">
+    <div class="col-lg-12">
+      <div class="card tight-card">
+        <div class="card-header">
+          <div class="panel-title">
+            <span i18n>
+              {{tracker.create_time() | date:'short'}} : 
+              <span class="font-weight-bold">{{tracker.name()}}</span>
+            </span>
+          </div>
+        </div>
+        <div class="card-body">
+          <div class="row">
+            <div class="col-lg-6">
+              <!-- ensure the progress shows 100% when complete -->
+              <eg-progress-inline 
+                [max]="tracker.state() == 'complete' ? tracker.actions_performed() : tracker.total_actions() || null"
+                [value]="tracker.actions_performed()">
+              </eg-progress-inline>
+            </div>
+            <div class="col-lg-6">
+              <!-- .id (not .id()) check to see if it's fleshed yet -->
+              <span i18n *ngIf="tracker.queue().id">
+                <a class="font-weight-bold"
+                  routerLink="/staff/cat/vandelay/queue/{{tracker.record_type()}}/{{tracker.queue().id()}}">
+                  Queue {{tracker.queue().name()}}
+                </a>
+              </span>
+              <span class="pl-2" *ngIf="tracker.action_type() == 'enqueue'" i18n>Enqueuing... </span>
+              <span class="pl-2" *ngIf="tracker.action_type() == 'import'" i18n>Importing... </span>
+              <span *ngIf="tracker.state() == 'active'" i18n>Active</span>
+              <span *ngIf="tracker.state() == 'complete'" i18n>Complete</span>
+              <span *ngIf="tracker.state() == 'error'" i18n>Error</span>
+              <span class='pl-3' *ngIf="tracker.state() == 'complete'">
+                <span class="material-icons text-success">thumb_up</span>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
new file mode 100644
index 0000000000..ad7b0588e0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
@@ -0,0 +1,140 @@
+import {Component, OnInit} 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 {VandelayService} from './vandelay.service';
+
+ at Component({
+    templateUrl: 'recent-imports.component.html'
+})
+
+export class RecentImportsComponent implements OnInit {
+
+    trackers: IdlObject[];
+    refreshInterval = 2000; // ms
+    sinceDate: string;
+    pollTimeout: any;
+
+    constructor(
+        private idl: IdlService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private vandelay: VandelayService
+    ) {
+        this.trackers = [];
+    }
+
+    ngOnInit() {
+        // Default to showing all trackers created today.
+        const d = new Date();
+        d.setHours(0);
+        d.setMinutes(0);
+        d.setSeconds(0);
+        this.sinceDate = d.toISOString();
+
+        this.pollTrackers();
+    }
+
+    dateFilterChange(iso: string) {
+        if (iso) {
+            this.sinceDate = iso;
+            if (this.pollTimeout) {
+                clearTimeout(this.pollTimeout);
+                this.pollTimeout = null;
+            }
+            this.trackers = [];
+            this.pollTrackers();
+        }
+    }
+
+    pollTrackers() {
+
+        // Report on recent trackers for this workstation and for the
+        // logged in user.  Always show active trackers regardless
+        // of sinceDate.
+        const query: any = {
+            '-and': [
+                {
+                    '-or': [
+                        {workstation: this.auth.user().wsid()},
+                        {usr: this.auth.user().id()}
+                    ],
+                }, {
+                    '-or': [
+                        {create_time: {'>=': this.sinceDate}},
+                        {state: 'active'}
+                    ]
+                }
+            ]
+        };
+
+        this.pcrud.search('vst', query, {order_by: {vst: 'create_time'}})
+        .subscribe(
+            tracker => {
+                // The screen flickers less if the tracker array is 
+                // updated inline instead of rebuilt every time.
+
+                const existing = 
+                    this.trackers.filter(t => t.id() === tracker.id())[0];
+
+                if (existing) {
+                    existing.update_time(tracker.update_time());
+                    existing.state(tracker.state());
+                    existing.total_actions(tracker.total_actions());
+                    existing.actions_performed(tracker.actions_performed());
+                } else {
+
+                    // Only show the import tracker when both an enqueue
+                    // and import tracker exist for a given session.
+                    const sameSes = this.trackers.filter(
+                        t => t.session_key() === tracker.session_key())[0];
+
+                    if (sameSes) {
+                        if (sameSes.action_type() === 'enqueue') {
+                            // Remove the enqueueu tracker
+
+                            for (let idx = 0; idx < this.trackers.length; idx++) {
+                                const trkr = this.trackers[idx];
+                                if (trkr.id() === sameSes.id()) {
+                                    console.debug(
+                                        `removing tracker ${trkr.id()} from the list`);
+                                    this.trackers.splice(idx, 1);
+                                    break;
+                                }
+                            }
+                       } else if (sameSes.action_type() === 'import') {
+                            // Avoid adding the new enqueue tracker
+                            return;
+                        }
+                    }
+
+                    console.debug(`adding tracker ${tracker.id()} to list`);
+
+                    this.trackers.unshift(tracker);
+                    this.fleshTrackerQueue(tracker);
+                }
+            },
+            err => {},
+            ()  => {
+                const active = 
+                    this.trackers.filter(t => t.state() === 'active');
+
+                // Continue updating the display with updated tracker
+                // data as long as we have any active trackers.
+                if (active.length > 0) {
+                    this.pollTimeout = setTimeout(
+                        () => this.pollTrackers(), this.refreshInterval);
+                } else {
+                    this.pollTimeout = null;
+                }
+            }
+        );
+    }
+
+    fleshTrackerQueue(tracker: IdlObject) {
+        const qClass = tracker.record_type() === 'bib' ? 'vbq' : 'vaq';
+        this.pcrud.retrieve(qClass, tracker.queue())
+        .subscribe(queue => tracker.queue(queue));
+    }
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.html
new file mode 100644
index 0000000000..012a579a1b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.html
@@ -0,0 +1,6 @@
+<eg-grid #itemsGrid 
+  showFields="record,import_error,imported_as,import_time,owning_lib,call_number,barcode"
+  persistKey="cat.vandelay.queue.bib.items"
+  idlClass="vii" [dataSource]="gridSource">
+</eg-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
new file mode 100644
index 0000000000..9852a640e8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
@@ -0,0 +1,37 @@
+import {Component, Input, ViewChild} from '@angular/core';
+import {Pager} from '@eg/share/util/pager';                                    
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+ at Component({
+  selector: 'eg-queued-record-items',
+  templateUrl: 'record-items.component.html'
+})
+export class RecordItemsComponent {
+
+    @Input() recordId: number;
+
+    gridSource: GridDataSource;
+    @ViewChild('itemsGrid') itemsGrid: GridComponent;
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private vandelay: VandelayService) {
+
+        this.gridSource = new GridDataSource();
+
+        // queue API does not support sorting
+        this.gridSource.getRows = (pager: Pager) => {
+            return this.pcrud.search('vii', 
+                {record: this.recordId}, {order_by: {vii: ['id']}});
+        };
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/routing.module.ts
new file mode 100644
index 0000000000..707b92b9ca
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/routing.module.ts
@@ -0,0 +1,75 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {VandelayComponent} from './vandelay.component';
+import {ImportComponent} from './import.component';
+import {ExportComponent} from './export.component';
+import {QueueListComponent} from './queue-list.component';
+import {QueueComponent} from './queue.component';
+import {QueuedRecordComponent} from './queued-record.component';
+import {DisplayAttrsComponent} from './display-attrs.component';
+import {MergeProfilesComponent} from './merge-profiles.component';
+import {HoldingsProfilesComponent} from './holdings-profiles.component';
+import {QueueItemsComponent} from './queue-items.component';
+import {MatchSetListComponent} from './match-set-list.component';
+import {MatchSetComponent} from './match-set.component';
+import {RecentImportsComponent} from './recent-imports.component';
+
+const routes: Routes = [{
+  path: '',
+  component: VandelayComponent,
+  children: [{
+    path: '',
+    pathMatch: 'full',
+    redirectTo: 'import'
+  }, {
+    path: 'import',
+    component: ImportComponent
+  }, {
+    path: 'export',
+    component: ExportComponent
+  }, {
+    path: 'queue',
+    component: QueueListComponent
+  }, {
+    path: 'queue/:qtype/:id',
+    component: QueueComponent
+  }, {
+    path: 'queue/:qtype/:id/record/:recordId',
+    component: QueuedRecordComponent
+  }, {
+    path: 'queue/:qtype/:id/record/:recordId/:recordTab',
+    component: QueuedRecordComponent
+  }, {
+    path: 'queue/:qtype/:id/items',
+    component: QueueItemsComponent
+  }, {
+    path: 'display_attrs',
+    component: DisplayAttrsComponent
+  }, {
+    path: 'display_attrs/:atype',
+    component: DisplayAttrsComponent
+  }, {
+    path: 'merge_profiles',
+    component: MergeProfilesComponent
+  }, {
+    path: 'holdings_profiles',
+    component: HoldingsProfilesComponent
+  }, {
+    path: 'match_sets',
+    component: MatchSetListComponent
+  }, {
+    path: 'match_sets/:id/:matchSetTab',
+    component: MatchSetComponent
+  }, {
+    path: 'active_imports',
+    component: RecentImportsComponent
+  }]
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: []
+})
+
+export class VandelayRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.html
new file mode 100644
index 0000000000..a81472d674
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.html
@@ -0,0 +1,44 @@
+
+<ul class="nav nav-pills nav-fill pb-4">
+  <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" [ngClass]="{active: tab=='import'}" 
+      routerLink="/staff/cat/vandelay/import" i18n>Import</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='queue'}" 
+      routerLink="/staff/cat/vandelay/queue" i18n>Inspect Queue</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='display_attrs'}" 
+      routerLink="/staff/cat/vandelay/display_attrs/bib" 
+        i18n>Record Display Attributes</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='merge_profiles'}" 
+      routerLink="/staff/cat/vandelay/merge_profiles" 
+        i18n>Merge / Overlay Profiles</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='match_sets'}" 
+      routerLink="/staff/cat/vandelay/match_sets" 
+        i18n>Record Match Sets</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='holdings_profiles'}" 
+      routerLink="/staff/cat/vandelay/holdings_profiles" 
+      i18n>Holdings Import Profiles</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [ngClass]="{active: tab=='active_imports'}" 
+      routerLink="/staff/cat/vandelay/active_imports" 
+      i18n>Recent Imports</a>
+  </li>
+</ul>
+
+<!-- load nav-specific page -->
+<router-outlet></router-outlet>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
new file mode 100644
index 0000000000..0bfad42d5c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
@@ -0,0 +1,34 @@
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from "@angular/router";
+import {take} from 'rxjs/operators/take';
+import {VandelayService} from './vandelay.service';
+import {IdlObject} from '@eg/core/idl.service';
+
+ at Component({
+  templateUrl: 'vandelay.component.html'
+})
+export class VandelayComponent implements OnInit, AfterViewInit {
+    tab: string;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private vandelay: VandelayService) {
+
+        // As the parent component of the vandelay route tree, our
+        // activated route never changes.  Instead, listen for global
+        // route events, then ask for the first segement of the first
+        // child, which will be the tab name.
+        this.router.events.subscribe(routeEvent => {
+            if (routeEvent instanceof NavigationEnd) {
+                this.route.firstChild.url.pipe(take(1))
+                .subscribe(segments => this.tab = segments[0].path);
+            }
+        });
+    }
+
+    ngOnInit() {}
+
+    ngAfterViewInit() {}
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts
new file mode 100644
index 0000000000..9bbfd46a26
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts
@@ -0,0 +1,61 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {HttpClientModule} from '@angular/common/http';
+import {TreeModule} from '@eg/share/tree/tree.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';
+import {QueueComponent} from './queue.component';
+import {QueueListComponent} from './queue-list.component';
+import {QueuedRecordComponent} from './queued-record.component';
+import {QueuedRecordMatchesComponent} from './queued-record-matches.component';
+import {DisplayAttrsComponent} from './display-attrs.component';
+import {MergeProfilesComponent} from './merge-profiles.component';
+import {HoldingsProfilesComponent} from './holdings-profiles.component';
+import {QueueItemsComponent} from './queue-items.component';
+import {RecordItemsComponent} from './record-items.component';
+import {MatchSetListComponent} from './match-set-list.component';
+import {MatchSetComponent} from './match-set.component';
+import {MatchSetExpressionComponent} from './match-set-expression.component';
+import {MatchSetQualityComponent} from './match-set-quality.component';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+import {RecentImportsComponent} from './recent-imports.component';
+
+ at NgModule({
+  declarations: [
+    VandelayComponent,
+    ImportComponent,
+    ExportComponent,
+    QueueComponent,
+    QueueListComponent,
+    QueuedRecordComponent,
+    QueuedRecordMatchesComponent,
+    DisplayAttrsComponent,
+    MergeProfilesComponent,
+    HoldingsProfilesComponent,
+    QueueItemsComponent,
+    RecordItemsComponent,
+    MatchSetListComponent,
+    MatchSetComponent,
+    MatchSetExpressionComponent,
+    MatchSetQualityComponent,
+    MatchSetNewPointComponent,
+    RecentImportsComponent
+  ],
+  imports: [
+    TreeModule,
+    StaffCommonModule,
+    CatalogCommonModule,
+    VandelayRoutingModule,
+    HttpClientModule,
+  ],
+  providers: [
+    VandelayService
+  ]
+})
+
+export class VandelayModule {
+}
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
index 0000000000..7a6d6405e4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts
@@ -0,0 +1,343 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {tap} from 'rxjs/operators/tap';
+import {map} from 'rxjs/operators/map';
+import {HttpClient} from '@angular/common/http';
+import {saveAs} from 'file-saver/FileSaver';
+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';
+
+export const VANDELAY_EXPORT_PATH = '/exporter';
+export const VANDELAY_UPLOAD_PATH = '/vandelay-upload';
+
+export class VandelayImportSelection {
+    recordIds: number[];
+    queue: IdlObject;
+    importQueue: boolean; // import the whole queue
+    overlayMap: {[qrId: number]: /* breId */ number};
+
+    constructor() {
+       this.recordIds = [];
+       this.overlayMap = {};
+    }
+}
+
+ at Injectable()
+export class VandelayService {
+
+    allQueues: {[qtype: string]: IdlObject[]};
+    activeQueues: {[qtype: string]: IdlObject[]}; 
+    attrDefs: {[atype: string]: IdlObject[]};
+    bibSources: IdlObject[];
+    bibBuckets: IdlObject[];
+    copyStatuses: IdlObject[];
+    matchSets: {[stype: string]: IdlObject[]};
+    importItemAttrDefs: IdlObject[];
+    bibTrashGroups: IdlObject[];
+    mergeProfiles: IdlObject[];
+
+    // Used for tracking records between the queue page and
+    // the import page.  Fields managed externally.
+    importSelection: VandelayImportSelection;
+
+    // Track the last grid offset in the queue page so we
+    // can return the user to the same page of data after
+    // going to the matches page.
+    queuePageOffset: number;
+
+    constructor(
+        private http: HttpClient,
+        private idl: IdlService,
+        private org: OrgService,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private perm: PermService
+    ) {
+        this.attrDefs = {};
+        this.activeQueues = {};
+        this.allQueues = {};
+        this.matchSets = {};
+        this.importSelection = null;
+        this.queuePageOffset = 0;
+    }
+
+    getAttrDefs(dtype: string): Promise<IdlObject[]> {
+        if (this.attrDefs[dtype]) {
+            return Promise.resolve(this.attrDefs[dtype]);
+        }
+        const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad';
+        const orderBy = {};
+        orderBy[cls] = 'id'
+        return this.pcrud.retrieveAll(cls, 
+            {order_by: orderBy}, {atomic: true}).toPromise()
+        .then(list => {
+            this.attrDefs[dtype] = list;
+            return list;
+        });
+    }
+
+    getMergeProfiles(): Promise<IdlObject[]> {
+        if (this.mergeProfiles) {
+            return Promise.resolve(this.mergeProfiles);
+        }
+
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+        return this.pcrud.search('vmp', 
+            {owner: owners}, {order_by: {vmp: ['name']}}, {atomic: true})
+        .toPromise().then(profiles => {
+            this.mergeProfiles = profiles;
+            return profiles;
+        });
+    }
+
+    // Returns a promise resolved with the list of queues.
+    // Also emits the onQueueListUpdate event so listeners
+    // can detect queue content changes.
+    getAllQueues(qtype: string): Promise<IdlObject[]> {
+        if (this.allQueues[qtype]) {
+            return Promise.resolve(this.allQueues[qtype]);
+        } else {
+            this.allQueues[qtype] = [];
+        }
+
+        // could be a big list, invoke in streaming mode
+        return this.net.request(
+            'open-ils.vandelay',
+            `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
+            this.auth.token()
+        ).pipe(tap(
+            queue => this.allQueues[qtype].push(queue)
+        )).toPromise().then(() => this.allQueues[qtype]);
+    }
+
+
+    // Returns a promise resolved with the list of queues.
+    // Also emits the onQueueListUpdate event so listeners
+    // can detect queue content changes.
+    getActiveQueues(qtype: string): Promise<IdlObject[]> {
+        if (this.activeQueues[qtype]) {
+            return Promise.resolve(this.activeQueues[qtype]);
+        } else {
+            this.activeQueues[qtype] = [];
+        }
+
+        // could be a big list, invoke in streaming mode
+        return this.net.request(
+            'open-ils.vandelay',
+            `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
+            this.auth.token(), null, {complete: 'f'}
+        ).pipe(tap(
+            queue => this.activeQueues[qtype].push(queue)
+        )).toPromise().then(() => this.activeQueues[qtype]);
+    }
+
+    getBibSources(): Promise<IdlObject[]> {
+        if (this.bibSources) {
+            return Promise.resolve(this.bibSources);
+        }
+
+        return this.pcrud.retrieveAll('cbs', 
+          {order_by: {cbs: 'id'}}, 
+          {atomic: true}
+        ).toPromise().then(sources => {
+            this.bibSources = sources;
+            return sources;
+        });
+    }
+
+    getItemImportDefs(): Promise<IdlObject[]> {
+        if (this.importItemAttrDefs) {
+            return Promise.resolve(this.importItemAttrDefs);
+        }
+
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+        return this.pcrud.search('viiad', {owner: owners}, {}, {atomic: true})
+        .toPromise().then(defs => {
+            this.importItemAttrDefs = defs;
+            return defs;
+        });
+    }
+
+    // todo: differentiate between biblio and authority a la queue api
+    getMatchSets(mtype: string): Promise<IdlObject[]> {
+    
+        const mstype = mtype.match(/bib/) ? 'biblio' : 'authority';
+
+        if (this.matchSets[mtype]) {
+            return Promise.resolve(this.matchSets[mtype]);
+        } else {
+            this.matchSets[mtype] = [];
+        }
+
+        const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+        return this.pcrud.search('vms', 
+            {owner: owners, mtype: mstype}, {}, {atomic: true})
+        .toPromise().then(sets => {
+            this.matchSets[mtype] = sets;
+            return sets;
+        });
+    }
+
+    getBibBuckets(): Promise<IdlObject[]> {
+        if (this.bibBuckets) {
+            return Promise.resolve(this.bibBuckets);
+        }
+
+        const bkts = [];
+        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 => bkts.push(bkt))).toPromise().then(() => bkts);
+        ).toPromise().then(bkts => {
+            this.bibBuckets = bkts;
+            return bkts;
+        });
+    }
+
+    getCopyStatuses(): Promise<any> {
+        if (this.copyStatuses) {
+            return Promise.resolve(this.copyStatuses);
+        }
+        return this.pcrud.retrieveAll('ccs', {}, {atomic: true})
+        .toPromise().then(stats => {
+            this.copyStatuses = stats;
+            return stats;
+        });
+    }
+
+    getBibTrashGroups(): Promise<any> {
+        if (this.bibTrashGroups) {
+            return Promise.resolve(this.bibTrashGroups);
+        }
+
+        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 => {
+            this.bibTrashGroups = groups;
+            return groups;
+        });
+    }
+
+
+    // Create a queue and return the ID of the new queue via promise.
+    createQueue(
+        queueName: string, 
+        recordType: string, 
+        importDefId: number, 
+        matchSet: number, 
+        matchBucket: number): Promise<number> {
+
+        const method = `open-ils.vandelay.${recordType}_queue.create`;
+
+        let qType = recordType;
+        if (recordType.match(/acq/)) {
+            let qType = 'acq';
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.vandelay', method, 
+                this.auth.token(), queueName, null, qType, 
+                matchSet, importDefId, matchBucket
+            ).subscribe(queue => {
+                const e = this.evt.parse(queue);
+                if (e) { 
+                    alert(e);
+                    reject(e);
+                } else {
+                    resolve(queue.id());
+                }
+            });
+        });
+    }
+
+    getQueuedRecords(queueId: number, queueType: string, 
+      options?: any, limitToMatches?: boolean): Observable<any> {
+
+        const qtype = queueType.match(/bib/) ? 'bib' : 'auth';
+
+        let method = 
+          `open-ils.vandelay.${qtype}_queue.records.retrieve`;
+
+        if (limitToMatches) {
+            method = 
+              `open-ils.vandelay.${qtype}_queue.records.matches.retrieve`;
+        }
+
+        return this.net.request('open-ils.vandelay', 
+            method, this.auth.token(), queueId, options);
+    }
+
+    // Download a queue as a MARC file.
+    exportQueue(queue: IdlObject, nonImported?: boolean) {
+
+        const etype = queue.queue_type().match(/auth/) ? 'auth' : 'bib';
+
+        let url = 
+          `${VANDELAY_EXPORT_PATH}?type=${etype}&queueid=${queue.id()}`
+
+        let saveName = queue.name();
+           
+        if (nonImported) {
+            url += '&nonimported=1';
+            saveName += '_nonimported';
+        }
+
+        saveName += '.mrc';
+
+        this.http.get(url, {responseType: 'text'}).subscribe(
+            data => {
+                saveAs(
+                    new Blob([data], {type: 'application/octet-stream'}),
+                    saveName
+                );
+            },
+            err  => {
+                console.error(err);
+            }
+        );
+    }
+
+    // Poll every 2 seconds for session tracker updates so long 
+    // as the session tracker is active.
+    // Returns an Observable of tracker objects.
+    pollSessionTracker(id: number): Observable<IdlObject> {
+        return new Observable(observer => {
+            this.getNextSessionTracker(id, observer);
+        });
+    }
+
+    getNextSessionTracker(id: number, observer: any) {
+
+		// No need for this to be an authoritative call.
+        // It will complete eventually regardless.
+        this.pcrud.retrieve('vst', id).subscribe(
+            tracker => {
+                if (tracker && tracker.state() === 'active') {
+                    observer.next(tracker);
+                    setTimeout(() => 
+                        this.getNextSessionTracker(id, observer), 2000);
+                } else {
+                    console.debug(
+                        `Vandelay session tracker ${id} is ${tracker.state()}`);
+                    observer.complete();
+                }
+            }
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
index b43d8e79da..92209218e6 100644
--- a/Open-ILS/src/eg2/src/app/staff/nav.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html
@@ -184,7 +184,7 @@
             <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">
+          <a routerLink="/staff/cat/vandelay/import" class="dropdown-item">
             <span class="material-icons">import_export</span>
             <span i18n>MARC Batch Import/Export</span>
           </a>
diff --git a/Open-ILS/src/eg2/src/app/staff/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
index b515f389af..6f20336660 100644
--- a/Open-ILS/src/eg2/src/app/staff/routing.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
@@ -31,6 +31,9 @@ const routes: Routes = [{
     path: 'circ',
     loadChildren : '@eg/staff/circ/routing.module#CircRoutingModule'
   }, {
+    path: 'cat',
+    loadChildren : '@eg/staff/cat/routing.module#CatRoutingModule'
+  }, {
     path: 'catalog',
     loadChildren : '@eg/staff/catalog/catalog.module#CatalogModule'
   }, {
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
index f5e4c94652..4399111883 100644
--- 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
@@ -1,6 +1,7 @@
 <ng-template #dialogContent>
   <div class="modal-header bg-info">
-    <h4 class="modal-title" i18n>Add To Record #{{recId}} to Bucket</h4>
+    <h4 class="modal-title" *ngIf="recId" i18n>Add To Record #{{recId}} to Bucket</h4>
+    <h4 class="modal-title" *ngIf="qId" i18n>Add Records from queue #{{qId}} to Bucket</h4>
     <button type="button" class="close" 
       i18n-aria-label aria-label="Close" 
       (click)="dismiss('cross_click')">
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
index 1f127b42c2..eb99aec3be 100644
--- 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
@@ -24,11 +24,19 @@ export class RecordBucketDialogComponent
     newBucketDesc: string;
     buckets: any[];
 
+    @Input() bucketType: string;
+
     recId: number;
     @Input() set recordId(id: number) {
         this.recId = id;
     }
 
+    // Add items from a (vandelay) bib queue to a bucket
+    qId: number;
+    @Input() set queueId(id: number) {
+        this.qId = id;
+    }
+
     constructor(
         private modal: NgbModal, // required for passing to parent
         private renderer: Renderer2,
@@ -41,6 +49,12 @@ export class RecordBucketDialogComponent
     }
 
     ngOnInit() {
+        
+        if (this.qId) {
+            this.bucketType = 'vandelay_queue';
+        } else {
+            this.bucketType = 'staff_client';
+        }
 
         this.onOpen$.subscribe(ok => {
             // Reset data on dialog open
@@ -53,7 +67,7 @@ export class RecordBucketDialogComponent
                 'open-ils.actor',
                 'open-ils.actor.container.retrieve_by_class.authoritative',
                 this.auth.token(), this.auth.user().id(),
-                'biblio', 'staff_client'
+                'biblio', this.bucketType
             ).subscribe(buckets => this.buckets = buckets);
         });
     }
@@ -69,7 +83,7 @@ export class RecordBucketDialogComponent
         bucket.owner(this.auth.user().id());
         bucket.name(this.newBucketName);
         bucket.description(this.newBucketDesc);
-        bucket.btype('staff_client');
+        bucket.btype(this.bucketType);
 
         this.net.request(
             'open-ils.actor',
@@ -80,6 +94,11 @@ export class RecordBucketDialogComponent
             if (evt) {
                 this.toast.danger(evt.desc);
             } else {
+                // make it find-able to the queue-add method which
+                // requires the bucket name.
+                bucket.id(bktId);
+                this.buckets.push(bucket);
+
                 this.addToBucket(bktId);
             }
         });
@@ -87,8 +106,16 @@ export class RecordBucketDialogComponent
 
     // Add the record to the selected existing bucket
     addToBucket(id: number) {
+        if (this.recId) {
+            this.addRecordToBucket(id);
+        } else if (this.qId) {
+            this.addQueueToBucket(id);
+        }
+    }
+
+    addRecordToBucket(bucketId: number) {
         const item = this.idl.create('cbrebi');
-        item.bucket(id);
+        item.bucket(bucketId);
         item.target_biblio_record_entry(this.recId);
         this.net.request(
             'open-ils.actor',
@@ -103,6 +130,24 @@ export class RecordBucketDialogComponent
             }
         });
     }
+
+    addQueueToBucket(bucketId: number) {
+        const bucket = this.buckets.filter(b => b.id() === bucketId)[0];
+        if (!bucket) { return; }
+
+        this.net.request(
+            'open-ils.vandelay',
+            'open-ils.vandelay.bib_queue.to_bucket',
+            this.auth.token(), this.qId, bucket.name()
+        ).toPromise().then(resp => {
+            const evt = this.evt.parse(resp);
+            if (evt) {
+                this.toast.danger(evt.toString());
+            } else {
+                this.close();
+            }
+        });
+    }
 }
 
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
index 9f11bd84da..4ee6b28502 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
@@ -532,17 +532,25 @@ sub retrieve_queued_records {
 
         } elsif( $$options{with_item_import_error} and $type eq 'bib') {
 
-            $query->{from} = {$class => 'vii'};
+            $query->{from} = {$class => {'vii' => {}}};
             $query->{where}->{'+vii'} = {import_error => {'!=' => undef}};
         }
     }
 
     if($self->api_name =~ /matches/) {
         # find only records that have matches
-        $query->{from} = {$class => {$mclass => {type => 'right'}}};
+        if (ref $query->{from}) {
+            $query->{from}{$class}{$mclass} = {type => 'right'};
+        } else {
+            $query->{from} = {$class => {$mclass => {type => 'right'}}};
+        }
     } else {
         # join to mclass for sorting (see below)
-        $query->{from} = {$class => {$mclass => {type => 'left'}}};
+        if (ref $query->{from}) {
+            $query->{from}{$class}{$mclass} = {type => 'left'};
+        } else {
+            $query->{from} = {$class => {$mclass => {type => 'left'}}};
+        }
     }
 
     # order by the matched bib records to group like queued records
@@ -915,7 +923,7 @@ sub create_session_tracker {
         # if other trackers exist for this key, adopt the name
         my $existing = 
             $e->search_vandelay_session_tracker({session_key => $key})->[0];
-        $name = $existing->name if $name;
+        $name = $existing->name if $existing;
 
     } else {
         # anonymous tracker
@@ -1536,7 +1544,7 @@ __PACKAGE__->register_method(
 );
 
 sub owner_queue_retrieve {
-    my($self, $conn, $auth, $owner_id, $filters) = @_;
+    my($self, $conn, $auth, $owner_id, $filters, $pager) = @_;
     my $e = new_editor(authtoken => $auth, xact => 1);
     return $e->die_event unless $e->checkauth;
     $owner_id = $e->requestor->id; # XXX add support for viewing other's queues?
@@ -1545,12 +1553,18 @@ sub owner_queue_retrieve {
     my $search = {owner => $owner_id};
     $search->{$_} = $filters->{$_} for keys %$filters;
 
+    my %paging;
+    if ($pager) {
+        $paging{limit} = $pager->{limit} || 1000;
+        $paging{offset} = $pager->{offset} || 0;
+    }
+
     if($self->{record_type} eq 'bib') {
         $queues = $e->search_vandelay_bib_queue(
-            [$search, {order_by => {vbq => 'evergreen.lowercase(name)'}}]);
+            [$search, {%paging, order_by => {vbq => 'evergreen.lowercase(name)'}}]);
     } else {
         $queues = $e->search_vandelay_authority_queue(
-            [$search, {order_by => {vaq => 'evergreen.lowercase(name)'}}]);
+            [$search, {%paging, order_by => {vaq => 'evergreen.lowercase(name)'}}]);
     }
     $conn->respond($_) for @$queues;
     $e->rollback;
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 26c9569f6b..e8ef9ed3f6 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -19420,6 +19420,65 @@ VALUES (
     )
 );
 
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.cat.vandelay.queue.bib', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.vandelay.queue.bib',
+        'Grid Config: Vandelay Bib Queue',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.vandelay.queue.auth', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.vandelay.queue.auth',
+        'Grid Config: Vandelay Authority Queue',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.match_set.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.match_set.list',
+        'Grid Config: Vandelay Match Sets',
+        'cwst', 'label'
+    )
+), (
+    'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+    oils_i18n_gettext(
+        'staff.cat.vandelay.match_set.quality',
+        'Grid Config: Vandelay Match Quality Metrics',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.items',
+        'Grid Config: Vandelay Queue Import Items',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.list.bib', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.list.bib',
+        'Grid Config: Vandelay Bib Queue List',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.bib.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.bib.items',
+        'Grid Config: Vandelay Bib Items',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.list.auth', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.list.auth',
+        'Grid Config: Vandelay Authority Queue List',
+        'cwst', 'label'
+    )
+);
+
 
 INSERT into config.org_unit_setting_type (name, label, description, datatype) 
 VALUES ( 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
new file mode 100644
index 0000000000..4eb2206201
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.vandelay-grid-settings.sql
@@ -0,0 +1,66 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.cat.vandelay.queue.bib', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.vandelay.queue.bib',
+        'Grid Config: Vandelay Bib Queue',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.vandelay.queue.auth', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.vandelay.queue.auth',
+        'Grid Config: Vandelay Authority Queue',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.match_set.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.match_set.list',
+        'Grid Config: Vandelay Match Sets',
+        'cwst', 'label'
+    )
+), (
+    'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+    oils_i18n_gettext(
+        'staff.cat.vandelay.match_set.quality',
+        'Grid Config: Vandelay Match Quality Metrics',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.items',
+        'Grid Config: Vandelay Queue Import Items',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.list.bib', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.list.bib',
+        'Grid Config: Vandelay Bib Queue List',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.bib.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.bib.items',
+        'Grid Config: Vandelay Bib Items',
+        'cwst', 'label'
+    )
+), (
+    'cat.vandelay.queue.list.auth', 'gui', 'object',
+    oils_i18n_gettext(
+        'cat.vandelay.queue.list.auth',
+        'Grid Config: Vandelay Authority Queue List',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;
+
+
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index ee48e6bd7b..eb1d4719c6 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -312,7 +312,7 @@
             </a>
           </li>
           <li>
-            <a href="./cat/catalog/vandelay" target="_self">
+            <a href="/eg2/staff/cat/vandelay/import">
               <span class="glyphicon glyphicon-transfer"></span>
               [% l('MARC Batch Import/Export') %]
             </a>

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/src/eg2/package-lock.json                 |   5 +
 Open-ILS/src/eg2/package.json                      |   1 +
 Open-ILS/src/eg2/src/app/common.module.ts          |   3 +
 .../src/app/share/combobox/combobox.component.ts   |   6 +
 .../eg2/src/app/share/dialog/alert.component.html  |   9 +
 .../eg2/src/app/share/dialog/alert.component.ts    |  18 +
 Open-ILS/src/eg2/src/app/share/grid/grid.ts        |   2 +-
 .../src/eg2/src/app/staff/cat/routing.module.ts    |  15 +
 .../cat/vandelay/display-attrs.component.html      |  17 +
 .../staff/cat/vandelay/display-attrs.component.ts  |  35 ++
 .../app/staff/cat/vandelay/export.component.html   | 125 ++++
 .../src/app/staff/cat/vandelay/export.component.ts | 138 +++++
 .../cat/vandelay/holdings-profiles.component.ts    |   9 +
 .../app/staff/cat/vandelay/import.component.html   | 301 ++++++++++
 .../src/app/staff/cat/vandelay/import.component.ts | 646 +++++++++++++++++++++
 .../vandelay/match-set-expression.component.html   |  69 +++
 .../cat/vandelay/match-set-expression.component.ts | 219 +++++++
 .../cat/vandelay/match-set-list.component.html     |  37 ++
 .../staff/cat/vandelay/match-set-list.component.ts |  78 +++
 .../vandelay/match-set-new-point.component.html    |  77 +++
 .../cat/vandelay/match-set-new-point.component.ts  |  65 +++
 .../cat/vandelay/match-set-quality.component.html  |  27 +
 .../cat/vandelay/match-set-quality.component.ts    | 104 ++++
 .../staff/cat/vandelay/match-set.component.html    |  36 ++
 .../app/staff/cat/vandelay/match-set.component.ts  |  51 ++
 .../staff/cat/vandelay/merge-profiles.component.ts |   9 +
 .../staff/cat/vandelay/queue-items.component.html  |  19 +
 .../staff/cat/vandelay/queue-items.component.ts    |  59 ++
 .../staff/cat/vandelay/queue-list.component.html   |  38 ++
 .../app/staff/cat/vandelay/queue-list.component.ts | 102 ++++
 .../app/staff/cat/vandelay/queue.component.html    | 156 +++++
 .../src/app/staff/cat/vandelay/queue.component.ts  | 256 ++++++++
 .../vandelay/queued-record-matches.component.html  |  96 +++
 .../vandelay/queued-record-matches.component.ts    | 152 +++++
 .../cat/vandelay/queued-record.component.html      |  31 +
 .../staff/cat/vandelay/queued-record.component.ts  |  42 ++
 .../cat/vandelay/recent-imports.component.html     |  67 +++
 .../staff/cat/vandelay/recent-imports.component.ts | 140 +++++
 .../staff/cat/vandelay/record-items.component.html |   6 +
 .../staff/cat/vandelay/record-items.component.ts   |  37 ++
 .../src/app/staff/cat/vandelay/routing.module.ts   |  75 +++
 .../app/staff/cat/vandelay/vandelay.component.html |  44 ++
 .../app/staff/cat/vandelay/vandelay.component.ts   |  34 ++
 .../src/app/staff/cat/vandelay/vandelay.module.ts  |  61 ++
 .../src/app/staff/cat/vandelay/vandelay.service.ts | 317 ++++++++++
 Open-ILS/src/eg2/src/app/staff/nav.component.html  |   2 +-
 Open-ILS/src/eg2/src/app/staff/routing.module.ts   |   3 +
 .../buckets/record-bucket-dialog.component.html    |   3 +-
 .../buckets/record-bucket-dialog.component.ts      |  51 +-
 .../perlmods/lib/OpenILS/Application/Vandelay.pm   |  28 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  90 +++
 .../upgrade/1150.data.vandelay-grid-settings.sql   |  88 +++
 .../1151.data.vandelay-template-settings.sql       |  15 +
 Open-ILS/src/templates/staff/navbar.tt2            |   2 +-
 .../Cataloging/vandelay-angular-port.adoc          |  28 +
 55 files changed, 4130 insertions(+), 14 deletions(-)
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/alert.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/display-attrs.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/export.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/holdings-profiles.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-expression.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-new-point.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-quality.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/merge-profiles.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-list.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/recent-imports.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/record-items.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.service.ts
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1150.data.vandelay-grid-settings.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1151.data.vandelay-template-settings.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list