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

Evergreen Git git at git.evergreen-ils.org
Thu Feb 16 15:23:40 EST 2017


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  500b7273183d62a1de67bbac6f0eafa8582bcb59 (commit)
       via  590f351e8399510384440f3baf351ef898a07f12 (commit)
       via  327542cb767675c334a1860567e6f0f16d6e17cb (commit)
       via  0d98891e081006a047212f7b874d990da8c79b5a (commit)
       via  1fc83b6041f1ccb67dd78948efb2cd3293336e7f (commit)
       via  6455db11ae7db888e00cad9b02a81197b878ffe5 (commit)
       via  0d828c7d35b2f9aa95a30c85bc6fb5f574357685 (commit)
       via  d0093ed2e3c56c8093c3def5b48a6ac359969ecd (commit)
       via  c649d0e83ec71385492a01803b5ef288d0c615e8 (commit)
       via  c72b8e318eec3d39d39b52ed307ab09b1bbbba99 (commit)
       via  c2254e85d0c958bf8321f916e52bc964d90ae8ee (commit)
      from  83b9d7973ae328d464233fffde3345db94fb52ca (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 500b7273183d62a1de67bbac6f0eafa8582bcb59
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Feb 16 12:17:55 2017 -0500

    LP#1646166 Hatch printing multi root-node templates
    
    Print templates with no root element or root elements that contained
    necessary attributes (id, style, etc.) failed to print correctly in
    some casese with Hatch HTML printing because the root node was dropped
    after compilation.  This commit changes how we extract the compiled HTML
    from the page, using the more reliable method of pulling it directly
    from the print element div instead of inspecting the compiled nodes.
    
    Commit also wraps all content in <html/><body/> for good measure.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 02424a9..67a26b3 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -79,6 +79,8 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
         }
 
         return promise.then(function(html) {
+            // For good measure, wrap the compiled HTML in container tags.
+            html = "<html><body>" + html + "</body></html>";
             return egHatch.remotePrint(
                 args.context || 'default',
                 args.content_type, 
@@ -214,15 +216,12 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
 
                     var resp = $compile($scope.elm.contents())(sub_scope);
 
+
                     var deferred = $q.defer();
                     $timeout(function(){
-                        // give the $digest a chance to complete then
-                        // resolve with the compiled HTML from our
-                        // print container
-
-                        deferred.resolve(
-                            resp.contents()[0].parentNode.innerHTML
-                        );
+                        // give the $digest a chance to complete then resolve
+                        // with the compiled HTML from our print container
+                        deferred.resolve($scope.elm.html());
                     });
 
                     return deferred.promise;

commit 590f351e8399510384440f3baf351ef898a07f12
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Feb 10 17:06:57 2017 -0500

    LP#1646166 Hatch templated print content type repair
    
    Fixes a thinko in the Hatch code that set a bad default content type for
    template-driven HTML printing.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 ef01410..02424a9 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -32,7 +32,7 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
             return service.getPrintTemplate(args.template)
             .then(function(content) {
                 args.content = content;
-                if (!args.content_type) args.content_type = 'html';
+                if (!args.content_type) args.content_type = 'text/html';
                 service.getPrintTemplateContext(args.template)
                 .then(function(context) {
                     args.context = context;

commit 327542cb767675c334a1860567e6f0f16d6e17cb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jan 27 10:39:31 2017 -0500

    LP#1646166 Hatch attach point is documentElement
    
    Hatch now inserts its attribute into the root documentElement instead of
    the body.  This is part of the move to using content_scripts instead of
    declarativeContent for the extension.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/base.tt2 b/Open-ILS/src/templates/staff/base.tt2
index 85cab84..445b1ad 100644
--- a/Open-ILS/src/templates/staff/base.tt2
+++ b/Open-ILS/src/templates/staff/base.tt2
@@ -27,7 +27,7 @@
     <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/cat.css" />
     <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/style.css" />
   </head>
-  <body hatch-is-welcome-here>
+  <body>
     <toast></toast>
 
     <!-- load the navbar template inline since it's used on every page -->
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 0dc1677..4fee7cb 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -123,9 +123,8 @@ angular.module('egCoreMod')
     service.openHatch = function() {
 
         // When the Hatch extension loads, it tacks an attribute onto
-        // the page body to indicate it's available.
-
-        if (!$window.document.body.getAttribute('hatch-is-open')) {
+        // the top-level documentElement to indicate it's available.
+        if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
             console.debug("Hatch is not available");
             return;
         }

commit 0d98891e081006a047212f7b874d990da8c79b5a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 25 16:40:15 2017 -0500

    LP#1646166 Hatch loader matches on explicit DOM content
    
    Make it more evident in the browser staff markup that Hatch is getting
    requested by the page.  Additionally, the <body> tag will never disappear,
    though in theory <eg-navbar> could.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/base.tt2 b/Open-ILS/src/templates/staff/base.tt2
index 445b1ad..85cab84 100644
--- a/Open-ILS/src/templates/staff/base.tt2
+++ b/Open-ILS/src/templates/staff/base.tt2
@@ -27,7 +27,7 @@
     <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/cat.css" />
     <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/style.css" />
   </head>
-  <body>
+  <body hatch-is-welcome-here>
     <toast></toast>
 
     <!-- load the navbar template inline since it's used on every page -->

commit 1fc83b6041f1ccb67dd78948efb2cd3293336e7f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jan 19 11:31:12 2017 -0500

    LP#1646166 Hatch settings migration
    
    Support copying all settings from local storage to Hatch and vice versa
    in the Hatch admin UI.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/admin/workstation/index.tt2 b/Open-ILS/src/templates/staff/admin/workstation/index.tt2
index 2d8b357..44842ea 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/index.tt2
@@ -21,6 +21,8 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
   s.PRINT_TEMPLATES_FAIL_EXPORT = "[% l('There are no customized print template to export') %]";
   s.PRINT_TEMPLATES_SUCCESS_IMPORT = "[% l('Imported one or more print template(s)') %]";
   s.PRINT_TEMPLATES_FAIL_IMPORT = "[% l('Failed to import any print template(s)') %]";
+  s.HATCH_SETTINGS_MIGRATION_SUCCESS = "[% l('Settings successfully migrated') %]";
+  s.HATCH_SETTINGS_MIGRATION_FAILURE = "[% l('Settings migration failed') %]";
 }]);
 </script>
 [% END %]
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2
index d8d87ea..836c893 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2
@@ -12,7 +12,7 @@
   </div>
 
   <div class="row">
-    <div class="col-md-6">
+    <div class="col-md-4">
       <div class="checkbox">
         <label>
           <input type="checkbox" 
@@ -24,7 +24,7 @@
   </div>
 
   <div class="row new-entry">
-    <div class="col-md-6">
+    <div class="col-md-4">
       <div class="checkbox">
         <label>
           <input type="checkbox" 
@@ -33,10 +33,20 @@
         </label>
       </div>
     </div>
+    <div class="col-md-3">
+      <button class="btn btn-default" ng-click="copy_to_hatch()">
+        [% l('Copy Local Storage Settings To Hatch') %]
+      </button>
+    </div>
+    <div class="col-md-3">
+      <button class="btn btn-default" ng-click="copy_to_local()">
+        [% l('Copy Hatch Settings To Local Storage') %]
+      </button>
+    </div>
   </div>
 
   <div class="row new-entry">
-    <div class="col-md-6">
+    <div class="col-md-4">
       <div class="checkbox">
         <label>
           <input type="checkbox" 
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 75804a6..a25d719 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
@@ -851,8 +851,8 @@ function($scope , $q , $window , $location , egCore , egAlertDialog , workstatio
 }])
 
 .controller('HatchCtrl',
-       ['$scope','egCore',
-function($scope , egCore) {
+       ['$scope','egCore','ngToast',
+function($scope , egCore , ngToast) {
     var hatch = egCore.hatch;  // convenience
 
     $scope.hatch_available = hatch.hatchAvailable;
@@ -877,6 +877,24 @@ function($scope , egCore) {
         hatch.setLocalItem('eg.hatch.enable.offline', newval);
     });
 
+    $scope.copy_to_hatch = function() {
+        hatch.copySettingsToHatch().then(
+            function() {
+                ngToast.create(egCore.strings.HATCH_SETTINGS_MIGRATION_SUCCESS)},
+            function() {
+                ngToast.warning(egCore.strings.HATCH_SETTINGS_MIGRATION_FAILURE)}
+        );
+    }
+
+    $scope.copy_to_local = function() {
+        hatch.copySettingsToLocal().then(
+            function() {
+                ngToast.create(egCore.strings.HATCH_SETTINGS_MIGRATION_SUCCESS)},
+            function() {
+                ngToast.warning(egCore.strings.HATCH_SETTINGS_MIGRATION_FAILURE)}
+        );
+    }
+
 }])
 
 
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 769f801..0dc1677 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -274,7 +274,7 @@ angular.module('egCoreMod')
      * tmp values are removed during logout or browser close.
      */
     service.setItem = function(key, value) {
-        if (service.useSettings())
+        if (!service.useSettings())
             return $q.when(service.setLocalItem(key, value));
 
         if (service.hatchAvailable)
@@ -447,6 +447,58 @@ angular.module('egCoreMod')
         service.setLocalItem('eg.hatch.login_keys', keys);
     }
 
+    // Copy all stored settings from localStorage to Hatch.
+    // If 'move' is true, delete the local settings once cloned.
+    service.copySettingsToHatch = function(move) {
+        var deferred = $q.defer();
+        var keys = service.getLocalKeys();
+
+        angular.forEach(keys, function(key) {
+
+            // Hatch keys are local-only
+            if (key.match(/^eg.hatch/)) return;
+
+            console.debug("Copying to Hatch Storage: " + key);
+            service.setRemoteItem(key, service.getLocalItem(key))
+            .then(function() { // key successfully cloned.
+
+                // delete the local copy if requested.
+                if (move) service.removeLocalItem(key);
+
+                // resolve the promise after processing the last key.
+                if (key == keys[keys.length-1]) 
+                    deferred.resolve();
+            });
+        });
+
+        return deferred.promise;
+    }
+
+    // Copy all stored settings from Hatch to localStorage.
+    // If 'move' is true, delete the Hatch settings once cloned.
+    service.copySettingsToLocal = function(move) {
+        var deferred = $q.defer();
+
+        service.getRemoteKeys().then(function(keys) {
+            angular.forEach(keys, function(key) {
+                service.getRemoteItem(key).then(function(val) {
+
+                    console.debug("Copying to Local Storage: " + key);
+                    service.setLocalItem(key, val);
+
+                    // delete the remote copy if requested.
+                    if (move) service.removeRemoteItem(key);
+
+                    // resolve the promise after processing the last key.
+                    if (key == keys[keys.length-1]) 
+                        deferred.resolve();
+                });
+            });
+        });
+
+        return deferred.promise;
+    }
+
     // 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 6455db11ae7db888e00cad9b02a81197b878ffe5
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 18 16:22:30 2017 -0500

    LP#1646166 Hatch availability display improvements
    
    Hide printer settings in the print config UI when Hatch is not available
    and provide better inline alerts.  Avoid attempts by the printer config
    UI from talking to Hatch when Hatch is known not to be available.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 4c88af2..8cd6bf9 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
@@ -15,19 +15,15 @@
 
   <div class="row"> 
     <div class="col-md-12">
-      <h2>[% l('Remote Printer Settings') %]</h2>
-
-      <div class="alert alert-warning" ng-if="!hatchIsOpen()">
-[% l('Remote printing is not available on this browser.  The settings below will have no effect.') %]
-      </div>
+      <h2>[% l('Hatch Printer Settings') %]</h2>
 
       <div class="alert alert-warning" 
         ng-if="hatchIsOpen() && !useHatchPrinting()">
         <p>
-[% l("Remote printing is not enabled on this browser.  The settings below will have no effect until remote printing is enabled.") %]
+[% l("Hatch printing is not enabled on this browser.  The settings below will have no effect until Hatch printing is enabled.") %]
           <a href="./admin/workstation/hatch" target="_self" 
             title="[% l('Hatch Administration') %]">
-            [% l('Enable Remote Printing.') %]
+            [% l('Enable Hatch Printing.') %]
           </a>
         </p>
       </div>
@@ -59,8 +55,12 @@
       <div class="tab-content">
         <div class="tab-pane active">
 
+          <div class="alert alert-warning" ng-if="!isTestView && !hatchIsOpen()">
+[% l('Hatch is not installed in this browser.  Printing must be configured via the native browser print configuration.') %]
+          </div>
+
           <!-- printer config UI -->
-          <div class="row" ng-hide="isTestView">
+          <div class="row" ng-hide="isTestView || !hatchIsOpen()">
             <div class="col-md-6">
               <div class="input-group">
                 <div class="input-group-btn" uib-dropdown>
@@ -100,7 +100,7 @@
             </div>
           </div><!-- row -->
 
-          <div class="row" ng-hide="isTestView"> 
+          <div class="row" ng-hide="isTestView || !hatchIsOpen()"> 
             <div class="col-md-10">
               <div class="row">
                 <div class="col-md-1"></div>
@@ -351,7 +351,7 @@
             </div><!-- col -->
           </div><!-- row -->
 
-          <div class="row" ng-hide="isTestView">
+          <div class="row" ng-hide="isTestView || !hatchIsOpen()">
             <div class="col-md-12">
               <h3>[% l('Compiled Printer Settings') %]</h3>
               <pre>{{printerConfString()}}</pre>
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 7de3694..75804a6 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
@@ -223,6 +223,9 @@ function($scope , egCore) {
     }
     $scope.setContext('default');
 
+    $scope.setContentType = function(type) { $scope.contentType = type }
+    $scope.setContentType('text/plain');
+
     $scope.useHatchPrinting = function() {
         return egCore.hatch.usePrinting();
     }
@@ -239,33 +242,6 @@ function($scope , egCore) {
         return printer;
     }
 
-    // fetch info on all remote printers
-    egCore.hatch.getPrinters()
-    .then(function(printers) { 
-        $scope.printers = printers;
-
-        var def = $scope.getPrinterByAttr('is-default', true);
-        if (!def && printers.length) def = printers[0];
-
-        if (def) {
-            $scope.defaultPrinter = def;
-            loadPrinterOptions(def.name);
-        }
-    }).then(function() {
-        angular.forEach(
-            ['default','receipt','label','mail','offline'],
-            function(ctx) {
-                egCore.hatch.getPrintConfig(ctx).then(function(conf) {
-                    if (conf) {
-                        $scope.printConfig[ctx] = conf;
-                    } else {
-                        $scope.resetPrinterSettings(ctx);
-                    }
-                });
-            }
-        );
-    });
-
     $scope.resetPrinterSettings = function(context) {
         $scope.printConfig[context] = {
             context : context,
@@ -299,9 +275,6 @@ function($scope , egCore) {
         loadPrinterOptions(name);
     }
 
-    // for testing
-    $scope.setContentType = function(type) { $scope.contentType = type }
-
     $scope.testPrint = function(withDialog) {
         if ($scope.contentType == 'text/plain') {
             egCore.print.print({
@@ -325,7 +298,36 @@ function($scope , egCore) {
         }
     }
 
-    $scope.setContentType('text/plain');
+    // Load startup data....
+    // Don't bother talking to Hatch if it's not there.
+    if (!egCore.hatch.hatchAvailable) return;
+
+    // fetch info on all remote printers
+    egCore.hatch.getPrinters()
+    .then(function(printers) { 
+        $scope.printers = printers;
+
+        var def = $scope.getPrinterByAttr('is-default', true);
+        if (!def && printers.length) def = printers[0];
+
+        if (def) {
+            $scope.defaultPrinter = def;
+            loadPrinterOptions(def.name);
+        }
+    }).then(function() {
+        angular.forEach(
+            ['default','receipt','label','mail','offline'],
+            function(ctx) {
+                egCore.hatch.getPrintConfig(ctx).then(function(conf) {
+                    if (conf) {
+                        $scope.printConfig[ctx] = conf;
+                    } else {
+                        $scope.resetPrinterSettings(ctx);
+                    }
+                });
+            }
+        );
+    });
 
 }])
 
@@ -661,8 +663,10 @@ function($scope , $q , egCore , egConfirmDialog) {
     function refreshKeys() {
         $scope.keys = {local : [], remote : []};
 
-        egCore.hatch.getRemoteKeys().then(
-            function(keys) { $scope.keys.remote = keys.sort() })
+        if (egCore.hatch.hatchAvailable) {
+            egCore.hatch.getRemoteKeys().then(
+                function(keys) { $scope.keys.remote = keys.sort() })
+        }
     
         // local calls are non-async
         $scope.keys.local = egCore.hatch.getLocalKeys();

commit 0d828c7d35b2f9aa95a30c85bc6fb5f574357685
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 18 14:36:11 2017 -0500

    LP#1646166 À la carte Hatch, on-call settings, strict access.
    
    1. Hatch now supports a al carte features instead of requiring all-or-none
    functionality.  Supported features currently include printing, settings,
    and offline.  (Note: offline handling pending merge of offline UI code).
    
    2. Adds support for on-call setting 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.
    
    The initital target use case for 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.
    
    3. egHatch no longer attempts requests at Hatch, falling through to
    local requests when Hatch fails.  With the exception of on-call keys
    (above), either Hatch is used or local requests are used, depending on
    the configuration.  The goal is to prevent any unintended and
    confusing blending of local and remote data.  In other words, if Hatch
    is broken, it needs to be fixed or disabled for regular work flow to
    continue.
    
    4. Hatch now has a dedicated UI under workstation administration.
    
    5. Workstation admin splash page rearranged to take advantage of more
    horizontal space and avoid pushing so many options down the page.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2
new file mode 100644
index 0000000..d8d87ea
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2
@@ -0,0 +1,52 @@
+<div class="container admin-splash-container">
+
+  <h2>[% l('Print / Storage Service ("Hatch")') %]</h2>
+
+  <div class="alert alert-success" ng-if="hatch_available">
+    [% l("Hatch is Available") %]
+    <span class="glyphicon glyphicon-thumbs-up"></span>
+  </div>
+
+  <div class="alert alert-danger" ng-if="!hatch_available">
+    [% l("Hatch is Not Installed In This Browser") %]
+  </div>
+
+  <div class="row">
+    <div class="col-md-6">
+      <div class="checkbox">
+        <label>
+          <input type="checkbox" 
+            ng-model="hatch_printing">
+            [% l('Use Hatch For Printing') %]
+        </label>
+      </div>
+    </div>
+  </div>
+
+  <div class="row new-entry">
+    <div class="col-md-6">
+      <div class="checkbox">
+        <label>
+          <input type="checkbox" 
+            ng-model="hatch_settings">
+            [% l('Store Local Settings in Hatch') %]
+        </label>
+      </div>
+    </div>
+  </div>
+
+  <div class="row new-entry">
+    <div class="col-md-6">
+      <div class="checkbox">
+        <label>
+          <input type="checkbox" 
+            ng-model="hatch_offline">
+            [% l('Store Offline Transaction Data in Hatch') %]
+        </label>
+      </div>
+    </div>
+  </div>
+
+</div>
+
+
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 8f4a34d..4c88af2 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
@@ -15,9 +15,21 @@
 
   <div class="row"> 
     <div class="col-md-12">
-      <h2>[% l('Printer Settings for Remote Printing') %]</h2>
-      <div class="alert alert-warning" ng-show="hatchNotConnected()">
-        [% l("Hatch is not connected") %]
+      <h2>[% l('Remote Printer Settings') %]</h2>
+
+      <div class="alert alert-warning" ng-if="!hatchIsOpen()">
+[% l('Remote printing is not available on this browser.  The settings below will have no effect.') %]
+      </div>
+
+      <div class="alert alert-warning" 
+        ng-if="hatchIsOpen() && !useHatchPrinting()">
+        <p>
+[% l("Remote printing is not enabled on this browser.  The settings below will have no effect until remote printing is enabled.") %]
+          <a href="./admin/workstation/hatch" target="_self" 
+            title="[% l('Hatch Administration') %]">
+            [% l('Enable Remote Printing.') %]
+          </a>
+        </p>
       </div>
     </div>
   </div>
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
index bb80cc2..578ba73 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
@@ -2,118 +2,126 @@
 <div class="container admin-splash-container">
 
   <div class="row">
-    <div class="col-md-6">
-      <div class="checkbox">
-        <label>
-          <input type="checkbox" ng-class="{disabled : !userHasAdminPerm}"
-            ng-model="hatchRequired" ng-change="updateHatchRequired()">
-[% l('This workstation uses a remote print / storage service ("Hatch")?') %]
-        </label>
+    <div class="col-md-6"><!-- left page column -->
+
+      <div class="row new-entry">
+        <div class="col-md-6">
+          <span class="glyphicon glyphicon-pushpin"></span>
+          <a target="_self" href="./admin/workstation/workstations">
+            [% l('Registered Workstations') %]
+          </a>
+        </div>
+      </div>
+
+      <div class="row new-entry">
+        <div class="col-md-6">
+          <span class="glyphicon glyphicon-print"></span>
+          <a target="_self" href="./admin/workstation/print/config">
+            [% l('Printer Settings') %]
+          </a>
+        </div>
+      </div>
+
+      <div class="row new-entry">
+        <div class="col-md-6">
+          <span class="glyphicon glyphicon-film"></span>
+          <a target="_self" href="./admin/workstation/print/templates">
+            [% l('Print Templates') %]
+          </a>
+        </div>
+      </div>
+
+      <div class="row new-entry">
+        <div class="col-md-6">
+          <span class="glyphicon glyphicon-info-sign"></span>
+          <a target="_self" href="./admin/workstation/stored_prefs">
+            [% l('Stored Preferences') %]
+          </a>
+        </div>
+      </div>
+
+      <div class="row new-entry">
+        <div class="col-md-6">
+          <span class="glyphicon glyphicon-retweet"></span>
+          <a target="_self" href="./admin/workstation/hatch">
+            [% l('Print/Storage Service ("Hatch")') %]
+          </a>
+        </div>
+      </div>
+
+      <div class="row new-entry">
+        <div class="col-md-4">
+          <div class="checkbox">
+            <label>
+              <input type="checkbox"
+                ng-model="disable_sound" 
+                  ng-change="apply_sound()">
+                [% l('Disable Sounds?') %]
+            </label>
+          </div>
+        </div>
+        <div class="col-md-8">
+          <span>Test: </span>
+          <button class="btn btn-success" ng-class="{disabled : disable_sound}" 
+            ng-click="test_audio('success')">[% l('Success') %]</button>
+          <button class="btn btn-info" ng-class="{disabled : disable_sound}" 
+            ng-click="test_audio('info')">[% l('Info') %]</button>
+          <button class="btn btn-warning" ng-class="{disabled : disable_sound}" 
+            ng-click="test_audio('warning')">[% l('Warning') %]</button>
+          <button class="btn btn-danger" ng-class="{disabled : disable_sound}" 
+            ng-click="test_audio('error')">[% l('Error') %]</button>
+        </div>
+      </div>
+
+    </div><!-- left column -->
+    <div class="col-md-6"><!-- right column -->
+
+      <div class="row">
+        <div class="col-md-8">
+            <label for="search_lib_selector">[% ('Default Search Library') %]</label>
+            <p>[% l('The default search library setting determines what library is searched from the advanced search screen and portal page by default. Manual selection of a search library will override it. One recommendation is to set the search library to the highest point you would normally want to search.') %]</p>
+        </div>
+        <div class="col-md-4">
+          <eg-org-selector id="search_lib_selector"
+            selected="search_lib" nodefault
+            label="[% l('Select...') %]"
+            onchange="handle_search_lib_changed">
+          </eg-org-selector>
+        </div>
+      </div><!-- row -->
+
+      <div class="row new-entry">
+        <div class="col-md-8">
+            <label for="pref_lib_selector">[% ('Preferred Library') %]</label>
+            <p>[% l('The preferred library is used to show copies and URIs regardless of the library searched. One recommendation is to set this to your workstation library so that local copies show up first in search results.') %]</p>
+        </div>
+        <div class="col-md-4">
+          <eg-org-selector id="pref_lib_selector"
+            selected="pref_lib" nodefault
+            label="[% l('Select...') %]"
+            onchange="handle_pref_lib_changed">
+          </eg-org-selector>
+        </div>
       </div>
-    </div><!-- row -->
-  </div>
-
-
-  <div class="row new-entry">
-    <div class="col-md-2">
-      <div class="checkbox">
-        <label>
-          <input type="checkbox"
-            ng-model="disable_sound" 
-              ng-change="apply_sound()">
-            [% l('Disable Sounds?') %]
-        </label>
+
+      <div class="row new-entry">
+        <div class="col-md-8">
+            <label for="adv_pane_selector">[% ('Advanced Search Default Pane') %]</label>
+            <p>[% l('Advanced search has secondary panes for Numeric and MARC Expert searching. You can change which one is loaded by default when opening a new catalog window here.') %]</p>
+        </div>
+        <div class="col-md-4">
+          <select id="adv_pane_selector" ng-model="adv_pane">
+            <option value="advanced">[% l('Advanced (default)') %]</option>
+            <option value="numeric" >[% l('Numeric') %]</option>
+            <option value="expert"  >[% l('MARC Expert') %]</option>
+          </select>
+        </div>
       </div>
-    </div>
-    <div class="col-md-4">
-      <span>Test: </span>
-      <button class="btn btn-success" ng-class="{disabled : disable_sound}" 
-        ng-click="test_audio('success')">[% l('Success') %]</button>
-      <button class="btn btn-info" ng-class="{disabled : disable_sound}" 
-        ng-click="test_audio('info')">[% l('Info') %]</button>
-      <button class="btn btn-warning" ng-class="{disabled : disable_sound}" 
-        ng-click="test_audio('warning')">[% l('Warning') %]</button>
-      <button class="btn btn-danger" ng-class="{disabled : disable_sound}" 
-        ng-click="test_audio('error')">[% l('Error') %]</button>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-4">
-        <label for="search_lib_selector">[% ('Default Search Library') %]</label>
-        <p>[% l('The default search library setting determines what library is searched from the advanced search screen and portal page by default. Manual selection of a search library will override it. One recommendation is to set the search library to the highest point you would normally want to search.') %]</p>
-    </div>
-    <div class="col-md-2">
-      <eg-org-selector id="search_lib_selector"
-        selected="search_lib" nodefault
-        label="[% l('Select...') %]"
-        onchange="handle_search_lib_changed">
-      </eg-org-selector>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-4">
-        <label for="pref_lib_selector">[% ('Preferred Library') %]</label>
-        <p>[% l('The preferred library is used to show copies and URIs regardless of the library searched. One recommendation is to set this to your workstation library so that local copies show up first in search results.') %]</p>
-    </div>
-    <div class="col-md-2">
-      <eg-org-selector id="pref_lib_selector"
-        selected="pref_lib" nodefault
-        label="[% l('Select...') %]"
-        onchange="handle_pref_lib_changed">
-      </eg-org-selector>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-4">
-        <label for="adv_pane_selector">[% ('Advanced Search Default Pane') %]</label>
-        <p>[% l('Advanced search has secondary panes for Numeric and MARC Expert searching. You can change which one is loaded by default when opening a new catalog window here.') %]</p>
-    </div>
-    <div class="col-md-2">
-      <select id="adv_pane_selector" ng-model="adv_pane">
-        <option value="advanced">[% l('Advanced (default)') %]</option>
-        <option value="numeric" >[% l('Numeric') %]</option>
-        <option value="expert"  >[% l('MARC Expert') %]</option>
-      </select>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-6">
-      <span class="glyphicon glyphicon-pushpin"></span>
-      <a target="_self" href="./admin/workstation/workstations">
-        [% l('Registered Workstations') %]
-      </a>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-6">
-      <span class="glyphicon glyphicon-print"></span>
-      <a target="_self" href="./admin/workstation/print/config">
-        [% l('Printer Settings') %]
-      </a>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-6">
-      <span class="glyphicon glyphicon-film"></span>
-      <a target="_self" href="./admin/workstation/print/templates">
-        [% l('Print Templates') %]
-      </a>
-    </div>
-  </div>
-
-  <div class="row new-entry">
-    <div class="col-md-6">
-      <span class="glyphicon glyphicon-info-sign"></span>
-      <a target="_self" href="./admin/workstation/stored_prefs">
-        [% l('Stored Preferences') %]
-      </a>
-    </div>
-  </div>
+
+    </div><!-- col -->
+  </div><!-- row -->
+
+
+
 
 </div>
diff --git a/Open-ILS/src/templates/staff/circ/checkin/t_checkin.tt2 b/Open-ILS/src/templates/staff/circ/checkin/t_checkin.tt2
index 9ce6ac7..ccc7e5d 100644
--- a/Open-ILS/src/templates/staff/circ/checkin/t_checkin.tt2
+++ b/Open-ILS/src/templates/staff/circ/checkin/t_checkin.tt2
@@ -112,13 +112,13 @@
         <button class="btn btn-default" 
           ng-click="print_receipt()">[% l('Print Receipt') %]</button>
       </div>
-      <div class="checkbox" ng-if="using_hatch">
+      <div class="checkbox" ng-if="using_hatch_printer">
         <label>
           <input ng-model="show_print_dialog" type="checkbox"/>
           [% l('Show Print Dialog') %]
         </label>
       </div>
-      <div class="pad-horiz" ng-if="using_hatch"></div>
+      <div class="pad-horiz" ng-if="using_hatch_printer"></div>
       <div class="checkbox">
         <label>
           <input ng-model="trim_list" type="checkbox"/>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
index ab12816..b2df987 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
@@ -123,8 +123,8 @@
       [% l('Strict Barcode') %]
     </label>
   </div>
-  <div class="pad-horiz" ng-if="using_hatch"></div>
-  <div class="checkbox" ng-if="using_hatch">
+  <div class="pad-horiz" ng-if="using_hatch_printer"></div>
+  <div class="checkbox" ng-if="using_hatch_printer">
     <label>
       <input ng-model="show_print_dialog" type="checkbox"/>
       [% l('Show Print Dialog') %]
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 310356a..7de3694 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
@@ -38,6 +38,11 @@ angular.module('egWorkstationAdmin',
         resolve : resolver
     });
 
+    $routeProvider.when('/admin/workstation/hatch', {
+        templateUrl: './admin/workstation/t_hatch',
+        controller: 'HatchCtrl',
+        resolve : resolver
+    });
 
     // default page 
     $routeProvider.otherwise({
@@ -165,16 +170,6 @@ function($q , $timeout , $location , egCore , egConfirmDialog) {
        ['$scope','$window','$location','egCore','egConfirmDialog',
 function($scope , $window , $location , egCore , egConfirmDialog) {
 
-    // ---------------------
-    // Hatch Configs
-    $scope.hatchRequired = 
-        egCore.hatch.getLocalItem('eg.hatch.required');
-
-    $scope.updateHatchRequired = function() {
-        egCore.hatch.setLocalItem(
-            'eg.hatch.required', $scope.hatchRequired);
-    }
-
     egCore.hatch.getItem('eg.audio.disable').then(function(val) {
         $scope.disable_sound = val;
     });
@@ -198,7 +193,7 @@ function($scope , $window , $location , egCore , egConfirmDialog) {
         $scope.adv_pane = val;
     });
     $scope.$watch('adv_pane', function(newVal, oldVal) {
-        if (newVal != oldVal) {
+        if (typeof newVal != 'undefined' && newVal != oldVal) {
             egCore.hatch.setItem('eg.search.adv_pane', newVal);
         }
     });
@@ -228,8 +223,12 @@ function($scope , egCore) {
     }
     $scope.setContext('default');
 
-    $scope.hatchNotConnected = function() {
-        return !egCore.hatch.hatchAvailable;
+    $scope.useHatchPrinting = function() {
+        return egCore.hatch.usePrinting();
+    }
+
+    $scope.hatchIsOpen = function() {
+        return egCore.hatch.hatchAvailable;
     }
 
     $scope.getPrinterByAttr = function(attr, value) {
@@ -847,4 +846,33 @@ function($scope , $q , $window , $location , egCore , egAlertDialog , workstatio
     }
 }])
 
+.controller('HatchCtrl',
+       ['$scope','egCore',
+function($scope , egCore) {
+    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();
+
+    // 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);
+    });
+
+    $scope.$watch('hatch_settings', function(newval) {
+        if (typeof newval != 'boolean') return;
+        hatch.setLocalItem('eg.hatch.enable.settings', newval);
+    });
+
+    $scope.$watch('hatch_offline', function(newval) {
+        if (typeof newval != 'boolean') return;
+        hatch.setLocalItem('eg.hatch.enable.offline', newval);
+    });
+
+}])
+
 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
index 5428f16..a9b70ca 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
@@ -41,7 +41,7 @@ function($scope , $q , $window , $location , egCore , checkinSvc , egGridDataPro
     $scope.checkins = checkinSvc.checkins;
     var today = new Date();
     $scope.checkinArgs = {backdate : today}
-    $scope.using_hatch = egCore.hatch.usingHatch();
+    $scope.using_hatch_printer = egCore.hatch.usePrinting();
     $scope.modifiers = {};
     $scope.fine_total = 0;
     $scope.is_capture = $location.path().match(/capture$/);
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
index 82d6e82..a32687e 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
@@ -63,7 +63,7 @@ function($scope , $q , $routeParams , egCore , egUser , patronSvc ,
         );
     }
 
-    $scope.using_hatch = egCore.hatch.usingHatch();
+    $scope.using_hatch_printer = egCore.hatch.usePrinting();
 
     egCore.hatch.getItem('circ.checkout.strict_barcode')
         .then(function(sb){ $scope.strict_barcode = sb });
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 f60e58a..769f801 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -3,10 +3,10 @@
  *
  * Dispatches print and data storage requests to the appropriate handler.
  *
- * With each top-level request, if a connection to Hatch is established,
- * the request is relayed.  If a connection has not been attempted, an
- * attempt is made then the request is handled.  If Hatch is known to be
- * inaccessible, requests are routed to local handlers.
+ * If Hatch is configured to honor the request -- current request types
+ * are 'settings', 'offline', and 'printing' -- the request will be
+ * relayed to the Hatch service.  Otherwise, the request is handled
+ * locally.
  *
  * Most handlers also provide direct remote and local variants to the
  * application can decide to which to use as needed.
@@ -31,13 +31,37 @@ angular.module('egCoreMod')
     var service = {};
     service.msgId = 1;
     service.messages = {};
-    service.pending = [];
-    service.hatchAvailable = null;
+    service.hatchAvailable = false;
 
     // key/value cache -- avoid unnecessary Hatch extension requests.
     // Only affects *RemoteItem calls.
     service.keyCache = {}; 
 
+    /**
+     * 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;
+    }
+
     // write a message to the Hatch port
     service.sendToHatch = function(msg) {
         var msg2 = {};
@@ -54,31 +78,21 @@ angular.module('egCoreMod')
         $window.postMessage(msg2, $window.location.origin);
     }
 
-    // Send the request to Hatch if it's available.  
-    // Otherwise handle the request locally.
+    // Send request to Hatch or reject if Hatch is unavailable
     service.attemptHatchDelivery = function(msg) {
-
         msg.msgid = service.msgId++;
         msg.deferred = $q.defer();
 
-        if (service.hatchAvailable === false) { // Hatch is closed.
-            msg.deferred.reject(msg);
-
-        } else if (service.hatchAvailable === true) { // Hatch is open.
+        if (service.hatchAvailable) {
             service.messages[msg.msgid] = msg;
             service.sendToHatch(msg);
 
-        } else { // Hatch state unknown
-            service.messages[msg.msgid] = msg;
-            service.pending.push(msg);
-            $timeout(service.openHatch);
+        } else {
+            console.error(
+                'Hatch request attempted but Hatch is not available');
+            msg.deferred.reject(msg);
         }
 
-        /*
-        console.debug(
-            Object.keys(service.messages).length + " pending Hatch requests");
-        */
-
         return msg.deferred.promise;
     }
 
@@ -112,7 +126,7 @@ angular.module('egCoreMod')
         // the page body to indicate it's available.
 
         if (!$window.document.body.getAttribute('hatch-is-open')) {
-            service.hatchWontOpen('Hatch is not available');
+            console.debug("Hatch is not available");
             return;
         }
 
@@ -132,38 +146,6 @@ angular.module('egCoreMod')
         }); 
 
         service.hatchAvailable = true; // public flag
-        service.hatchOpened();
-    }
-
-    service.hatchWontOpen = function(err) {
-        console.debug("Hatch connection failed: " + err);
-        service.hatchAvailable = false;
-        while ( (msg = service.pending.shift()) ) {
-            msg.deferred.reject(msg);
-            delete service.messages[msg.msgid];
-        }
-    }
-
-    // Returns true if Hatch is required or if we are currently
-    // communicating with the Hatch service. 
-    service.usingHatch = function() {
-        return service.hatchAvailable || service.hatchRequired();
-    }
-
-    // Returns true if this browser (via localStorage) is 
-    // configured to require Hatch.
-    service.hatchRequired = function() {
-        return service.getLocalItem('eg.hatch.required');
-    }
-
-    service.hatchOpened = function() {
-        // let others know we're connected
-        if (service.onHatchOpen) service.onHatchOpen();
-
-        // Deliver any previously queued requests 
-        while ( (msg = service.pending.shift()) ) {
-            service.sendToHatch(msg);
-        };
     }
 
     service.remotePrint = function(
@@ -217,17 +199,36 @@ angular.module('egCoreMod')
         );
     }
 
+    service.usePrinting = function() {
+        return service.getLocalItem('eg.hatch.enable.printing');
+    }
+
+    service.useSettings = function() {
+        return service.getLocalItem('eg.hatch.enable.settings');
+    }
+
+    service.useOffline = function() {
+        return service.getLocalItem('eg.hatch.enable.offline');
+    }
+
     // get the value for a stored item
     service.getItem = function(key) {
-        return service.getRemoteItem(key)['catch'](
-            function(msg) {
-                if (service.hatchRequired()) {
-                    console.error("getRemoteItem() failed for key " + key);
-                    return null;
-                }
-                return service.getLocalItem(msg.key);
-            }
-        );
+
+        if (!service.useSettings())
+            return $q.when(service.getLocalItem(key));
+
+        if (service.hatchAvailable) 
+            return service.getRemoteItem(key);
+
+        if (service.keyIsOnCall(key)) {
+            console.warn("Unable to getItem from Hatch: " + key + 
+                ". Retrieving item from local storage instead");
+
+            return $q.when(service.getLocalItem(key));
+        }
+
+        console.error("Unable to getItem from Hatch: " + key);
+        return $q.reject();
     }
 
     service.getRemoteItem = function(key) {
@@ -246,7 +247,14 @@ angular.module('egCoreMod')
     service.getLocalItem = function(key) {
         var val = $window.localStorage.getItem(key);
         if (val == null) return;
-        return JSON.parse(val);
+        try {
+            return JSON.parse(val);
+        } catch(E) {
+            console.error(
+                "Deleting invalid JSON for localItem: " + key + " => " + val);
+            service.removeLocalItem(key);
+            return null;
+        }
     }
 
     service.getLoginSessionItem = function(key) {
@@ -266,15 +274,21 @@ angular.module('egCoreMod')
      * tmp values are removed during logout or browser close.
      */
     service.setItem = function(key, value) {
-        return service.setRemoteItem(key, value)['catch'](
-            function(msg) {
-                if (service.hatchRequired()) {
-                    console.error("setRemoteItem() failed for key " + key);
-                     return null;
-                }
-                return service.setLocalItem(msg.key, value);
-            }
-        );
+        if (service.useSettings())
+            return $q.when(service.setLocalItem(key, value));
+
+        if (service.hatchAvailable)
+            return service.setRemoteItem(key, value);
+
+        if (service.keyIsOnCall(key)) {
+            console.warn("Unable to setItem in Hatch: " + 
+                key + ". Setting in local storage instead");
+
+            return $q.when(service.setLocalItem(key, value));
+        }
+
+        console.error("Unable to setItem in Hatch: " + key);
+        return $q.reject();
     }
 
     // set the value for a stored or new item
@@ -320,28 +334,23 @@ angular.module('egCoreMod')
         $window.sessionStorage.setItem(key, jsonified);
     }
 
-    // assumes the appender and appendee are both strings
-    // TODO: support arrays as well
-    service.appendLocalItem = function(key, value) {
-        var item = service.getLocalItem(key);
-        if (item) {
-            if (typeof item != 'string') {
-                logger.warn("egHatch.appendLocalItem => "
-                    + "cannot append to a non-string item: " + key);
-                return;
-            }
-            value = item + value; // concatenate our value
-        }
-        service.setLocalitem(key, value);
-    }
-
     // remove a stored item
     service.removeItem = function(key) {
-        return service.removeRemoteItem(key)['catch'](
-            function(msg) { 
-                return service.removeLocalItem(msg.key) 
-            }
-        );
+        if (!service.useSettings())
+            return $q.when(service.removeLocalItem(key));
+
+        if (service.hatchAvailable) 
+            return service.removeRemoteItem(key);
+
+        if (service.keyIsOnCall(key)) {
+            console.warn("Unable to removeItem from Hatch: " + key + 
+                ". Removing item from local storage instead");
+
+            return $q.when(service.removeLocalItem(key));
+        }
+
+        console.error("Unable to removeItem from Hatch: " + key);
+        return $q.reject();
     }
 
     service.removeRemoteItem = function(key) {
@@ -379,15 +388,9 @@ angular.module('egCoreMod')
 
     // if set, prefix limits the return set to keys starting with 'prefix'
     service.getKeys = function(prefix) {
-        return service.getRemoteKeys(prefix)['catch'](
-            function() { 
-                if (service.hatchRequired()) {
-                    console.error("getRemoteKeys() failed");
-                    return [];
-                }
-                return service.getLocalKeys(prefix) 
-            }
-        );
+        if (service.useSettings()) 
+            return service.getRemoteKeys(prefix);
+        return $q.when(service.getLocalKeys(prefix));
     }
 
     service.getRemoteKeys = function(prefix) {
@@ -444,6 +447,10 @@ angular.module('egCoreMod')
         service.setLocalItem('eg.hatch.login_keys', keys);
     }
 
+    // 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();
+
     return service;
 }])
 
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 d2ffbc9..ef01410 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -59,7 +59,7 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
     service.print_content = function(args) {
         service.fleshPrintScope(args.scope);
 
-        var promise = egHatch.hatchAvailable ?
+        var promise = egHatch.usePrinting() ?
             service.print_via_hatch(args) :
             service.print_via_browser(args);
 

commit d0093ed2e3c56c8093c3def5b48a6ac359969ecd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 11 14:33:13 2017 -0500

    LP#1646166 Hatch print requires no print CSS
    
    Avoid inserting the print CSS into the print content when printing
    remotely.
    
    Commit includes some refactoring of the print code to create more
    obvious separation between remote vs. in-browser printing.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 2c90818..d2ffbc9 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -59,52 +59,64 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
     service.print_content = function(args) {
         service.fleshPrintScope(args.scope);
 
+        var promise = egHatch.hatchAvailable ?
+            service.print_via_hatch(args) :
+            service.print_via_browser(args);
+
+        return promise['finally'](
+            function() { service.clear_print_content() });
+    }
+
+    service.print_via_hatch = function(args) {
         var promise;
-        if (args.content_type == 'text/html') {
 
-            // all HTML content is assumed to require compilation, 
-            // regardless of the print destination
+        if (args.content_type == 'text/html') {
             promise = service.ingest_print_content(
                 args.content_type, args.content, args.scope);
-
         } else {
-            // text content does not require compilation for remote printing
+            // text content requires no compilation for remote printing.
             promise = $q.when(args.content);
         }
 
-        var context = args.context || 'default';
-
         return promise.then(function(html) {
-
-            return egHatch.remotePrint(context,
-                args.content_type, html, args.show_dialog)['catch'](
-
-                function(msg) {
-                    // remote print not available; 
-
-                    if (egHatch.hatchRequired()) {
-                        console.error("Unable to print data; "
-                         + "hatchRequired=true, but hatch is not connected");
-                         return $q.reject();
-                    }
-
-                    if (args.content_type != 'text/html') {
-                        // text content does require compilation 
-                        // (absorption) for browser printing
-                        return service.ingest_print_content(
-                            args.content_type, args.content, args.scope
-                        ).then(function() { $window.print(); service.clear_print_content(); });
-                    } else {
-                        // HTML content is already ingested and accessible
-                        // within the page to the printer.  
-                        $window.print();
-                        service.clear_print_content();
-                    }
-                }
+            return egHatch.remotePrint(
+                args.context || 'default',
+                args.content_type, 
+                html, 
+                args.show_dialog
             );
         });
     }
 
+    service.print_via_browser = function(args) {
+        var type = args.content_type;
+        var content = args.content;
+        var printScope = args.scope;
+
+        if (type == 'text/csv' || type == 'text/plain') {
+            // preserve newlines, spaces, etc.
+            content = '<pre>' + content + '</pre>';
+        }
+
+        // Fetch the print CSS required for in-browser printing.
+        return $http.get(egEnv.basePath + 'css/print.css')
+        .then(function(response) {
+
+            // Add the bare CSS to the content
+            return '<style type="text/css" media="print">' +
+                  response.data +
+                  '</style>' +
+                  content;
+
+        }).then(function(content) {
+            // Ingest the content into the page DOM.
+            return service.ingest_print_content(type, content, printScope);
+
+        }).then(function() { 
+            $window.print();
+        });
+    }
+
     // loads an HTML print template by name from the server
     // If no template is available in local/hatch storage, 
     // fetch the template as an HTML file from the server.
@@ -163,14 +175,14 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
  * The div housing eg-print-container must apply the correct
  * print media CSS to ensure this content (and not the rest
  * of the page) is printed.
+ *
+ * NOTE: There should only ever be 1 egPrintContainer instance per page.
+ * egPrintContainer attaches functions to the egPrint service with
+ * closures around the egPrintContainer instance's $scope (including its
+ * DOM element). Having multiple egPrintContainers could result in chaos.
  */
 
-// FIXME: only apply print CSS when print commands are issued via the 
-// print container, otherwise using the browser's native print page 
-// option will always result in empty pages.  Move the print CSS
-// out of the standalone CSS file and put it into a template file
-// for this directive.
-.directive('egPrintContainer', ['$compile', '$http', function($compile, $http) {
+.directive('egPrintContainer', ['$compile', function($compile) {
     return {
         restrict : 'AE',
         scope : {}, // isolate our scope
@@ -186,29 +198,13 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
                     $compile($scope.elm.contents())($scope.$new(true));
                 }
 
+                // Insert the printable content into the DOM.
+                // For remote printing, this lets us exract the compiled HTML
+                // from the DOM.
+                // For local printing, this lets us print directly from the
+                // DOM with print CSS.
+                // Returns a promise reolved with the compiled HTML as a string.
                 egPrint.ingest_print_content = function(type, content, printScope) {
-
-                    if (type == 'text/csv' || type == 'text/plain') {
-                        // preserve newlines, spaces, etc.
-                        content = '<pre>' + content + '</pre>';
-                    }
-
-                    return $http.get(egEnv.basePath + 'css/print.css').then(
-                        function(response) {
-                            content = '<style type="text/css" media="print">' +
-                                      response.data +
-                                      '</style>' +
-                                      content;
-                            return finish_ingest_print_content(type, content, printScope);
-                        },
-                        function() {
-                            return finish_ingest_print_content(type, content, printScope);
-                        }
-                    );
-
-                }
-
-                function finish_ingest_print_content(type, content, printScope) {
                     $scope.elm.html(content);
 
                     var sub_scope = $scope.$new(true);

commit c649d0e83ec71385492a01803b5ef288d0c615e8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 28 12:12:47 2016 -0500

    LP#1646166 Hatch extension connect via DOM, remote cache
    
    Determine Hatch availability by checking for the presence of a
    well-known Hatch attribute in the DOM, inserted into the page
    by the extension at load time.
    
    Cache get/set values when talking to the Hatch extension (on a per-page
    basis) to reduce unnecessary Hatch I/O.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 c235294..f60e58a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -33,8 +33,10 @@ angular.module('egCoreMod')
     service.messages = {};
     service.pending = [];
     service.hatchAvailable = null;
-    service.cachedPrintConfig = {};
-    service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
+
+    // key/value cache -- avoid unnecessary Hatch extension requests.
+    // Only affects *RemoteItem calls.
+    service.keyCache = {}; 
 
     // write a message to the Hatch port
     service.sendToHatch = function(msg) {
@@ -46,7 +48,7 @@ angular.module('egCoreMod')
             msg2[key] = val;
         });
 
-        console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
+        console.debug("sending to Hatch: " + JSON.stringify(msg2));
 
         msg2.from = 'page';
         $window.postMessage(msg2, $window.location.origin);
@@ -59,20 +61,24 @@ angular.module('egCoreMod')
         msg.msgid = service.msgId++;
         msg.deferred = $q.defer();
 
-        if (service.state == 'NO_CONNECTION') {
+        if (service.hatchAvailable === false) { // Hatch is closed.
             msg.deferred.reject(msg);
 
-        } else if (service.state.match(/CONNECTED|INIT/)) {
-            // Hatch is known to be open
+        } else if (service.hatchAvailable === true) { // Hatch is open.
             service.messages[msg.msgid] = msg;
             service.sendToHatch(msg);
 
-        } else if (service.state == 'IDLE') { 
+        } else { // Hatch state unknown
             service.messages[msg.msgid] = msg;
             service.pending.push(msg);
             $timeout(service.openHatch);
         }
 
+        /*
+        console.debug(
+            Object.keys(service.messages).length + " pending Hatch requests");
+        */
+
         return msg.deferred.promise;
     }
 
@@ -82,8 +88,7 @@ angular.module('egCoreMod')
     service.resolveRequest = function(msg) {
 
         if (!service.messages[msg.msgid]) {
-            console.warn('no cached message for ' 
-                + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
+            console.error('no cached message for id = ' + msg.msgid);
             return;
         }
 
@@ -92,32 +97,12 @@ angular.module('egCoreMod')
         msg.deferred = service.messages[msg.msgid].deferred;
         delete service.messages[msg.msgid]; // un-cache
 
-        switch (service.state) {
-
-            case 'CONNECTED': // received a standard Hatch response
-                if (msg.status == 200) {
-                    msg.deferred.resolve(msg.content);
-                } else {
-                    msg.deferred.reject();
-                    console.warn("Hatch command failed with status=" 
-                        + msg.status + " and message=" + msg.message);
-                }
-                break;
-
-            case 'INIT':
-                if (msg.status == 200) {
-                    service.hatchAvailable = true; // public flag
-                    service.state = 'CONNECTED';
-                    service.hatchOpened();
-                } else {
-                    msg.deferred.reject();
-                    service.hatchWontOpen(msg.message);
-                }
-                break;
-
-            default:
-                console.warn(
-                    "Received message in unexpected state: " + service.state); 
+        if (msg.status == 200) {
+            msg.deferred.resolve(msg.content);
+        } else {
+            console.warn("Hatch command failed with status=" 
+                + msg.status + " and message=" + msg.message);
+            msg.deferred.reject();
         }
     }
 
@@ -138,39 +123,31 @@ angular.module('egCoreMod')
             // We only care about messages from the Hatch extension.
             if (event.data && event.data.from == 'extension') {
 
-                console.debug('Hatch says: ' 
-                    + JSON.stringify(event.data, null, 2));
+                // Avoid logging full Hatch responses. they can get large.
+                console.debug(
+                    'Hatch responded to message ID ' + event.data.msgid);
 
                 service.resolveRequest(event.data);
             }
         }); 
 
-        service.state = 'INIT';
-        service.attemptHatchDelivery({action : 'init'});
+        service.hatchAvailable = true; // public flag
+        service.hatchOpened();
     }
 
     service.hatchWontOpen = function(err) {
         console.debug("Hatch connection failed: " + err);
-        service.state = 'NO_CONNECTION';
         service.hatchAvailable = false;
-        service.hatchClosed();
-    }
-
-    service.hatchClosed = function() {
-        service.printers = [];
-        service.printConfig = {};
         while ( (msg = service.pending.shift()) ) {
             msg.deferred.reject(msg);
             delete service.messages[msg.msgid];
         }
-        if (service.onHatchClose)
-            service.onHatchClose();
     }
 
     // Returns true if Hatch is required or if we are currently
     // communicating with the Hatch service. 
     service.usingHatch = function() {
-        return service.state == 'CONNECTED' || service.hatchRequired();
+        return service.hatchAvailable || service.hatchRequired();
     }
 
     // Returns true if this browser (via localStorage) is 
@@ -206,19 +183,11 @@ angular.module('egCoreMod')
         );
     }
 
-    // 'force' avoids using the config cache
-    service.getPrintConfig = function(context, force) {
-        if (service.cachedPrintConfig[context] && !force) {
-            return $q.when(service.cachedPrintConfig[context])
-        }
-        return service.getRemoteItem('eg.print.config.' + context)
-        .then(function(config) {
-            return service.cachedPrintConfig[context] = config;
-        });
+    service.getPrintConfig = function(context) {
+        return service.getRemoteItem('eg.print.config.' + context);
     }
 
     service.setPrintConfig = function(context, config) {
-        service.cachedPrintConfig[context] = config;
         return service.setRemoteItem('eg.print.config.' + context, config);
     }
 
@@ -253,9 +222,8 @@ angular.module('egCoreMod')
         return service.getRemoteItem(key)['catch'](
             function(msg) {
                 if (service.hatchRequired()) {
-                    console.error("Unable to getItem: " + key
-                     + "; hatchRequired=true, but hatch is not connected");
-                     return null;
+                    console.error("getRemoteItem() failed for key " + key);
+                    return null;
                 }
                 return service.getLocalItem(msg.key);
             }
@@ -263,10 +231,16 @@ angular.module('egCoreMod')
     }
 
     service.getRemoteItem = function(key) {
+        
+        if (service.keyCache[key] != undefined)
+            return $q.when(service.keyCache[key])
+
         return service.attemptHatchDelivery({
             key : key,
             action : 'get'
-        })
+        }).then(function(content) {
+            return service.keyCache[key] = content;
+        });
     }
 
     service.getLocalItem = function(key) {
@@ -295,8 +269,7 @@ angular.module('egCoreMod')
         return service.setRemoteItem(key, value)['catch'](
             function(msg) {
                 if (service.hatchRequired()) {
-                    console.error("Unable to setItem: " + key
-                     + "; hatchRequired=true, but hatch is not connected");
+                    console.error("setRemoteItem() failed for key " + key);
                      return null;
                 }
                 return service.setLocalItem(msg.key, value);
@@ -306,6 +279,7 @@ angular.module('egCoreMod')
 
     // set the value for a stored or new item
     service.setRemoteItem = function(key, value) {
+        service.keyCache[key] = value;
         return service.attemptHatchDelivery({
             key : key, 
             content : value, 
@@ -371,6 +345,7 @@ angular.module('egCoreMod')
     }
 
     service.removeRemoteItem = function(key) {
+        delete service.keyCache[key];
         return service.attemptHatchDelivery({
             key : key,
             action : 'remove'
@@ -407,9 +382,8 @@ angular.module('egCoreMod')
         return service.getRemoteKeys(prefix)['catch'](
             function() { 
                 if (service.hatchRequired()) {
-                    console.error("Unable to get pref keys; "
-                     + "hatchRequired=true, but hatch is not connected");
-                     return [];
+                    console.error("getRemoteKeys() failed");
+                    return [];
                 }
                 return service.getLocalKeys(prefix) 
             }

commit c72b8e318eec3d39d39b52ed307ab09b1bbbba99
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Nov 18 12:13:38 2016 -0500

    LP#1646166 Hatch print configiguration interface
    
    1. Query printers for available options via Hatch.
    2. Allow the user to apply print options on a per-context / per-printer
       basis.
    3. As before, store printer conifugration options via Hatch.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 ce4d670..8f4a34d 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
@@ -16,6 +16,9 @@
   <div class="row"> 
     <div class="col-md-12">
       <h2>[% l('Printer Settings for Remote Printing') %]</h2>
+      <div class="alert alert-warning" ng-show="hatchNotConnected()">
+        [% l("Hatch is not connected") %]
+      </div>
     </div>
   </div>
 
@@ -71,10 +74,274 @@
                   value="{{printConfig[context].printer}}">
               </div><!-- /input-group -->
             </div><!-- col -->
+            <div class="col-md-4">
+              <span class="pad-right-min">
+                <button class="btn btn-warning" 
+                  ng-click="resetPrinterSettings(context)">
+                  [% l('Reset Form') %]
+                </button>
+              </span>
+              <button class="btn btn-success" 
+                ng-click="savePrinterSettings(context)">
+                [% l('Apply Changes') %]
+              </button>
+            </div>
           </div><!-- row -->
+
           <div class="row" ng-hide="isTestView"> 
+            <div class="col-md-10">
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Print Color') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].printColor"
+                    ng-options="val for val in printerOptions.printColor | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPrintColor}}') %]</span>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Paper Source') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].paperSource"
+                    ng-options="val for val in printerOptions.paperSource | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPaperSource}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Paper Type') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].paper"
+                    ng-options="val for val in printerOptions.paper | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPaper}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Page Orientation') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].pageOrientation"
+                    ng-options="val for val in printerOptions.pageOrientation | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPageOrientation}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Collation') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].collation"
+                    ng-options="val for val in printerOptions.collation | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultCollation}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Print Quality') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].printQuality"
+                    ng-options="val for val in printerOptions.printQuality | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPrintQuality}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Print Sides') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].printSides"
+                    ng-options="val for val in printerOptions.printSides | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultPrintSides}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2">
+                  <label>[% l('Number of Copies') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <input type="text" size="4" class="form-control"
+                    ng-model="printConfig[context].copies"/>
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultCopies}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1">
+                  <input type="radio" name="margins" 
+                    ng-click="printConfig[context].autoMargins=true"
+                    ng-checked="printConfig[context].autoMargins"/>
+                </div>
+                <div class="col-md-2">
+                  <label>[% l('Automatic Margins') %]</label>
+                </div>
+                <div class="col-md-4">
+                  <select
+                    class="form-control"
+                    ng-model="printConfig[context].marginType"
+                    ng-disabled="!printConfig[context].autoMargins" 
+                    ng-options="val for val in printerOptions.marginType | orderBy:'val'">
+                  </select>  
+                </div>
+                <div class="col-md-3">
+                  <span>[% l('Default: [_1]', 
+                    '{{printerOptions.defaultMarginType}}') %]</span>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1">
+                  <input type="radio" name="margins"
+                    ng-click="printConfig[context].autoMargins=false"
+                    ng-checked="!printConfig[context].autoMargins"/>
+                </div>
+                <div class="col-md-2">
+                  <label>[% l('Manual Margins') %]</label>
+                </div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('Left') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].autoMargins"
+                      ng-model="printConfig[context].leftMargin"/>
+                  </div>
+                </div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('Top') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].autoMargins"
+                      ng-model="printConfig[context].topMargin"/>
+                  </div>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-3"></div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('Right') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].autoMargins"
+                      ng-model="printConfig[context].rightMargin"/>
+                  </div>
+                </div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('Bottom') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].autoMargins"
+                      ng-model="printConfig[context].bottomMargin"/>
+                  </div>
+                </div>
+              </div>
+
+              <div class="row">
+                <div class="col-md-1"></div>
+                <div class="col-md-2"><label>[% l('Page Ranges') %]</label></div>
+                <div class="col-md-2">
+                  [% l('All Pages') %]
+                  <input type='radio' name='pageRanges' 
+                    ng-checked="printConfig[context].allPages"
+                    ng-click="printConfig[context].allPages=true"/>
+                </div>
+                <div class="col-md-2">
+                  [% l('Page Range') %]
+                  <input type='radio' name='pageRanges' 
+                    ng-checked="!printConfig[context].allPages"
+                    ng-click="printConfig[context].allPages=false"/>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-3"></div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('Start') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].allPages"
+                      ng-model="printConfig[context].pageRanges[0]"/>
+                  </div>
+                </div>
+                <div class="col-md-2">
+                  <div class="input-group">
+                    <span class="input-group-addon">[% l('End') %]</span>
+                    <input type="text" class="form-control"
+                      ng-disabled="printConfig[context].allPages"
+                      ng-model="printConfig[context].pageRanges[1]"/>
+                  </div>
+                </div>
+                <!-- TODO: support multiple page ranges by 
+                    dynamically adding additional pageRanges[X] pairs -->
+              </div>
+            </div><!-- col -->
+          </div><!-- row -->
+
+          <div class="row" ng-hide="isTestView">
             <div class="col-md-12">
-              <h2>[% l('Compiled Printer Settings') %]</h2>
+              <h3>[% l('Compiled Printer Settings') %]</h3>
               <pre>{{printerConfString()}}</pre>
             </div><!-- col -->
           </div><!-- row -->
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index 93881b6..4e6f962 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -128,6 +128,7 @@ table.list tr.selected td { /* deprecated? */
 .pad-vert {padding : 20px 0px 10px 0px;}
 .pad-left {padding-left: 10px;}
 .pad-right {padding-right: 10px;}
+.pad-right-min {padding-right: 5px;}
 .pad-all-min {padding : 5px; }
 .pad-all-min2 {padding : 2px; }
 .pad-all {padding : 10px; }
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 444f920..310356a 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
@@ -220,18 +220,18 @@ function($scope , $window , $location , egCore , egConfirmDialog) {
 .controller('PrintConfigCtrl',
        ['$scope','egCore',
 function($scope , egCore) {
-    console.log('PrintConfigCtrl');
-
-    $scope.actionPending = false;
-    $scope.isTestView = false;
 
+    $scope.printConfig = {};
     $scope.setContext = function(ctx) { 
         $scope.context = ctx; 
         $scope.isTestView = false;
-        $scope.actionPending = false;
     }
     $scope.setContext('default');
 
+    $scope.hatchNotConnected = function() {
+        return !egCore.hatch.hatchAvailable;
+    }
+
     $scope.getPrinterByAttr = function(attr, value) {
         var printer;
         angular.forEach($scope.printers, function(p) {
@@ -240,50 +240,48 @@ function($scope , egCore) {
         return printer;
     }
 
-    $scope.currentPrinter = function() {
-        if ($scope.printConfig && $scope.printConfig[$scope.context]) {
-            return $scope.getPrinterByAttr(
-                'name', $scope.printConfig[$scope.context].printer
-            );
-        }
-    }
-
     // fetch info on all remote printers
     egCore.hatch.getPrinters()
     .then(function(printers) { 
         $scope.printers = printers;
-        $scope.defaultPrinter = 
-            $scope.getPrinterByAttr('is-default', true);
-    })
-    .then(function() { return egCore.hatch.getPrintConfig() })
-    .then(function(config) {
-        $scope.printConfig = config;
-
-        var pname = '';
-        if ($scope.defaultPrinter) {
-            pname = $scope.defaultPrinter.name;
-
-        } else if ($scope.printers.length == 1) {
-            // if the OS does not report a default printer, but only
-            // one printer is available, treat it as the default.
-            pname = $scope.printers[0].name;
-        }
 
-        // apply the default printer to every context which has
-        // no printer configured.
+        var def = $scope.getPrinterByAttr('is-default', true);
+        if (!def && printers.length) def = printers[0];
+
+        if (def) {
+            $scope.defaultPrinter = def;
+            loadPrinterOptions(def.name);
+        }
+    }).then(function() {
         angular.forEach(
             ['default','receipt','label','mail','offline'],
             function(ctx) {
-                if (!$scope.printConfig[ctx]) {
-                    $scope.printConfig[ctx] = {
-                        context : ctx,
-                        printer : pname
+                egCore.hatch.getPrintConfig(ctx).then(function(conf) {
+                    if (conf) {
+                        $scope.printConfig[ctx] = conf;
+                    } else {
+                        $scope.resetPrinterSettings(ctx);
                     }
-                }
+                });
             }
         );
     });
 
+    $scope.resetPrinterSettings = function(context) {
+        $scope.printConfig[context] = {
+            context : context,
+            printer : $scope.defaultPrinter ? $scope.defaultPrinter.name : null,
+            autoMargins : true, 
+            allPages : true,
+            pageRanges : []
+        };
+    }
+
+    $scope.savePrinterSettings = function(context) {
+        return egCore.hatch.setPrintConfig(
+            context, $scope.printConfig[context]);
+    }
+
     $scope.printerConfString = function() {
         if ($scope.printConfigError) return $scope.printConfigError;
         if (!$scope.printConfig) return;
@@ -292,24 +290,14 @@ function($scope , egCore) {
             $scope.printConfig[$scope.context], undefined, 2);
     }
 
-    $scope.resetConfig = function() {
-        $scope.actionPending = true;
-        $scope.printConfigError = null;
-        $scope.printConfig[$scope.context] = {
-            context : $scope.context
-        }
-        
-        if ($scope.defaultPrinter) {
-            $scope.printConfig[$scope.context].printer = 
-                $scope.defaultPrinter.name;
-        }
-
-        egCore.hatch.setPrintConfig($scope.printConfig)
-        .finally(function() {$scope.actionPending = false});
+    function loadPrinterOptions(name) {
+        egCore.hatch.getPrinterOptions(name).then(
+            function(options) {$scope.printerOptions = options});
     }
 
     $scope.setPrinter = function(name) {
         $scope.printConfig[$scope.context].printer = name;
+        loadPrinterOptions(name);
     }
 
     // for testing
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 c38920e..c235294 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -33,6 +33,7 @@ angular.module('egCoreMod')
     service.messages = {};
     service.pending = [];
     service.hatchAvailable = null;
+    service.cachedPrintConfig = {};
     service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
 
     // write a message to the Hatch port
@@ -188,31 +189,15 @@ angular.module('egCoreMod')
         };
     }
 
-    service.getPrintConfig = function() {
-        if (service.printConfig) 
-            return $q.when(service.printConfig);
-
-        return service.getRemoteItem('eg.print.config')
-        .then(function(conf) { 
-            return (service.printConfig = conf || {}) 
-        });
-    }
-
-    service.setPrintConfig = function(conf) {
-        service.printConfig = conf;
-        return service.setRemoteItem('eg.print.config', conf);
-    }
-
-
     service.remotePrint = function(
         context, contentType, content, withDialog) {
 
-        return service.getPrintConfig().then(
-            function(conf) {
+        return service.getPrintConfig(context).then(
+            function(config) {
                 // print configuration retrieved; print
                 return service.attemptHatchDelivery({
                     action : 'print',
-                    config : conf[context],
+                    settings : config,
                     content : content, 
                     contentType : contentType,
                     showDialog : withDialog,
@@ -221,43 +206,27 @@ angular.module('egCoreMod')
         );
     }
 
-    // launch the print dialog then attach the resulting configuration
-    // to the requested context, then store the final values.
-    service.configurePrinter = function(context, printer) {
-
-        // load current settings
-        return service.getPrintConfig()
-
-        // dispatch the print configuration request
+    // 'force' avoids using the config cache
+    service.getPrintConfig = function(context, force) {
+        if (service.cachedPrintConfig[context] && !force) {
+            return $q.when(service.cachedPrintConfig[context])
+        }
+        return service.getRemoteItem('eg.print.config.' + context)
         .then(function(config) {
+            return service.cachedPrintConfig[context] = config;
+        });
+    }
 
-            // loaded remote config
-            if (!config[context]) config[context] = {};
-            config[context].printer = printer;
-            return service.attemptHatchDelivery({
-                key : 'no-op', 
-                action : 'print-config',
-                config : config[context]
-            })
-        })
-
-        // set the returned settings to the requested context
-        .then(function(newconf) {
-            if (angular.isObject(newconf)) {
-                newconf.printer = printer;
-                return service.printConfig[context] = newconf;
-            } else {
-                console.warn("configurePrinter() returned " + newconf);
-            }
-        })
-
-        // store the newly linked settings
-        .then(function() {
-            service.setItem('eg.print.config', service.printConfig);
-        })
+    service.setPrintConfig = function(context, config) {
+        service.cachedPrintConfig[context] = config;
+        return service.setRemoteItem('eg.print.config.' + context, config);
+    }
 
-        // return the final settings to the caller
-        .then(function() {return service.printConfig});
+    service.getPrinterOptions = function(name) {
+        return service.attemptHatchDelivery({
+            action : 'printer-options',
+            printer : name
+        });
     }
 
     service.getPrinters = function() {

commit c2254e85d0c958bf8321f916e52bc964d90ae8ee
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 14 12:58:33 2016 -0500

    LP#1640255 Hatch native messaging extension
    
    Replaces Hatch Websockets communication layer with browser extension-
    based communication.
    
    Hatch API remains the same with 2 notable exceptions:
    
    1. appendItem() API call has been removed.  It did not work as designed
       and (thus far) has served no purpose.  It was originally intended for
       offline data storage, but that will probably require something a
       little smarter.
    
    2. The printer configuration API is no more.  This will be replaced with
       an in-app configuration page.  Note, this does not prevent use of the
       printer dialog, it only means settings are not collected from the
       printer dialog.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 ef5dafb..ce4d670 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
@@ -71,24 +71,6 @@
                   value="{{printConfig[context].printer}}">
               </div><!-- /input-group -->
             </div><!-- col -->
-            <div class="col-md-6">
-              <div class="input-group">
-                <div class="input-group-btn">
-                  <button type="button" 
-                    ng-click="configurePrinter()"
-                    ng-class="{disabled : actionPending || !printers[0]}"
-                    class="btn btn-default btn-success">
-                      [% l('Configure Printer') %]
-                  </button>
-                  <button type="button" 
-                    ng-click="resetConfig()"
-                    ng-class="{disabled : actionPending}"
-                    class="btn btn-default btn-warning">
-                      [% l('Reset Configuration') %]
-                  </button>
-                </div>
-              </div>
-            </div>
           </div><!-- row -->
           <div class="row" ng-hide="isTestView"> 
             <div class="col-md-12">
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
index ab77e66..bb80cc2 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
@@ -12,15 +12,6 @@
       </div>
     </div><!-- row -->
   </div>
-  <div class="row">
-    <div class="col-md-6">
-      <input type='text' class='form-control'  
-        ng-disabled="!hatchRequired || !userHasRegPerm"
-        title="[% l('Hatch URL') %]"
-        placeholder="[% l('Hatch URL') %]"
-        ng-change='updateHatchURL()' ng-model='hatchURL'/>
-    </div>
-  </div>
 
 
   <div class="row new-entry">
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 9723c021..444f920 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
@@ -167,7 +167,6 @@ function($scope , $window , $location , egCore , egConfirmDialog) {
 
     // ---------------------
     // Hatch Configs
-    $scope.hatchURL = egCore.hatch.hatchURL();
     $scope.hatchRequired = 
         egCore.hatch.getLocalItem('eg.hatch.required');
 
@@ -176,11 +175,6 @@ function($scope , $window , $location , egCore , egConfirmDialog) {
             'eg.hatch.required', $scope.hatchRequired);
     }
 
-    $scope.updateHatchURL = function() {
-        egCore.hatch.setLocalItem(
-            'eg.hatch.url', $scope.hatchURL);
-    }
-
     egCore.hatch.getItem('eg.audio.disable').then(function(val) {
         $scope.disable_sound = val;
     });
@@ -314,20 +308,6 @@ function($scope , egCore) {
         .finally(function() {$scope.actionPending = false});
     }
 
-    $scope.configurePrinter = function() {
-        $scope.printConfigError = null;
-        $scope.actionPending = true;
-        egCore.hatch.configurePrinter(
-            $scope.context,
-            $scope.printConfig[$scope.context].printer
-        )
-        .then(
-            function(config) {$scope.printConfig = config},
-            function(error) {$scope.printConfigError = error}
-        )
-        .finally(function() {$scope.actionPending = false});
-    }
-
     $scope.setPrinter = function(name) {
         $scope.printConfig[$scope.context].printer = 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 ff517fc..c38920e 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -29,14 +29,13 @@ angular.module('egCoreMod')
     function($q , $window , $timeout , $interpolate , $http , $cookies) {
 
     var service = {};
-    service.msgId = 0;
+    service.msgId = 1;
     service.messages = {};
     service.pending = [];
-    service.socket = null;
     service.hatchAvailable = null;
-    service.defaultHatchURL = 'wss://localhost:8443/hatch'; 
+    service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
 
-    // write a message to the Hatch websocket
+    // write a message to the Hatch port
     service.sendToHatch = function(msg) {
         var msg2 = {};
 
@@ -47,7 +46,9 @@ angular.module('egCoreMod')
         });
 
         console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
-        service.socket.send(JSON.stringify(msg2));
+
+        msg2.from = 'page';
+        $window.postMessage(msg2, $window.location.origin);
     }
 
     // Send the request to Hatch if it's available.  
@@ -57,18 +58,18 @@ angular.module('egCoreMod')
         msg.msgid = service.msgId++;
         msg.deferred = $q.defer();
 
-        if (service.hatchAvailable === false) { // Hatch is closed
+        if (service.state == 'NO_CONNECTION') {
             msg.deferred.reject(msg);
 
-        } else if (service.hatchAvailable === true) { // Hatch is open
+        } else if (service.state.match(/CONNECTED|INIT/)) {
             // Hatch is known to be open
             service.messages[msg.msgid] = msg;
             service.sendToHatch(msg);
 
-        } else {  // Hatch status unknown; attempt to connect
+        } else if (service.state == 'IDLE') { 
             service.messages[msg.msgid] = msg;
             service.pending.push(msg);
-            service.hatchConnect();
+            $timeout(service.openHatch);
         }
 
         return msg.deferred.promise;
@@ -90,18 +91,71 @@ angular.module('egCoreMod')
         msg.deferred = service.messages[msg.msgid].deferred;
         delete service.messages[msg.msgid]; // un-cache
 
-        // resolve / reject
-        if (msg.error) {
-            throw new Error(
-            "egHatch command failed : " 
-                + JSON.stringify(msg.error, null, 2));
-        } else {
-            msg.deferred.resolve(msg.content);
-        } 
+        switch (service.state) {
+
+            case 'CONNECTED': // received a standard Hatch response
+                if (msg.status == 200) {
+                    msg.deferred.resolve(msg.content);
+                } else {
+                    msg.deferred.reject();
+                    console.warn("Hatch command failed with status=" 
+                        + msg.status + " and message=" + msg.message);
+                }
+                break;
+
+            case 'INIT':
+                if (msg.status == 200) {
+                    service.hatchAvailable = true; // public flag
+                    service.state = 'CONNECTED';
+                    service.hatchOpened();
+                } else {
+                    msg.deferred.reject();
+                    service.hatchWontOpen(msg.message);
+                }
+                break;
+
+            default:
+                console.warn(
+                    "Received message in unexpected state: " + service.state); 
+        }
+    }
+
+    service.openHatch = function() {
+
+        // When the Hatch extension loads, it tacks an attribute onto
+        // the page body to indicate it's available.
+
+        if (!$window.document.body.getAttribute('hatch-is-open')) {
+            service.hatchWontOpen('Hatch is not available');
+            return;
+        }
+
+        $window.addEventListener("message", function(event) {
+            // We only accept messages from our own content script.
+            if (event.source != window) return;
+
+            // We only care about messages from the Hatch extension.
+            if (event.data && event.data.from == 'extension') {
+
+                console.debug('Hatch says: ' 
+                    + JSON.stringify(event.data, null, 2));
+
+                service.resolveRequest(event.data);
+            }
+        }); 
+
+        service.state = 'INIT';
+        service.attemptHatchDelivery({action : 'init'});
+    }
+
+    service.hatchWontOpen = function(err) {
+        console.debug("Hatch connection failed: " + err);
+        service.state = 'NO_CONNECTION';
+        service.hatchAvailable = false;
+        service.hatchClosed();
     }
 
     service.hatchClosed = function() {
-        service.socket = null;
         service.printers = [];
         service.printConfig = {};
         while ( (msg = service.pending.shift()) ) {
@@ -112,15 +166,10 @@ angular.module('egCoreMod')
             service.onHatchClose();
     }
 
-    service.hatchURL = function() {
-        return service.getLocalItem('eg.hatch.url') 
-            || service.defaultHatchURL;
-    }
-
     // Returns true if Hatch is required or if we are currently
     // communicating with the Hatch service. 
     service.usingHatch = function() {
-        return service.hatchAvailable || service.hatchRequired();
+        return service.state == 'CONNECTED' || service.hatchRequired();
     }
 
     // Returns true if this browser (via localStorage) is 
@@ -129,58 +178,14 @@ angular.module('egCoreMod')
         return service.getLocalItem('eg.hatch.required');
     }
 
-    service.hatchConnect = function() {
-
-        if (service.socket && 
-            service.socket.readyState == service.socket.CONNECTING) {
-            // connection in progress.  Nothing to do.  Our queued
-            // message will be delivered when onopen() fires
-            return;
-        }
-
-        try {
-            service.socket = new WebSocket(service.hatchURL());
-        } catch(e) {
-            service.hatchAvailable = false;
-            service.hatchClosed();
-            return;
-        }
-
-        service.socket.onopen = function() {
-            console.debug('connected to Hatch');
-            service.hatchAvailable = true;
-            if (service.onHatchOpen) 
-                service.onHatchOpen();
-            while ( (msg = service.pending.shift()) ) {
-                service.sendToHatch(msg);
-            };
-        }
-
-        service.socket.onclose = function() {
-            if (service.hatchAvailable === false) return; // already registered
-
-            // onclose() will be called regularly as we disconnect from
-            // Hatch via timeouts.  Return hatchAvailable to its unknow state
-            service.hatchAvailable = null;
-            service.hatchClosed();
-        }
-
-        service.socket.onerror = function() {
-            if (service.hatchAvailable === false) return; // already registered
-            service.hatchAvailable = false;
-            console.debug(
-                "unable to connect to Hatch server at " + service.hatchURL());
-            service.hatchClosed();
-        }
-
-        service.socket.onmessage = function(evt) {
-            var msgStr = evt.data;
-            if (!msgStr) throw new Error("Hatch returned empty message");
+    service.hatchOpened = function() {
+        // let others know we're connected
+        if (service.onHatchOpen) service.onHatchOpen();
 
-            var msgObj = JSON.parse(msgStr);
-            console.debug('Hatch says ' + JSON.stringify(msgObj, null, 2));
-            service.resolveRequest(msgObj); 
-        }
+        // Deliver any previously queued requests 
+        while ( (msg = service.pending.shift()) ) {
+            service.sendToHatch(msg);
+        };
     }
 
     service.getPrintConfig = function() {
@@ -291,8 +296,8 @@ angular.module('egCoreMod')
     service.getRemoteItem = function(key) {
         return service.attemptHatchDelivery({
             key : key,
-            action : 'get', 
-        });
+            action : 'get'
+        })
     }
 
     service.getLocalItem = function(key) {
@@ -318,16 +323,14 @@ angular.module('egCoreMod')
      * tmp values are removed during logout or browser close.
      */
     service.setItem = function(key, value) {
-        var str = JSON.stringify(value);
-
-        return service.setRemoteItem(key, str)['catch'](
+        return service.setRemoteItem(key, value)['catch'](
             function(msg) {
                 if (service.hatchRequired()) {
                     console.error("Unable to setItem: " + key
                      + "; hatchRequired=true, but hatch is not connected");
                      return null;
                 }
-                return service.setLocalItem(msg.key, null, str);
+                return service.setLocalItem(msg.key, value);
             }
         );
     }
@@ -336,7 +339,7 @@ angular.module('egCoreMod')
     service.setRemoteItem = function(key, value) {
         return service.attemptHatchDelivery({
             key : key, 
-            value : value, 
+            content : value, 
             action : 'set',
         });
     }
@@ -374,29 +377,6 @@ angular.module('egCoreMod')
         $window.sessionStorage.setItem(key, jsonified);
     }
 
-    // appends the value to the existing item stored at key.
-    // If not item is found at key, this behaves just like setItem()
-    service.appendItem = function(key, value) {
-        return service.appendRemoteItem(key, value)['catch'](
-            function(msg) {
-                if (service.hatchRequired()) {
-                    console.error("Unable to appendItem: " + key
-                     + "; hatchRequired=true, but hatch is not connected");
-                     return null;
-                }
-                service.appendLocalItem(msg.key, msg.value);
-            }
-        );
-    }
-
-    service.appendRemoteItem = function(key, value) {
-        return service.attemptHatchDelivery({
-            key : key, 
-            value : value, 
-            action : 'append',
-        });
-    }
-
     // assumes the appender and appendee are both strings
     // TODO: support arrays as well
     service.appendLocalItem = function(key, value) {

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

Summary of changes:
 .../templates/staff/admin/workstation/index.tt2    |    2 +
 .../templates/staff/admin/workstation/t_hatch.tt2  |   62 +++
 .../staff/admin/workstation/t_print_config.tt2     |  301 ++++++++++++-
 .../templates/staff/admin/workstation/t_splash.tt2 |  239 +++++------
 .../src/templates/staff/circ/checkin/t_checkin.tt2 |    4 +-
 .../src/templates/staff/circ/patron/t_checkout.tt2 |    4 +-
 Open-ILS/src/templates/staff/css/style.css.tt2     |    1 +
 .../js/ui/default/staff/admin/workstation/app.js   |  212 +++++-----
 .../web/js/ui/default/staff/circ/checkin/app.js    |    2 +-
 .../js/ui/default/staff/circ/patron/checkout.js    |    2 +-
 Open-ILS/web/js/ui/default/staff/services/hatch.js |  451 ++++++++++----------
 Open-ILS/web/js/ui/default/staff/services/print.js |  133 +++---
 12 files changed, 866 insertions(+), 547 deletions(-)
 create mode 100644 Open-ILS/src/templates/staff/admin/workstation/t_hatch.tt2


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list