[open-ils-commits] [GIT] Evergreen ILS branch rel_3_4 updated. 4b7914768f3149249e1d86152c09df3c98a587dd

Evergreen Git git at git.evergreen-ils.org
Fri Dec 13 14:38:33 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, rel_3_4 has been updated
       via  4b7914768f3149249e1d86152c09df3c98a587dd (commit)
       via  c2fdba8260e1ab7670a51a4e95a576e886095931 (commit)
       via  45dbb43574d2bfa5506064735f7eac8131e778ec (commit)
       via  12468d0e0212027d8772583d434760643dfdb04d (commit)
       via  032dc3b1fbfa1ccf93238948dd73d54b6baadd1c (commit)
       via  eed52b506f4cdf3a607f12b2f122ed9844710103 (commit)
       via  e01b4c822e317ca4da0ca74336d480dc5a57a3c5 (commit)
       via  520139cb60d90dc5fea8529ec637a846a83a7790 (commit)
       via  436d7a1be877684b3ac43b09ac5cb52a6dbf87b3 (commit)
       via  085ebb527ee2eff750e273dcb30f86ef4825966d (commit)
       via  63438e048405301e613d4287c79a49f296fd8100 (commit)
      from  cbbffe290091043b2d6a4fa93c281a1a65e9166a (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 4b7914768f3149249e1d86152c09df3c98a587dd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Dec 13 14:27:16 2019 -0500

    LP1830391 Stamping DB upgrate (hatch omnibus)
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 68ceac645e..36b5d08863 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1195', :eg_version); -- dbwells/khuckins/gmcharlt
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1197', :eg_version); -- berick/jboyer
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql b/Open-ILS/src/sql/Pg/upgrade/1197.data.hatch-enable-print.sql
similarity index 94%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
rename to Open-ILS/src/sql/Pg/upgrade/1197.data.hatch-enable-print.sql
index f6cc5b3ced..2008b1345c 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1197.data.hatch-enable-print.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1197', :eg_version);
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (

commit c2fdba8260e1ab7670a51a4e95a576e886095931
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Dec 13 14:13:40 2019 -0500

    LP1830391 Angular test spec updates for Hatch store updates
    
    The store service now leverages Hatch for workstation storage.  Update
    unit tests which load the store service to also load and propagate the
    Hatch service.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/eg2/src/app/core/format.spec.ts b/Open-ILS/src/eg2/src/app/core/format.spec.ts
index cd5feaa107..8a4877f1dc 100644
--- a/Open-ILS/src/eg2/src/app/core/format.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.spec.ts
@@ -9,6 +9,7 @@ import {OrgService} from './org.service';
 import {LocaleService} from './locale.service';
 import {Location} from '@angular/common';
 import {FormatService} from './format.service';
+import {HatchService} from './hatch.service';
 import {SpyLocation} from '@angular/common/testing';
 import localeArJO from '@angular/common/locales/ar-JO';
 import localeCs from '@angular/common/locales/cs';
@@ -26,6 +27,7 @@ describe('FormatService', () => {
     let evtService: EventService;
     let storeService: StoreService;
     let localeService: LocaleService;
+    let hatchService: HatchService;
     // tslint:disable-next-line:prefer-const
     let location: SpyLocation;
     let service: FormatService;
@@ -35,7 +37,8 @@ describe('FormatService', () => {
         datePipe = new DatePipe('en');
         idlService = new IdlService();
         evtService = new EventService();
-        storeService = new StoreService(null /* CookieService */);
+        hatchService = new HatchService();
+        storeService = new StoreService(null /* CookieService */, hatchService);
         netService = new NetService(evtService);
         authService = new AuthService(evtService, netService, storeService);
         pcrudService = new PcrudService(idlService, netService, authService);
diff --git a/Open-ILS/src/eg2/src/app/core/org.spec.ts b/Open-ILS/src/eg2/src/app/core/org.spec.ts
index 78c2f26657..90c5ac599f 100644
--- a/Open-ILS/src/eg2/src/app/core/org.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/org.spec.ts
@@ -5,6 +5,7 @@ import {AuthService} from './auth.service';
 import {PcrudService} from './pcrud.service';
 import {StoreService} from './store.service';
 import {OrgService} from './org.service';
+import {HatchService} from './hatch.service';
 
 describe('OrgService', () => {
     let idlService: IdlService;
@@ -14,11 +15,13 @@ describe('OrgService', () => {
     let orgService: OrgService;
     let evtService: EventService;
     let storeService: StoreService;
+    let hatchService: HatchService;
 
     beforeEach(() => {
         idlService = new IdlService();
         evtService = new EventService();
-        storeService = new StoreService(null /* CookieService */);
+        hatchService = new HatchService();
+        storeService = new StoreService(null /* CookieService */, hatchService);
         netService = new NetService(evtService);
         authService = new AuthService(evtService, netService, storeService);
         pcrudService = new PcrudService(idlService, netService, authService);
diff --git a/Open-ILS/src/eg2/src/app/core/store.spec.ts b/Open-ILS/src/eg2/src/app/core/store.spec.ts
index ae6c27fbf5..011ecb2c51 100644
--- a/Open-ILS/src/eg2/src/app/core/store.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/store.spec.ts
@@ -1,9 +1,12 @@
 import {StoreService} from './store.service';
+import {HatchService} from './hatch.service';
 
 describe('StoreService', () => {
     let service: StoreService;
+    let hatchService: HatchService;
     beforeEach(() => {
-        service = new StoreService(null /* CookieService */);
+        hatchService = new HatchService();
+        service = new StoreService(null /* CookieService */, hatchService);
     });
 
     it('should set/get a localStorage value', () => {
diff --git a/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
index 1e1208e0cf..814160355e 100644
--- a/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
+++ b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
@@ -4,15 +4,18 @@ import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
 import {EventService} from '@eg/core/event.service';
 import {StoreService} from '@eg/core/store.service';
+import {HatchService} from '@eg/core/hatch.service';
 
 let netService: NetService;
 let authService: AuthService;
 let evtService: EventService;
 let storeService: StoreService;
+let hatchService: HatchService;
 
 beforeEach(() => {
     evtService = new EventService();
-    storeService = new StoreService(null /* CookieService */);
+    hatchService = new HatchService();
+    storeService = new StoreService(null /* CookieService */, hatchService);
     netService = new NetService(evtService);
     authService = new AuthService(evtService, netService, storeService);
 });

commit 45dbb43574d2bfa5506064735f7eac8131e778ec
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Dec 12 16:47:31 2019 -0500

    LP1830391 Warn on dupe workstation settings
    
    Check for duplication workstations (by name and workstation) before
    applying the UNIQUE constraint on actor.workstation_setting.  If found,
    raise a notice to the user to provide suggestions.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
index e9e8b0e79a..f6cc5b3ced 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
@@ -12,8 +12,41 @@ VALUES (
     )
 );
 
-ALTER TABLE actor.workstation_setting
+
+DO $SQL$
+BEGIN
+
+    PERFORM COUNT(*), workstation, name 
+    FROM actor.workstation_setting GROUP BY 2, 3 HAVING COUNT(*) > 1;
+
+    IF FOUND THEN
+
+        RAISE NOTICE $NOTICE$
+
+---
+The actor.workstation_setting table contains duplicate rows.  The duplicates 
+should be removed before applying a new UNIQUE constraint.  To find the rows, 
+execute the following SQL:
+
+SELECT COUNT(*), workstation, name FROM actor.workstation_setting 
+    GROUP BY 2, 3 HAVING COUNT(*) > 1;  
+    
+Once the duplicates are cleared, execute the following SQL: 
+
+ALTER TABLE actor.workstation_setting 
     ADD CONSTRAINT ws_once_per_key UNIQUE (workstation, name);
+---
+
+$NOTICE$;
+
+    ELSE
+
+        ALTER TABLE actor.workstation_setting
+            ADD CONSTRAINT ws_once_per_key UNIQUE (workstation, name);
+    END IF;
+
+END;
+$SQL$;
 
 COMMIT;
 

commit 12468d0e0212027d8772583d434760643dfdb04d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Oct 28 13:53:46 2019 -0400

    LP1830391 Angular Hatch enabled flag lookup repair
    
    Fix the Angular Hatch enabled setting lookup to pull the value from the
    workstation setting instead of localStorage.  This required shuffling a
    few things around to support the asynchronicity.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.html b/Open-ILS/src/eg2/src/app/share/print/print.component.html
index 823b5d8c18..9fb4debd4c 100644
--- a/Open-ILS/src/eg2/src/app/share/print/print.component.html
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.html
@@ -14,13 +14,14 @@ Error generating print content for template name="{{name}}" / id="{{id}}"
 <eg-string key='eg.print.template.error' [template]="notWorking"></eg-string>
 
 <div id='eg-print-container'>
+
   <!-- container for inline template compilation -->
   <ng-container *ngIf="template">
     <ng-container *ngTemplateOutlet="template; context:context">
     </ng-container>
   </ng-container>
-  <div id='eg-print-html-container'>
-  </div>
-<!-- container for pre-compiled HTML -->
+
+  <!-- container for pre-compiled HTML -->
+  <div id='eg-print-html-container'> </div>
 </div>
 
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
index 708ebf3fd6..6af06bd8f9 100644
--- a/Open-ILS/src/eg2/src/app/share/print/print.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
@@ -28,6 +28,9 @@ export class PrintComponent implements OnInit {
 
     printQueue: PrintRequest[];
 
+    // True if Hatch printing is enabled and we're able to talk to Hatch.
+    useHatchPrinting: boolean = null;
+
     constructor(
         private renderer: Renderer2,
         private elm: ElementRef,
@@ -50,6 +53,19 @@ export class PrintComponent implements OnInit {
             this.renderer.selectRootElement('#eg-print-html-container');
     }
 
+
+    // Returns promise of true if Hatch should be used for printing.
+    // To avoid race conditions, always check this inline before
+    // relaying print requests.
+    checkHatchEnabled(): Promise<boolean> {
+        if (this.useHatchPrinting !== null) {
+            return Promise.resolve(this.useHatchPrinting);
+        }
+
+        return this.serverStore.getItem('eg.hatch.enable.printing')
+            .then(use => this.useHatchPrinting = (use && this.hatch.connect()));
+    }
+
     handlePrintRequest(printReq: PrintRequest) {
 
         if (this.isPrinting) {
@@ -63,8 +79,7 @@ export class PrintComponent implements OnInit {
         this.applyTemplate(printReq).then(() => {
             // Give templates a chance to render before printing
             setTimeout(() => {
-                this.dispatchPrint(printReq);
-                this.reset();
+                this.dispatchPrint(printReq).then(_ => this.reset());
             });
         });
     }
@@ -123,17 +138,20 @@ export class PrintComponent implements OnInit {
 
         return promise.then(() => {
 
-            // Insert HTML into the browser DOM for in-browser printing.
-            if (printReq.text && !this.useHatch()) {
+            return this.checkHatchEnabled().then(enabled => {
 
-                if (printReq.contentType === 'text/plain') {
-                // Wrap text/plain content in pre's to prevent
-                // unintended html formatting.
-                    printReq.text = `<pre>${printReq.text}</pre>`;
-                }
+                // Insert HTML into the browser DOM for in-browser printing.
+                if (printReq.text && !enabled) {
 
-                this.htmlContainer.innerHTML = printReq.text;
-            }
+                    if (printReq.contentType === 'text/plain') {
+                    // Wrap text/plain content in pre's to prevent
+                    // unintended html formatting.
+                        printReq.text = `<pre>${printReq.text}</pre>`;
+                    }
+
+                    this.htmlContainer.innerHTML = printReq.text;
+                }
+            });
         });
     }
 
@@ -149,12 +167,17 @@ export class PrintComponent implements OnInit {
         }
     }
 
-    dispatchPrint(printReq: PrintRequest) {
+    dispatchPrint(printReq: PrintRequest): Promise<any> {
 
         if (!printReq.text) {
+
+            // Extract the print container div from our component markup.
+            const container =
+                this.elm.nativeElement.querySelector('#eg-print-container');
+
             // Sometimes the results come from an externally-parsed HTML
             // template, other times they come from an in-page template.
-            printReq.text = this.elm.nativeElement.innerHTML;
+            printReq.text = container.innerHTML;
         }
 
         // Retain a copy of each printed document in localStorage
@@ -166,17 +189,14 @@ export class PrintComponent implements OnInit {
             show_dialog: printReq.showDialog
         });
 
-        if (this.useHatch()) {
-            this.printViaHatch(printReq);
-        } else {
-            // Here the needed HTML is already in the page.
-            window.print();
-        }
-    }
-
-    useHatch(): boolean {
-        return this.store.getLocalItem('eg.hatch.enable.printing')
-            && this.hatch.connect();
+        return this.checkHatchEnabled().then(enabled => {
+            if (enabled) {
+                this.printViaHatch(printReq);
+            } else {
+                // Here the needed HTML is already in the page.
+                window.print();
+            }
+        });
     }
 
     printViaHatch(printReq: PrintRequest) {

commit 032dc3b1fbfa1ccf93238948dd73d54b6baadd1c
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Aug 5 10:44:55 2019 -0400

    LP1830391 Hatch core mod import/export repairs
    
    No need to import/export HatchService from the base common module since
    it's now a core service which is exported automatically to root.
    
    Includes minor lint repair.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
index ab41d5a155..56eddab1eb 100644
--- a/Open-ILS/src/eg2/src/app/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -14,7 +14,6 @@ They do not have to be added to the providers list.
 */
 
 // consider moving these to core...
-import {HatchService} from '@eg/share/print/hatch.service';
 import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service';
 import {PrintService} from '@eg/share/print/print.service';
 
@@ -81,7 +80,6 @@ export class EgCommonModule {
             ngModule: EgCommonModule,
             providers: [
                 HtmlToTxtService,
-                HatchService,
                 PrintService,
                 ToastService
             ]
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
index 91341de14d..708ebf3fd6 100644
--- a/Open-ILS/src/eg2/src/app/share/print/print.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
@@ -28,8 +28,6 @@ export class PrintComponent implements OnInit {
 
     printQueue: PrintRequest[];
 
-    useHatch: boolean;
-
     constructor(
         private renderer: Renderer2,
         private elm: ElementRef,
@@ -50,9 +48,6 @@ export class PrintComponent implements OnInit {
 
         this.htmlContainer =
             this.renderer.selectRootElement('#eg-print-html-container');
-
-        this.serverStore.getItem('eg.hatch.enable.printing')
-            .then(use => this.useHatch = use);
     }
 
     handlePrintRequest(printReq: PrintRequest) {
@@ -171,7 +166,7 @@ export class PrintComponent implements OnInit {
             show_dialog: printReq.showDialog
         });
 
-        if (this.useHatch) {
+        if (this.useHatch()) {
             this.printViaHatch(printReq);
         } else {
             // Here the needed HTML is already in the page.
@@ -179,6 +174,11 @@ export class PrintComponent implements OnInit {
         }
     }
 
+    useHatch(): boolean {
+        return this.store.getLocalItem('eg.hatch.enable.printing')
+            && this.hatch.connect();
+    }
+
     printViaHatch(printReq: PrintRequest) {
         if (!printReq.contentType) {
             printReq.contentType = 'text/html';

commit eed52b506f4cdf3a607f12b2f122ed9844710103
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu May 23 18:02:41 2019 -0400

    LP1830391 Workstation settings unique constraint
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index 0572543768..33ae6282a1 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -1108,7 +1108,8 @@ CREATE TABLE actor.workstation_setting (
                        ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     name        TEXT   NOT NULL REFERENCES config.workstation_setting_type (name) 
                        ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
-    value       JSON   NOT NULL
+    value       JSON   NOT NULL,
+    CONSTRAINT  ws_once_per_key UNIQUE (workstation, name)
 );
 
 CREATE INDEX actor_workstation_setting_workstation_idx 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
index 315c85b0d2..e9e8b0e79a 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
@@ -12,6 +12,8 @@ VALUES (
     )
 );
 
+ALTER TABLE actor.workstation_setting
+    ADD CONSTRAINT ws_once_per_key UNIQUE (workstation, name);
 
 COMMIT;
 

commit e01b4c822e317ca4da0ca74336d480dc5a57a3c5
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Apr 12 15:47:24 2019 -0400

    LP1824391 Hatch File Writer release notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/docs/RELEASE_NOTES_NEXT/Client/hatch-file-writer.adoc b/docs/RELEASE_NOTES_NEXT/Client/hatch-file-writer.adoc
new file mode 100644
index 0000000000..4fb2ec88e6
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Client/hatch-file-writer.adoc
@@ -0,0 +1,11 @@
+Hatch File Writer Print Option
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Adds a new stock print option in the Hatch printer configuration interface
+called Hatch File Writer (translatable).  When selected, any print content
+that is delivered to this printer is translated into text where necessary
+and written to a file in the Hatch profile directory.
+
+The name of the file written is based on the print context: 
+"receipt.<context>.txt".  For example, 'receipt.label.txt'.
+

commit 520139cb60d90dc5fea8529ec637a846a83a7790
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Apr 15 11:34:21 2019 -0400

    LP1824391 Hatch print-to-file Angular edition
    
    Adds support for the Angular hatch service for sending 'bare' text/plain
    print-to-file requests.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/eg2/src/app/app.component.ts b/Open-ILS/src/eg2/src/app/app.component.ts
index c70938ff99..a297f2285d 100644
--- a/Open-ILS/src/eg2/src/app/app.component.ts
+++ b/Open-ILS/src/eg2/src/app/app.component.ts
@@ -4,7 +4,7 @@ import {DialogComponent} from '@eg/share/dialog/dialog.component';
 
 @Component({
   selector: 'eg-root',
-  template: '<router-outlet></router-outlet><eg-print></eg-print>'
+  template: '<router-outlet></router-outlet>'
 })
 
 export class BaseComponent {
diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
index d61eeabb3a..ab41d5a155 100644
--- a/Open-ILS/src/eg2/src/app/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -15,6 +15,7 @@ They do not have to be added to the providers list.
 
 // consider moving these to core...
 import {HatchService} from '@eg/share/print/hatch.service';
+import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service';
 import {PrintService} from '@eg/share/print/print.service';
 
 // Globally available components
@@ -79,6 +80,7 @@ export class EgCommonModule {
         return {
             ngModule: EgCommonModule,
             providers: [
+                HtmlToTxtService,
                 HatchService,
                 PrintService,
                 ToastService
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
index 0d780f3637..91341de14d 100644
--- a/Open-ILS/src/eg2/src/app/share/print/print.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
@@ -5,6 +5,8 @@ import {ServerStoreService} from '@eg/core/server-store.service';
 import {HatchService, HatchMessage} from '@eg/core/hatch.service';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {StringService} from '@eg/share/string/string.service';
+import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service';
+const HATCH_FILE_WRITER_PRINTER = 'hatch_file_writer';
 
 @Component({
     selector: 'eg-print',
@@ -33,6 +35,7 @@ export class PrintComponent implements OnInit {
         private elm: ElementRef,
         private store: StoreService,
         private serverStore: ServerStoreService,
+        private h2txt: HtmlToTxtService,
         private hatch: HatchService,
         private toast: ToastService,
         private strings: StringService,
@@ -177,6 +180,9 @@ export class PrintComponent implements OnInit {
     }
 
     printViaHatch(printReq: PrintRequest) {
+        if (!printReq.contentType) {
+            printReq.contentType = 'text/html';
+        }
 
         // Send a full HTML document to Hatch
         let html = printReq.text;
@@ -187,13 +193,30 @@ export class PrintComponent implements OnInit {
         this.serverStore.getItem(`eg.print.config.${printReq.printContext}`)
         .then(config => {
 
-            const msg = new HatchMessage({
-                action: 'print',
-                content: html,
-                settings: config || {},
-                contentType: 'text/html',
-                showDialog: printReq.showDialog
-            });
+            let msg: HatchMessage;
+
+            if (config && config.printer === HATCH_FILE_WRITER_PRINTER) {
+
+                const text = printReq.contentType === 'text/plain' ?
+                    html : this.h2txt.htmlToTxt(html);
+
+                msg = new HatchMessage({
+                    action: 'set',
+                    key: `receipt.${printReq.printContext}.txt`,
+                    content: text,
+                    bare: true
+                });
+
+            } else {
+
+                msg = new HatchMessage({
+                    action: 'print',
+                    content: html,
+                    settings: config || {},
+                    contentType: 'text/html',
+                    showDialog: printReq.showDialog
+                });
+            }
 
             this.hatch.sendRequest(msg).then(
                 ok  => console.debug('Print request succeeded'),
diff --git a/Open-ILS/src/eg2/src/app/share/util/htmltotxt.service.ts b/Open-ILS/src/eg2/src/app/share/util/htmltotxt.service.ts
new file mode 100644
index 0000000000..e265b3d2fa
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/util/htmltotxt.service.ts
@@ -0,0 +1,75 @@
+import {Injectable} from '@angular/core';
+
+const ENTITY_REGEX = /&[^\s]+;/;
+
+/**
+ * Translates HTML text into plain text.
+ */
+
+ at Injectable()
+export class HtmlToTxtService {
+
+   unEscapeHtml(text: string): string {
+        text = text.replace(/&/g, '&');
+        text = text.replace(/"/g, '"');
+        text = text.replace(/ /g, ' ');
+        text = text.replace(/</g, '<');
+        text = text.replace(/>/g, '>');
+        return text;
+    }
+
+    // https://stackoverflow.com/questions/7394748
+    entityToChars(text: string): string {
+        if (text && text.match(ENTITY_REGEX)) {
+            const node = document.createElement('textarea');
+            node.innerHTML = text;
+            return node.value;
+        }
+        return text;
+    }
+
+    // Translate an HTML string into plain text.
+    // Removes HTML elements.
+    // Replaces <li> with "*"
+    // Replaces HTML entities with their character equivalent.
+    htmlToTxt(html: string): string {
+        if (!html || html === '') {
+            return '';
+        }
+
+        // First remove multi-line comments.
+        html = html.replace(/<!--(.*?)-->/gs, '');
+
+        const lines = html.split(/\n/);
+        const newLines = [];
+
+        lines.forEach(line => {
+
+            if (!line) {
+                newLines.push(line);
+                return;
+            }
+
+            line = this.unEscapeHtml(line);
+            line = this.entityToChars(line);
+
+            line = line.replace(/<head.*?>.*?<\/head>/gi, '');
+            line = line.replace(/<br.*?>/gi, '\r\n');
+            line = line.replace(/<table.*?>/gi, '');
+            line = line.replace(/<\/tr>/gi, '\r\n'); // end of row
+            line = line.replace(/<\/td>/gi, ' '); // end of cell
+            line = line.replace(/<\/th>/gi, ' '); // end of th
+            line = line.replace(/<tr.*?>/gi, '');
+            line = line.replace(/<hr.*?>/gi, '\r\n');
+            line = line.replace(/<p.*?>/gi, '');
+            line = line.replace(/<block.*?>/gi, '');
+            line = line.replace(/<li.*?>/gi, ' * ');
+            line = line.replace(/<.+?>/gi, '');
+
+            if (line) { newLines.push(line); }
+        });
+
+        return newLines.join('\n');
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
index ee46d1eb54..ed1b5813b4 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -403,3 +403,4 @@
     </div>
   </div>
 </div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index 3557532d4f..0ee8048085 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -22,6 +22,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import * as Moment from 'moment-timezone';
 import {SampleDataService} from '@eg/share/util/sample-data.service';
+import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service';
 
 @Component({
   templateUrl: 'sandbox.component.html',
@@ -114,7 +115,8 @@ export class SandboxComponent implements OnInit {
         private toast: ToastService,
         private format: FormatService,
         private printer: PrintService,
-        private samples: SampleDataService
+        private samples: SampleDataService,
+        private h2txt: HtmlToTxtService
     ) {
         // BroadcastChannel is not yet defined in PhantomJS and elsewhere
         this.sbChannel = (typeof BroadcastChannel === 'undefined') ?
@@ -271,6 +273,9 @@ export class SandboxComponent implements OnInit {
                     }
             } )
         });
+
+        const str = 'C&#xe9;sar & Me';
+        console.log(this.h2txt.htmlToTxt(str));
     }
 
     sbChannelHandler = msg => {
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.html b/Open-ILS/src/eg2/src/app/staff/staff.component.html
index 9002fa8e1f..6cc1bc02fd 100644
--- a/Open-ILS/src/eg2/src/app/staff/staff.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/staff.component.html
@@ -21,3 +21,7 @@
 
 <!-- global toast alerts -->
 <eg-toast></eg-toast>
+
+<!-- global print handler component -->
+<eg-print></eg-print>
+

commit 436d7a1be877684b3ac43b09ac5cb52a6dbf87b3
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Apr 12 12:52:09 2019 -0400

    LP1824391 Hatch File Writer print support
    
    Adds support for translating receipt data to plain text and writing the
    output to a file instead of sending it to a printer.
    
    Adds a new stock printer named "Hatch File Writer" to the print
    configuration interface.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_print_config.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_print_config.tt2
index 043a446305..4e4063201f 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_print_config.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_print_config.tt2
@@ -70,7 +70,12 @@
                   <ul uib-dropdown-menu>
                     <li ng-repeat="printer in printers">
                       <a href='' ng-click="setPrinter(printer.name)">
-                        {{printer.name}}
+                        <span ng-if="printer.name == 'hatch_file_writer'">
+                          [% l('Hatch File Writer') %]
+                        </span>
+                        <span ng-if="printer.name != 'hatch_file_writer'">
+                          {{printer.name}}
+                        </span>
                       </a>
                     </li>
                   </ul>
@@ -81,8 +86,10 @@
                 <input ng-if="printers[0] && !printConfig[context]" type="text" 
                   class="form-control" disabled="disabled"
                   value="[% l('No Printer Selected') %]">
-                <input ng-if="printConfig[context].printer" type="text" 
-                  class="form-control" disabled="disabled"
+                <input ng-if="useFileWriter()" type="text" disabled="disabled"
+                  class="form-control" value="[% l('Hatch File Writer') %]"/>
+                <input ng-if="printConfig[context].printer && !useFileWriter()" 
+                  type="text" class="form-control" disabled="disabled"
                   value="{{printConfig[context].printer}}">
               </div><!-- /input-group -->
             </div><!-- col -->
@@ -100,7 +107,17 @@
             </div>
           </div><!-- row -->
 
-          <div class="row" ng-hide="isTestView || !hatchIsOpen()"> 
+          <div class="pad-vert" 
+            ng-show="!isTestView && hatchIsOpen() && useFileWriter()">
+            <div class="alert alert-info">
+              [% |l %]Hatch File Writer translates print output to plain text 
+and writes the content to a text file in the Hatch profile directory.  No
+additional settings are required.[% END %]
+            </div>
+          </div>
+
+          <div class="row" 
+            ng-show="!isTestView && hatchIsOpen() && !useFileWriter()"
             <div class="col-md-10">
               <div class="row">
                 <div class="col-md-1"></div>
@@ -136,7 +153,6 @@
                     '{{printerOptions.defaultPaperSource}}') %]</span>
                 </div>
               </div>
-
               <div class="row">
                 <div class="col-md-1"></div>
                 <div class="col-md-2">
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index b8d9a95100..9dfbc9c873 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -279,8 +279,12 @@ function($scope , egCore) {
     }
 
     function loadPrinterOptions(name) {
-        egCore.hatch.getPrinterOptions(name).then(
-            function(options) {$scope.printerOptions = options});
+        if (name == 'hatch_file_writer') {
+            $scope.printerOptions = {};
+        } else {
+            egCore.hatch.getPrinterOptions(name).then(
+                function(options) {$scope.printerOptions = options});
+        }
     }
 
     $scope.setPrinter = function(name) {
@@ -311,6 +315,13 @@ function($scope , egCore) {
         }
     }
 
+    $scope.useFileWriter = function() {
+        return (
+            $scope.printConfig[$scope.context] &&
+            $scope.printConfig[$scope.context].printer == 'hatch_file_writer'
+        );
+    }
+
     // Load startup data....
     // Don't bother talking to Hatch if it's not there.
     if (!egCore.hatch.hatchAvailable) return;
@@ -320,6 +331,12 @@ function($scope , egCore) {
     .then(function(printers) { 
         $scope.printers = printers;
 
+        printers.push({
+            // We need a static name for saving configs.
+            // Human-friendly label is set in the template.
+            name: 'hatch_file_writer' 
+        });
+
         var def = $scope.getPrinterByAttr('is-default', true);
         if (!def && printers.length) def = printers[0];
 
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index 6b565201b6..1e907c5c13 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -162,6 +162,13 @@ angular.module('egCoreMod')
 
         return service.getPrintConfig(context).then(
             function(config) {
+                if (config.printer == 'hatch_file_writer') {
+                    if (contentType == 'text/html') {
+                        content = service.html2txt(content);
+                    }
+                    return service.setRemoteItem(
+                        'receipt.' + context + '.txt', content, true);
+                } 
                 // print configuration retrieved; print
                 return service.attemptHatchDelivery({
                     action : 'print',
@@ -704,12 +711,15 @@ angular.module('egCoreMod')
 
 
     // set the value for a stored or new item
-    service.setRemoteItem = function(key, value) {
+    // When "bare" is true, the value will not be JSON-encoded
+    // on the file system.
+    service.setRemoteItem = function(key, value, bare) {
         service.keyCache[key] = value;
         return service.attemptHatchDelivery({
             key : key, 
             content : value, 
             action : 'set',
+            bare: bare
         });
     }
 
@@ -1002,6 +1012,60 @@ angular.module('egCoreMod')
         return $q.when(null);
     }
 
+    // COPIED FROM XUL util/text.js
+    service.reverse_preserve_string_in_html = function( text ) {
+        text = text.replace(/&/g, '&');
+        text = text.replace(/"/g, '"');
+        text = text.replace(/'/g, "'");
+        text = text.replace(/ /g, ' ');
+        text = text.replace(/</g, '<');
+        text = text.replace(/>/g, '>');
+        return text;
+    }
+
+    // COPIED FROM XUL util/print.js
+    service.html2txt = function(html) {
+        var lines = html.split(/\n/);
+        var new_lines = [];
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i];
+            if (!line) {
+                new_lines.push(line);
+                continue;
+            }
+
+            // This undoes the util.text.preserve_string_in_html 
+            // call that spine_label.js does
+            line = service.reverse_preserve_string_in_html(line);
+
+            // This looks for @hex attributes containing 2-digit hex 
+            // codes, and converts them into real characters
+            line = line.replace(/(<.+?)hex=['"](.+?)['"](.*?>)/gi, 
+                function(str,p1,p2,p3,offset,s) {
+
+                var raw_chars = '';
+                var hex_chars = p2.match(/[0-9,a-f,A-F][0-9,a-f,A-F]/g);
+                for (var j = 0; j < hex_chars.length; j++) {
+                    raw_chars += String.fromCharCode( parseInt(hex_chars[j],16) );
+                }
+                return p1 + p3 + raw_chars;
+            });
+
+            line = line.replace(/<head.*?>.*?<\/head>/gi, '');
+            line = line.replace(/<br.*?>/gi,'\r\n');
+            line = line.replace(/<table.*?>/gi,'');
+            line = line.replace(/<tr.*?>/gi,'');
+            line = line.replace(/<hr.*?>/gi,'\r\n');
+            line = line.replace(/<p.*?>/gi,'');
+            line = line.replace(/<block.*?>/gi,'');
+            line = line.replace(/<li.*?>/gi,' * ');
+            line = line.replace(/<.+?>/gi,'');
+            if (line) { new_lines.push(line); }
+        }
+
+        return new_lines.join('\n');
+    }
+
     // The only requirement for opening Hatch is that the DOM be loaded.
     // Open the connection now so its state will be immediately available.
     service.openHatch();
diff --git a/Open-ILS/web/js/ui/default/staff/services/print.js b/Open-ILS/web/js/ui/default/staff/services/print.js
index d12a6cd84f..ae1108241e 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -106,16 +106,18 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
 
         if (args.content_type == 'text/html') {
             promise = service.ingest_print_content(
-                args.content_type, args.content, args.scope);
+                args.content_type, args.content, args.scope
+            ).then(function(html) {
+                // For good measure, wrap the compiled HTML in container tags.
+                return "<html><body>" + html + "</body></html>";
+            });
         } else {
             // text content requires no compilation for remote printing.
             promise = $q.when(args.content);
         }
 
-        return promise.then(function(html) {
-            // For good measure, wrap the compiled HTML in container tags.
-            html = "<html><body>" + html + "</body></html>";
-            service.last_print.content = html;
+        return promise.then(function(content) {
+            service.last_print.content = content;
             service.last_print.context = args.context || 'default';
             service.last_print.content_type = args.content_type;
             service.last_print.show_dialog = args.show_dialog;

commit 085ebb527ee2eff750e273dcb30f86ef4825966d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Apr 22 14:02:05 2019 -0700

    LP1825891 Use Hatch 'hostname' for workstation reg.
    
    Populate the workstation name with the hostname of the PC when
    registring a new workstation for Hatch-enabled workstations.
    
    Note the code gracefully returns null if Hatch returns a 404 (not found)
    or Hatch is not active.
    
    Updates the AngJS and Angular workstation admin pages to load the
    hostname value when possible.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/eg2/src/app/core/hatch.service.ts b/Open-ILS/src/eg2/src/app/core/hatch.service.ts
index b61ee176e4..45ba5f4cd7 100644
--- a/Open-ILS/src/eg2/src/app/core/hatch.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/hatch.service.ts
@@ -106,6 +106,15 @@ export class HatchService {
         }
     }
 
+    // Returns promise of null if Hatch is not available.
+    hostname(): Promise<string> {
+        const msg = new HatchMessage({action: 'hostname'});
+        return this.sendRequest(msg).then(
+            (m: HatchMessage) => m.response,
+            (err) => null
+        );
+    }
+
     getItem(key: string): Promise<any> {
         const msg = new HatchMessage({action: 'get', key: key});
         return this.sendRequest(msg).then((m: HatchMessage) => m.response);
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
index bd500d8420..ab27cc2a87 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
@@ -8,6 +8,7 @@ import {AuthService} from '@eg/core/auth.service';
 import {OrgService} from '@eg/core/org.service';
 import {EventService} from '@eg/core/event.service';
 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {HatchService} from '@eg/core/hatch.service';
 
 // Slim version of the WS that's stored in the cache.
 interface Workstation {
@@ -46,6 +47,7 @@ export class WorkstationsComponent implements OnInit {
         private store: StoreService,
         private auth: AuthService,
         private org: OrgService,
+        private hatch: HatchService,
         private perm: PermService
     ) {}
 
@@ -54,9 +56,14 @@ export class WorkstationsComponent implements OnInit {
 
         .then(wsList => {
             this.workstations = wsList || [];
-            return this.store.getDefaultWorkstation();
 
-        }).then(def => {
+            // Populate the new WS name field with the hostname when available.
+            return this.setNewName();
+
+        }).then(
+            ok => this.store.getDefaultWorkstation()
+
+        ).then(def => {
             this.defaultName = def;
             this.selectedName = this.auth.workstation() || this.defaultName;
             const rm = this.route.snapshot.paramMap.get('remove');
@@ -101,11 +108,10 @@ export class WorkstationsComponent implements OnInit {
 
         this.workstations = this.workstations.filter(w => w.name !== name);
         this.store.setWorkstations(this.workstations);
+    }
 
-        if (this.defaultName === name) {
-            this.defaultName = null;
-            this.store.removeWorkstations();
-        }
+    setNewName() {
+        this.hatch.hostname().then(name => this.newName = name || '');
     }
 
     canDeleteSelected(): boolean {
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index 70f87b9f8e..b8d9a95100 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -828,6 +828,10 @@ function($scope , $q , $window , $location , egCore , egAlertDialog , workstatio
 
     console.log('set context org to ' + $scope.contextOrg);
 
+    egCore.hatch.hostname().then(function(name) {
+        $scope.newWSName = name || '';
+    });
+
     // fetch workstation reg perms
     egCore.perm.hasPermAt('REGISTER_WORKSTATION', true)
     .then(function(orgList) { 
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index c7e2f39485..6b565201b6 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -989,6 +989,19 @@ angular.module('egCoreMod')
         return deferred.promise;
     }
 
+    service.hostname = function() {
+        if (service.hatchAvailable) {
+            return service.attemptHatchDelivery({action : 'hostname'})
+            .then(
+                function(name) { return name; },
+                // Gracefully handle case where Hatch has not yet been 
+                // updated to include the hostname command.
+                function() {return null}
+            );
+        } 
+        return $q.when(null);
+    }
+
     // The only requirement for opening Hatch is that the DOM be loaded.
     // Open the connection now so its state will be immediately available.
     service.openHatch();

commit 63438e048405301e613d4287c79a49f296fd8100
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Apr 23 07:57:31 2019 -0700

    LP1825896 Store workstations in Hatch when available
    
    When Hatch is enabled, use Hatch for storing workstation registration
    information.
    
    If workstations are found in localStorage, they are merged into the
    collection of workstations stored in hatch and removed from
    localStorage.
    
    Include DB udpate to add workstation setting 'eg.hatch.enable.printing'
    so that it may live on the server.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jason Boyer <JBoyer at eoli.info>

diff --git a/Open-ILS/src/eg2/src/app/core/auth.service.ts b/Open-ILS/src/eg2/src/app/core/auth.service.ts
index dad2acdb90..a173d6304a 100644
--- a/Open-ILS/src/eg2/src/app/core/auth.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/auth.service.ts
@@ -286,22 +286,22 @@ export class AuthService {
         }
 
         return new Promise((resolve, reject) => {
-            const workstations =
-                this.store.getLocalItem('eg.workstation.all');
+            return this.store.getWorkstations().then(workstations => {
 
-            if (workstations) {
-                const ws = workstations.filter(
-                    w => Number(w.id) === Number(this.user().wsid()))[0];
+                if (workstations) {
+                    const ws = workstations.filter(
+                        w => Number(w.id) === Number(this.user().wsid()))[0];
 
-                if (ws) {
-                    this.activeUser.workstation = ws.name;
-                    this.workstationState = AuthWsState.VALID;
-                    return resolve();
+                    if (ws) {
+                        this.activeUser.workstation = ws.name;
+                        this.workstationState = AuthWsState.VALID;
+                        return resolve();
+                    }
                 }
-            }
 
-            this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
-            reject();
+                this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
+                reject();
+            });
         });
     }
 
diff --git a/Open-ILS/src/eg2/src/app/share/print/hatch.service.ts b/Open-ILS/src/eg2/src/app/core/hatch.service.ts
similarity index 81%
rename from Open-ILS/src/eg2/src/app/share/print/hatch.service.ts
rename to Open-ILS/src/eg2/src/app/core/hatch.service.ts
index bd087b7747..b61ee176e4 100644
--- a/Open-ILS/src/eg2/src/app/share/print/hatch.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/hatch.service.ts
@@ -22,7 +22,7 @@ export class HatchMessage {
     }
 }
 
- at Injectable()
+ at Injectable({providedIn: 'root'})
 export class HatchService {
 
     isAvailable: boolean;
@@ -44,7 +44,7 @@ export class HatchService {
         // When the Hatch extension loads, it tacks an attribute onto
         // the top-level documentElement to indicate it's available.
         if (!window.document.documentElement.getAttribute('hatch-is-open')) {
-            console.warn('Could not connect to Hatch');
+            console.debug('Could not connect to Hatch');
             return this.isAvailable = false;
         }
 
@@ -105,5 +105,20 @@ export class HatchService {
             msg.rejector(msg);
         }
     }
+
+    getItem(key: string): Promise<any> {
+        const msg = new HatchMessage({action: 'get', key: key});
+        return this.sendRequest(msg).then((m: HatchMessage) => m.response);
+    }
+
+    setItem(key: string, val: any): Promise<any> {
+        const msg = new HatchMessage({action: 'set', key: key, content: val});
+        return this.sendRequest(msg).then((m: HatchMessage) => m.response);
+    }
+
+    removeItem(key: string): Promise<any> {
+        const msg = new HatchMessage({action: 'remove', key: key});
+        return this.sendRequest(msg).then((m: HatchMessage) => m.response);
+    }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/core/store.service.ts b/Open-ILS/src/eg2/src/app/core/store.service.ts
index 46dd6214fe..7b239d2ad2 100644
--- a/Open-ILS/src/eg2/src/app/core/store.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/store.service.ts
@@ -11,6 +11,10 @@
  */
 import {Injectable} from '@angular/core';
 import {CookieService} from 'ngx-cookie';
+import {HatchService} from './hatch.service';
+
+const WS_ALL_KEY = 'eg.workstation.all';
+const WS_DEF_KEY = 'eg.workstation.default';
 
 @Injectable({providedIn: 'root'})
 export class StoreService {
@@ -30,7 +34,8 @@ export class StoreService {
     ];
 
     constructor(
-        private cookieService: CookieService) {
+        private cookieService: CookieService,
+        private hatch: HatchService) {
     }
 
     private parseJson(valJson: string): any {
@@ -74,6 +79,31 @@ export class StoreService {
             {path : this.loginSessionBasePath, secure: true});
     }
 
+    setWorkstations(val: any, isJson?: boolean): Promise<any> {
+        if (this.hatch.isAvailable) {
+            return this.hatch.setItem(WS_ALL_KEY, val).then(
+                ok => {
+                    // When clearing workstations, remove the default.
+                    if (!val || val.length === 0) {
+                        return this.hatch.removeItem(WS_DEF_KEY);
+                    }
+                }
+            );
+        } else {
+            return Promise.resolve(
+                this.setLocalItem(WS_ALL_KEY, val, isJson));
+        }
+    }
+
+    setDefaultWorkstation(val: string, isJson?: boolean): Promise<any> {
+        if (this.hatch.isAvailable) {
+            return this.hatch.setItem(WS_DEF_KEY, val);
+        } else {
+            return Promise.resolve(
+                this.setLocalItem(WS_DEF_KEY, val, isJson));
+        }
+    }
+
     getLocalItem(key: string): any {
         return this.parseJson(window.localStorage.getItem(key));
     }
@@ -86,6 +116,79 @@ export class StoreService {
         return this.parseJson(this.cookieService.get(key));
     }
 
+    getWorkstations(): Promise<any> {
+        if (this.hatch.isAvailable) {
+            return this.mergeWorkstations().then(ok => {
+                this.removeLocalItem(WS_ALL_KEY);
+                return this.hatch.getItem(WS_ALL_KEY);
+            });
+        } else {
+            return Promise.resolve(this.getLocalItem(WS_ALL_KEY));
+        }
+    }
+
+    // See if any workstatoins are stored in local storage.  If so, also
+    // see if we have any stored in Hatch.  If both, merged workstations
+    // from localStorage in Hatch storage, skipping any whose name
+    // collide with a workstation in Hatch.  If none exist in Hatch,
+    // copy the localStorage workstations over wholesale.
+    mergeWorkstations(): Promise<any> {
+        const existing = this.getLocalItem(WS_ALL_KEY);
+
+        if (!existing || existing.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.hatch.getItem(WS_ALL_KEY).then(inHatch => {
+
+            if (!inHatch || inHatch.length === 0) {
+                // Nothing to merge, copy the data over directly
+                return this.hatch.setItem('eg.workstation.all', existing);
+            }
+
+            const addMe: any = [];
+            existing.forEach(ws => {
+                const match = inHatch.filter(w => w.name === ws.name)[0];
+                if (!match) {
+                    console.log(
+                        'Migrating workstation from local storage to hatch: '
+                        + ws.name
+                    );
+                    addMe.push(ws);
+                }
+            });
+            inHatch = inHatch.concat(addMe);
+            return this.hatch.setItem(WS_ALL_KEY, inHatch);
+        });
+    }
+
+    getDefaultWorkstation(): Promise<any> {
+        if (this.hatch.isAvailable) {
+            return this.hatch.getItem(WS_DEF_KEY).then(name => {
+                if (name) {
+                    // We have a default in Hatch, remove any lingering
+                    // value from localStorage.
+                    this.removeLocalItem(WS_DEF_KEY);
+                    return name;
+                } else {
+                    // Nothing in Hatch, see if we have a localStorage
+                    // value to migrate to Hatch
+                    name = this.getLocalItem(WS_DEF_KEY);
+                    if (name) {
+                        console.debug(
+                            'Migrating default workstation to Hatch ' + name);
+                        return this.hatch.setItem(WS_DEF_KEY, name)
+                        .then(ok => name);
+                    } else {
+                        return null;
+                    }
+                }
+            });
+        } else {
+            return Promise.resolve(this.getLocalItem(WS_DEF_KEY));
+        }
+    }
+
     removeLocalItem(key: string): void {
         window.localStorage.removeItem(key);
     }
@@ -98,6 +201,16 @@ export class StoreService {
         this.cookieService.remove(key, {path : this.loginSessionBasePath});
     }
 
+    removeDefaultWorkstation(val: string, isJson?: boolean): Promise<any> {
+        if (this.hatch.isAvailable) {
+            return this.hatch.removeItem(WS_DEF_KEY);
+        } else {
+            return Promise.resolve(
+                this.removeLocalItem(WS_DEF_KEY));
+        }
+    }
+
+
     clearLoginSessionItems(): void {
         this.loginSessionKeys.forEach(
             key => this.removeLoginSessionItem(key)
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
index 20c056721d..0d780f3637 100644
--- a/Open-ILS/src/eg2/src/app/share/print/print.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit, TemplateRef, ElementRef, Renderer2} from '@angular/co
 import {PrintService, PrintRequest} from './print.service';
 import {StoreService} from '@eg/core/store.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
-import {HatchService, HatchMessage} from './hatch.service';
+import {HatchService, HatchMessage} from '@eg/core/hatch.service';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {StringService} from '@eg/share/string/string.service';
 
@@ -26,6 +26,8 @@ export class PrintComponent implements OnInit {
 
     printQueue: PrintRequest[];
 
+    useHatch: boolean;
+
     constructor(
         private renderer: Renderer2,
         private elm: ElementRef,
@@ -45,6 +47,9 @@ export class PrintComponent implements OnInit {
 
         this.htmlContainer =
             this.renderer.selectRootElement('#eg-print-html-container');
+
+        this.serverStore.getItem('eg.hatch.enable.printing')
+            .then(use => this.useHatch = use);
     }
 
     handlePrintRequest(printReq: PrintRequest) {
@@ -163,7 +168,7 @@ export class PrintComponent implements OnInit {
             show_dialog: printReq.showDialog
         });
 
-        if (this.useHatch()) {
+        if (this.useHatch) {
             this.printViaHatch(printReq);
         } else {
             // Here the needed HTML is already in the page.
@@ -171,11 +176,6 @@ export class PrintComponent implements OnInit {
         }
     }
 
-    useHatch(): boolean {
-        return this.store.getLocalItem('eg.hatch.enable.printing')
-            && this.hatch.connect();
-    }
-
     printViaHatch(printReq: PrintRequest) {
 
         // Send a full HTML document to Hatch
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
index 5ce77d42c1..bd500d8420 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
@@ -50,13 +50,18 @@ export class WorkstationsComponent implements OnInit {
     ) {}
 
     ngOnInit() {
-        this.workstations = this.store.getLocalItem('eg.workstation.all') || [];
-        this.defaultName = this.store.getLocalItem('eg.workstation.default');
-        this.selectedName = this.auth.workstation() || this.defaultName;
-        const rm = this.route.snapshot.paramMap.get('remove');
-        if (rm) {
-            this.removeSelected(this.removeWorkstation = rm);
-        }
+        this.store.getWorkstations()
+
+        .then(wsList => {
+            this.workstations = wsList || [];
+            return this.store.getDefaultWorkstation();
+
+        }).then(def => {
+            this.defaultName = def;
+            this.selectedName = this.auth.workstation() || this.defaultName;
+            const rm = this.route.snapshot.paramMap.get('remove');
+            if (rm) { this.removeSelected(this.removeWorkstation = rm); }
+        });
 
         // TODO: use the org selector limitPerm option
         this.perm.hasWorkPermAt(['REGISTER_WORKSTATION'], true)
@@ -85,7 +90,7 @@ export class WorkstationsComponent implements OnInit {
     setDefault(): void {
       if (this.selected()) {
             this.defaultName = this.selected().name;
-            this.store.setLocalItem('eg.workstation.default', this.defaultName);
+            this.store.setDefaultWorkstation(this.defaultName);
         }
     }
 
@@ -95,11 +100,11 @@ export class WorkstationsComponent implements OnInit {
         }
 
         this.workstations = this.workstations.filter(w => w.name !== name);
-        this.store.setLocalItem('eg.workstation.all', this.workstations);
+        this.store.setWorkstations(this.workstations);
 
         if (this.defaultName === name) {
             this.defaultName = null;
-            this.store.removeLocalItem('eg.workstation.default');
+            this.store.removeWorkstations();
         }
     }
 
@@ -170,7 +175,7 @@ export class WorkstationsComponent implements OnInit {
         };
 
         this.workstations.push(ws);
-        this.store.setLocalItem('eg.workstation.all', this.workstations);
+        this.store.setWorkstations(this.workstations);
         this.newName = '';
         // when registering our first workstation, mark it as the
         // default and show it as selected in the ws selector.
diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.ts b/Open-ILS/src/eg2/src/app/staff/login.component.ts
index de889f9fb2..2f2b9312e7 100644
--- a/Open-ILS/src/eg2/src/app/staff/login.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/login.component.ts
@@ -36,10 +36,14 @@ export class StaffLoginComponent implements OnInit {
         // Focus username
         this.renderer.selectRootElement('#username').focus();
 
-        this.workstations = this.store.getLocalItem('eg.workstation.all');
-        this.args.workstation =
-            this.store.getLocalItem('eg.workstation.default');
-        this.applyWorkstation();
+        this.store.getWorkstations()
+        .then(wsList => {
+            this.workstations = wsList;
+            return this.store.getDefaultWorkstation();
+        }).then(def => {
+            this.args.workstation = def;
+            this.applyWorkstation();
+        });
     }
 
     applyWorkstation() {
diff --git a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
index 94b469ee67..853bf402f1 100644
--- a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
+++ b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
@@ -9,6 +9,7 @@ import {AuthService, AuthWsState} from '@eg/core/auth.service';
 import {PermService} from '@eg/core/perm.service';
 import {OrgService} from '@eg/core/org.service';
 import {FormatService} from '@eg/core/format.service';
+import {HatchService} from '@eg/core/hatch.service';
 
 const LOGIN_PATH = '/staff/login';
 const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
@@ -26,6 +27,7 @@ export class StaffResolver implements Resolve<Observable<any>> {
         private router: Router,
         private route: ActivatedRoute,
         private ngLocation: Location,
+        private hatch: HatchService,
         private store: StoreService,
         private org: OrgService,
         private net: NetService,
@@ -38,6 +40,8 @@ export class StaffResolver implements Resolve<Observable<any>> {
         route: ActivatedRouteSnapshot,
         state: RouterStateSnapshot): Observable<any> {
 
+        this.hatch.connect();
+
         // Staff cookies stay in /$base/staff/
         // NOTE: storing session data at '/' so it can be shared by
         // Angularjs apps.
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 04ca260c36..37d7ebb9ec 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -20288,3 +20288,14 @@ VALUES (
     'Grid Config: circ.patron.xact_details_details_payments',
     'cwst', 'label')
 );
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.hatch.enable.printing', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.hatch.enable.printing',
+        'Use Hatch for printing',
+        'cwst', 'label'
+    )
+);
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
new file mode 100644
index 0000000000..315c85b0d2
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hatch-enable-print.sql
@@ -0,0 +1,18 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.hatch.enable.printing', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.hatch.enable.printing',
+        'Use Hatch for printing',
+        'cwst', 'label'
+    )
+);
+
+
+COMMIT;
+
+
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index d863844e3d..70f87b9f8e 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -72,16 +72,16 @@ function($q , $timeout , $location , egCore , egConfirmDialog) {
     var service = {};
 
     service.get_all = function() {
-        return egCore.hatch.getItem('eg.workstation.all')
+        return egCore.hatch.getWorkstations()
         .then(function(all) { return all || [] });
     }
 
     service.get_default = function() {
-        return egCore.hatch.getItem('eg.workstation.default');
+        return egCore.hatch.getDefaultWorkstation();
     }
 
     service.set_default = function(name) {
-        return egCore.hatch.setItem('eg.workstation.default', name);
+        return egCore.hatch.setDefaultWorkstation(name);
     }
 
     service.register_workstation = function(base_name, name, org_id) {
@@ -140,7 +140,7 @@ function($q , $timeout , $location , egCore , egConfirmDialog) {
         return service.get_all()
         .then(function(all) {
             all.push(new_ws);
-            return egCore.hatch.setItem('eg.workstation.all', all)
+            return egCore.hatch.setWorkstations(all)
             .then(function() { return new_ws });
         });
     }
@@ -150,13 +150,13 @@ function($q , $timeout , $location , egCore , egConfirmDialog) {
     service.remove_workstation = function(name) {
         console.debug('Removing workstation: ' + name);
 
-        return egCore.hatch.getItem('eg.workstation.all')
+        return egCore.hatch.getWorkstations()
 
         // remove from list of all workstations
         .then(function(all) {
             if (!all) all = [];
             var keep = all.filter(function(ws) {return ws.name != name});
-            return egCore.hatch.setItem('eg.workstation.all', keep)
+            return egCore.hatch.setWorkstations(keep);
 
         }).then(function() { 
 
@@ -165,7 +165,7 @@ function($q , $timeout , $location , egCore , egConfirmDialog) {
         }).then(function(def) {
             if (def == name) {
                 console.debug('Removing default workstation: ' + name);
-                return egCore.hatch.removeItem('eg.workstation.default');
+                return egCore.hatch.removeDefaultWorkstation();
             }
         });
     }
@@ -234,8 +234,13 @@ function($scope , egCore) {
     $scope.setContentType = function(type) { $scope.contentType = type }
     $scope.setContentType('text/plain');
 
+    var hatchPrinting = false;
+    egCore.hatch.usePrinting().then(function(answer) {
+        hatchPrinting = answer;
+    });
+
     $scope.useHatchPrinting = function() {
-        return egCore.hatch.usePrinting();
+        return hatchPrinting;
     }
 
     $scope.hatchIsOpen = function() {
@@ -959,15 +964,18 @@ function($scope , egCore , ngToast) {
     var hatch = egCore.hatch;  // convenience
 
     $scope.hatch_available = hatch.hatchAvailable;
-    $scope.hatch_printing = hatch.usePrinting();
     $scope.hatch_settings = hatch.useSettings();
     $scope.hatch_offline  = hatch.useOffline();
 
+    hatch.usePrinting().then(function(answer) {
+        $scope.hatch_printing = answer;
+    });
+
     // Apply Hatch settings as changes occur in the UI.
     
     $scope.$watch('hatch_printing', function(newval) {
         if (typeof newval != 'boolean') return;
-        hatch.setLocalItem('eg.hatch.enable.printing', newval);
+        hatch.setItem('eg.hatch.enable.printing', newval);
     });
 
     $scope.$watch('hatch_settings', function(newval) {
diff --git a/Open-ILS/web/js/ui/default/staff/app.js b/Open-ILS/web/js/ui/default/staff/app.js
index 053f4014a6..e38b76f92d 100644
--- a/Open-ILS/web/js/ui/default/staff/app.js
+++ b/Open-ILS/web/js/ui/default/staff/app.js
@@ -60,7 +60,7 @@ function($routeProvider , $locationProvider) {
         // if the user is already logged in, jump to splash page
         if (egCore.auth.user()) $location.path('/');
 
-        egCore.hatch.getItem('eg.workstation.all')
+        egCore.hatch.getWorkstations()
         .then(function(all) {
             if (all && all.length) {
                 $scope.workstations = all.map(function(a) { return a.name });
@@ -79,7 +79,7 @@ function($routeProvider , $locationProvider) {
                     }
                 } else {
                     // no workstation requested; use the default
-                    egCore.hatch.getItem('eg.workstation.default')
+                    egCore.hatch.getDefaultWorkstation()
                     .then(function(ws) {
                         $scope.args = {workstation : ws}
                     });
diff --git a/Open-ILS/web/js/ui/default/staff/offline.js b/Open-ILS/web/js/ui/default/staff/offline.js
index 2a3f820c58..21e10b0dd3 100644
--- a/Open-ILS/web/js/ui/default/staff/offline.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -360,7 +360,7 @@ function($routeProvider , $locationProvider , $compileProvider) {
             if (setting !== undefined) $scope.do_check_changed = true;
         });
 
-        egCore.hatch.getItem('eg.workstation.all')
+        egCore.hatch.getWorkstations()
         .then(function(all) {
             if (all && all.length) {
                 $scope.workstations = all;
@@ -381,7 +381,7 @@ function($routeProvider , $locationProvider , $compileProvider) {
                     }
                 } else {
                     // no workstation requested; use the default
-                    egCore.hatch.getItem('eg.workstation.default')
+                    egCore.hatch.getDefaultWorkstation()
                     .then(function(ws) {
                         var ws_obj = all.filter(function(w) {
                             return ws == w.name
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index 5b6f9235d2..c7e2f39485 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -44,36 +44,11 @@ angular.module('egCoreMod')
     service.serverSettingSummaries = {};
 
     /**
-     * List string prefixes for On-Call storage keys. On-Call keys
-     * are those that can be set/get/remove'd from localStorage when
-     * Hatch is not avaialable, even though Hatch is configured as the
-     * primary storage location for the key in question.  On-Call keys
-     * are those that allow the user to login and perform basic admin
-     * tasks (like disabling Hatch) even when Hatch is down.
-     * AKA Browser Staff Run Level 3.
-     * Note that no attempt is made to synchronize data between Hatch
-     * and localStorage for On-Call keys.  Only one destation is active 
-     * at a time and each maintains its own data separately.
-     */
-    service.onCallPrefixes = ['eg.workstation'];
-    
-    // Returns true if the key can be set/get in localStorage even when 
-    // Hatch is not available.
-    service.keyIsOnCall = function(key) {
-        var oncall = false;
-        angular.forEach(service.onCallPrefixes, function(pfx) {
-            if (key.match(new RegExp('^' + pfx))) 
-                oncall = true;
-        });
-        return oncall;
-    }
-
-    /**
      * Settings with these prefixes will always live in the browser.
      */
     service.browserOnlyPrefixes = [
-        'eg.workstation', 
-        'eg.hatch',
+        'eg.hatch.enable.settings', // deprecated
+        'eg.hatch.enable.offline', // deprecated
         'eg.cache',
         'current_tag_table_marc21_biblio',
         'FFPos',
@@ -233,24 +208,169 @@ angular.module('egCoreMod')
         );
     }
 
-    // TODO: once Hatch is printing-only, should probably store
-    // this preference on the server.
     service.usePrinting = function() {
-        return service.getLocalItem('eg.hatch.enable.printing');
+        return service.getItem('eg.hatch.enable.printing');
     }
 
+    // DEPRECATED
     service.useSettings = function() {
         return service.getLocalItem('eg.hatch.enable.settings');
     }
 
+    // DEPRECATED
     service.useOffline = function() {
         return service.getLocalItem('eg.hatch.enable.offline');
     }
 
+    service.getWorkstations = function() {
+        if (service.hatchAvailable) {
+            return service.mergeWorkstations().then(
+                function() {
+                    service.removeLocalItem('eg.workstation.all');
+                    return service.getRemoteItem('eg.workstation.all');
+                }
+            );
+        } else {
+            return $q.when(service.getLocalItem('eg.workstation.all'));
+        }
+    }
+
+    // See if any workstations are stored in local storage.  If so, also
+    // see if we have any stored in Hatch.  If both, merged workstations
+    // from localStorage in Hatch storage, skipping any whose name
+    // collide with a workstation in Hatch.  If none exist in Hatch,
+    // copy the localStorage workstations over wholesale.
+    service.mergeWorkstations = function() {
+        var existing = service.getLocalItem('eg.workstation.all');
+
+        if (!existing || existing.length === 0) {
+            return $q.when();
+        }
+
+        return service.getRemoteItem('eg.workstation.all')
+        .then(function(inHatch) {
+
+            if (!inHatch || inHatch.length === 0) {
+                // Nothing to merge, copy the data over directly
+                console.debug('No workstations in hatch to merge');
+                return service.setRemoteItem('eg.workstation.all', existing);
+            }
+
+            var addMe = [];
+            existing.forEach(function(ws) {
+                var match = inHatch.filter(
+                    function(w) {return w.name === ws.name})[0];
+                if (!match) {
+                    console.log(
+                        'Migrating workstation from local storage to hatch: ' 
+                        + ws.name
+                    );
+                    addMe.push(ws);
+                }
+            });
+            inHatch = inHatch.concat(addMe);
+            return service.setRemoteItem('eg.workstation.all', inHatch);
+        });
+    }
+
+    service.getDefaultWorkstation = function() {
+
+        if (service.hatchAvailable) {
+            return service.getRemoteItem('eg.workstation.default')
+            .then(function(name) {
+                if (name) {
+                    // We have a default in Hatch, remove any lingering
+                    // value from localStorage.
+                    service.removeLocalItem('eg.workstation.default');
+                    return name;
+                }
+
+                name = service.getLocalItem('eg.workstation.default');
+                if (name) {
+                    console.log('Migrating default workstation to Hatch ' + name);
+                    return service.setRemoteItem('eg.workstation.default', name)
+                    .then(function() {return name;});
+                }
+
+                return null;
+            });
+        } else {
+            return $q.when(service.getLocalItem('eg.workstation.default'));
+        }
+    }
+
+    service.setWorkstations = function(workstations, isJson) {
+        if (service.hatchAvailable) {
+            return service.setRemoteItem('eg.workstation.all', workstations);
+        } else {
+            return $q.when(
+                service.setLocalItem('eg.workstation.all', workstations, isJson));
+        }
+    }
+
+    service.setDefaultWorkstation = function(name, isJson) {
+        if (service.hatchAvailable) {
+            return service.setRemoteItem('eg.workstation.default', name);
+        } else {
+            return $q.when(
+                service.setLocalItem('eg.workstation.default', name, isJson));
+        }
+    }
+
+    service.removeWorkstations = function() {
+        if (service.hatchAvailable) {
+            return service.removeRemoteItem('eg.workstation.all');
+        } else {
+            return $q.when(
+                service.removeLocalItem('eg.workstation.all'));
+        }
+    }
+
+    service.removeDefaultWorkstation = function() {
+        if (service.hatchAvailable) {
+            return service.removeRemoteItem('eg.workstation.default');
+        } else {
+            return $q.when(
+                service.removeLocalItem('eg.workstation.default'));
+        }
+    }
+
+
+    // Workstation actions always use Hatch when it's available
+    service.getWorkstationItem = function(key) {
+        if (service.hatchAvailable) {
+            return service.getRemoteItem(key);
+        } else {
+            return $q.when(service.getLocalItem(key));
+        }
+    }
+
+    service.setWorkstationItem = function(key, value) {
+        if (service.hatchAvailable) {
+            return service.setRemoteItem(key, value);
+        } else {
+            return $q.when(service.setLocalItem(key, value));
+        }
+    }
+
+    service.removeWorkstationItem = function(key) {
+        if (service.hatchAvailable) {
+            return service.removeRemoteItem(key);
+        } else {
+            return $q.when(service.removeLocalItem(key));
+        }
+    }
+
+    service.keyIsWorkstation = function(key) {
+        return Boolean(key.match(/eg.workstation/));
+    }
+
     // get the value for a stored item
     service.getItem = function(key) {
 
-        console.debug('getting item: ' + key);
+        if (service.keyIsWorkstation(key)) {
+            return service.getWorkstationItem(key);
+        }
 
         if (!service.keyStoredInBrowser(key)) {
             return service.getServerItem(key);
@@ -260,14 +380,7 @@ angular.module('egCoreMod')
 
         service.getBrowserItem(key).then(
             function(val) { deferred.resolve(val); },
-
             function() { // Hatch error
-                if (service.keyIsOnCall(key)) {
-                    console.warn("Unable to getItem from Hatch: " + key + 
-                        ". Retrieving item from local storage instead");
-                    deferred.resolve(service.getLocalItem(key));
-                }
-
                 deferred.reject("Unable to getItem from Hatch: " + key);
             }
         );
@@ -388,6 +501,10 @@ angular.module('egCoreMod')
      */
     service.setItem = function(key, value) {
 
+        if (service.keyIsWorkstation(key)) {
+            return service.setWorkstationItem(key, value);
+        }
+
         if (!service.keyStoredInBrowser(key)) {
             return service.setServerItem(key, value);
         }
@@ -397,13 +514,6 @@ angular.module('egCoreMod')
             function(val) {deferred.resolve(val);},
 
             function() { // Hatch error
-
-                if (service.keyIsOnCall(key)) {
-                    console.warn("Unable to setItem in Hatch: " + 
-                        key + ". Setting in local storage instead");
-
-                    deferred.resolve(service.setLocalItem(key, value));
-                }
                 deferred.reject("Unable to setItem in Hatch: " + key);
             }
         );
@@ -627,13 +737,6 @@ angular.module('egCoreMod')
         if (service.hatchAvailable)
             return service.appendRemoteItem(key, value);
 
-        if (service.keyIsOnCall(key)) {
-            console.warn("Unable to appendItem in Hatch: " + 
-                key + ". Setting in local storage instead");
-
-            return $q.when(service.appendLocalItem(key, value));
-        }
-
         console.error("Unable to appendItem in Hatch: " + key);
         return $q.reject();
     }
@@ -686,6 +789,10 @@ angular.module('egCoreMod')
     // remove a stored item
     service.removeItem = function(key) {
 
+        if (service.keyIsWorkstation(key)) {
+            return service.removeWorkstationItem(key);
+        }
+
         if (!service.keyStoredInBrowser(key)) {
             return service.removeServerItem(key);
         }
@@ -694,14 +801,6 @@ angular.module('egCoreMod')
         service.removeBrowserItem(key).then(
             function(response) {deferred.resolve(response);},
             function() { // Hatch error
-
-                if (service.keyIsOnCall(key)) {
-                    console.warn("Unable to removeItem from Hatch: " + key + 
-                        ". Removing item from local storage instead");
-
-                    deferred.resolve(service.removeLocalItem(key));
-                }
-
                 deferred.reject("Unable to removeItem from Hatch: " + key);
             }
         );

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

Summary of changes:
 Open-ILS/src/eg2/src/app/app.component.ts          |   2 +-
 Open-ILS/src/eg2/src/app/common.module.ts          |   4 +-
 Open-ILS/src/eg2/src/app/core/auth.service.ts      |  24 +-
 Open-ILS/src/eg2/src/app/core/format.spec.ts       |   5 +-
 .../src/app/{share/print => core}/hatch.service.ts |  28 +-
 Open-ILS/src/eg2/src/app/core/org.spec.ts          |   5 +-
 Open-ILS/src/eg2/src/app/core/store.service.ts     | 115 +++++++-
 Open-ILS/src/eg2/src/app/core/store.spec.ts        |   5 +-
 .../eg2/src/app/share/print/print.component.html   |   7 +-
 .../src/eg2/src/app/share/print/print.component.ts | 107 +++++---
 .../eg2/src/app/share/util/htmltotxt.service.ts    |  75 ++++++
 .../patron_barcode_validator.directive.spec.ts     |   5 +-
 .../workstations/workstations.component.ts         |  39 ++-
 Open-ILS/src/eg2/src/app/staff/login.component.ts  |  12 +-
 Open-ILS/src/eg2/src/app/staff/resolver.service.ts |   4 +
 .../src/app/staff/sandbox/sandbox.component.html   |   1 +
 .../eg2/src/app/staff/sandbox/sandbox.component.ts |   7 +-
 .../src/eg2/src/app/staff/staff.component.html     |   4 +
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   2 +-
 Open-ILS/src/sql/Pg/005.schema.actors.sql          |   3 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  11 +
 .../Pg/upgrade/1197.data.hatch-enable-print.sql    |  53 ++++
 .../staff/admin/workstation/t_print_config.tt2     |  26 +-
 .../js/ui/default/staff/admin/workstation/app.js   |  53 +++-
 Open-ILS/web/js/ui/default/staff/app.js            |   4 +-
 Open-ILS/web/js/ui/default/staff/offline.js        |   4 +-
 Open-ILS/web/js/ui/default/staff/services/hatch.js | 298 ++++++++++++++++-----
 Open-ILS/web/js/ui/default/staff/services/print.js |  12 +-
 .../Client/hatch-file-writer.adoc                  |  11 +
 29 files changed, 761 insertions(+), 165 deletions(-)
 rename Open-ILS/src/eg2/src/app/{share/print => core}/hatch.service.ts (75%)
 create mode 100644 Open-ILS/src/eg2/src/app/share/util/htmltotxt.service.ts
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1197.data.hatch-enable-print.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Client/hatch-file-writer.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list