web staff : initial patron search grid integration
authorBill Erickson <berick@esilibrary.com>
Sun, 6 Apr 2014 19:57:02 +0000 (15:57 -0400)
committerBill Erickson <berick@esilibrary.com>
Sun, 6 Apr 2014 19:57:02 +0000 (15:57 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/circ/patron/t_search.tt2
Open-ILS/src/templates/staff/circ/patron/t_search_results.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/services/grid.js

index 076ba8e..c5fab21 100644 (file)
@@ -6,7 +6,8 @@
 %]
 
 [% BLOCK APP_JS %]
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script><!-- remove me -->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/app.js"></script>
index 601af0d..02d3a43 100644 (file)
       </div>
     </form>
   </div>
-  <div class="col-md-1 text-right">
-    [% INCLUDE 'staff/circ/patron/t_search_actions.tt2' %]
-  </div>
 </div>
 
 
 <br/>
 <div class="row">
   <div class="col-md-12">
+<!--
     <div class="alert alert-info alert-dismissable" 
       ng-hide="tips.dismissed('circ.patron.search')">
       <button type="button" class="close" data-dismiss="alert" aria-hidden="true" 
         <li>Middle-click to open a patron in a new browser tab.</li>
       </ol>
     </div> 
+-->
     [% INCLUDE 'staff/circ/patron/t_search_results.tt2' %]
   </div>
 </div>
index 2200316..1c5bf86 100644 (file)
@@ -1,77 +1,28 @@
-[% 
-# Default / available display columns
-# Since there will be demand for configurable columns in this UI,
-# experiment with automagic column creation. 
-#
-# We could autogenerate much of this from the IDL. However, since there
-# are special cases to handle (e.g. billing vs mailing address) and
-# because table autogeneration will likely evolve over time, go ahead
-# and list the columns explicitly for now.
-#
-# the 'name' field doubles as the path to the value and as a unique
-# key for the column picker
-COLUMNS = [
+<eg-grid
+  idl-class="au" sort="[]" id-field="id"
+  main-label="[% l('Patron Search Resuts') %]"
+  items-provider="patronSearchGridProvider"
+  persist-key="eg.staff.circ.patron.search">
+  <eg-grid-field label="[% ('ID') %]" path='id'></eg-grid-field>
+  <eg-grid-field label="[% ('Card') %]" path='card.barcode'></eg-grid-field>
+  <eg-grid-field label="[% ('Last Name') %]" path='family_name'></eg-grid-field>
+  <eg-grid-field label="[% ('First Name') %]" path='first_given_name'></eg-grid-field>
+  <eg-grid-field label="[% ('Middle Name') %]" path='second_given_name'></eg-grid-field>
+  <eg-grid-field label="[% ('DoB') %]" path='dob'></eg-grid-field>
+  <eg-grid-field label="[% ('Home Library') %]" path='home_ou.shortname'></eg-grid-field>
+  <eg-grid-field label="[% ('Created On') %]" path='create_date'></eg-grid-field>
 
-{label => l('ID'),          name => 'id',               display => 1},
-{label => l('Card'),        name => 'card.barcode',     display => 1},
-{label => l('Last Name'),   name => 'family_name',      display => 1},
-{label => l('First Name'),  name => 'first_given_name', display => 1},
-{label => l('Middle Name'), name => 'second_given_name',display => 1},
-{label => l('DoB'),         name => 'dob',              display => 1},
-{label => l('Home Library'),name => 'home_ou.shortname',display => 1},
-{label => l('Created On'),  name => 'create_date',      display => 1},
-
-{label => l('Mailing:Street 1'), name => 'mailing_address.street1', display => 1},
-{label => l('Mailing:Street 2'), name => 'mailing_address.street2'},
-{label => l('Mailing:City'),     name => 'mailing_address.city'},
-{label => l('Mailing:County'),   name => 'mailing_address.county'},
-{label => l('Mailing:State'),    name => 'mailing_address.state'},
-{label => l('Mailing:Zip'),      name => 'mailing_address.post_code'},
-
-{label => l('Billing:Street 1'), name => 'billing_address.street1'},
-{label => l('Billing:Street 2'), name => 'billing_address.street2'},
-{label => l('Billing:City'),     name => 'billing_address.city'},
-{label => l('Billing:County'),   name => 'billing_address.county'},
-{label => l('Billing:State'),    name => 'billing_address.state'},
-{label => l('Billing:Zip'),      name => 'billing_address.post_code'}
-
-]
-%]
-
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-patrons.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-])">
-</div>
-
-<table class="list table table-hover table-condensed">
-  <thead>
-    <tr>
-      <th>#</th>
-      <th><a href='' ng-click="patrons.toggleSelectAll()">&#x2713;</a></th>
-      <th ng-repeat="col in patrons.allColumns" 
-        ng-show="patrons.displayColumns[col.name]">
-        {{col.label}}
-      </th>
-    </tr>
-  </thead>
-  <!-- giving tbody a tabindex allows the keyup event to fire.
-    requires outline:none to prevent selected element CSS bordering -->
-  <tbody _tabindex="-1" _style="outline:none" _ng-keyup="navigateResults($event)">
-    <tr ng-repeat="user in patrons.items track by $index"
-        ng-click="onPatronClick($event, user)"
-        ng-dblclick="onPatronDblClick($event, user)"
-        ng-class="{selected : patrons.selected[user.id()]}">
-      <td>{{$index + 1}}</td>
-      <td><span ng-if="patrons.selected[user.id()]">&#x2713;</span> 
-      <td ng-repeat="col in patrons.allColumns" 
-        ng-show="patrons.displayColumns[col.name]">
-        {{patrons.fieldValue(user, col.name)}}
-      </td>
-    </tr>
-  </tbody>
-</table>
+  <eg-grid-field label="[% ('Mailing:Street 1') %]" path='mailing_address.street1'></eg-grid-field>
+  <eg-grid-field label="[% ('Mailing:Street 2') %]" path='mailing_address.street2' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Mailing:City') %]" path='mailing_address.city' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Mailing:County') %]" path='mailing_address.county' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Mailing:State') %]" path='mailing_address.state' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Mailing:Zip') %]" path='mailing_address.post_code' display="false"></eg-grid-field>
 
+  <eg-grid-field label="[% ('Billing:Street 1') %]" path='billing_address.street1' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Billing:Street 2') %]" path='billing_address.street2' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Billing:City') %]" path='billing_address.city' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Billing:County') %]" path='billing_address.county' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Billing:State') %]" path='billing_address.state' display="false"></eg-grid-field>
+  <eg-grid-field label="[% ('Billing:Zip') %]" path='billing_address.post_code' display="false"></eg-grid-field>
+</eg-grid>
index 0d8c02b..00ff7e9 100644 (file)
@@ -8,10 +8,11 @@
  */
 
 angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 
-    'egCoreMod', 'egUiMod', 'egListMod', 'egUserMod'])
+    'egCoreMod', 'egUiMod', 'egGridMod', 'egListMod', 'egUserMod'])
 
-.config(function($routeProvider, $locationProvider) {
+.config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(blob):/); // grid export
 
     // data loaded at startup which only requires an authtoken goes
     // here. this allows the requests to be run in parallel instead of
@@ -230,12 +231,27 @@ function($scope,  $q,  $filter,  egNet,  egAuth,  egUser,  patronSvc,  egEnv,  e
 .controller('PatronSearchCtrl',
        ['$scope','$q','$routeParams','$timeout','$window','$location','egEnv',
        '$filter','egIDL','egNet','egAuth','egEvent','egList','egUser','patronSvc',
+       'egGridFlatDataProvider',
 function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egEnv,
-        $filter,  egIDL,  egNet,  egAuth,  egEvent,  egList,  egUser,  patronSvc) {
+        $filter,  egIDL,  egNet,  egAuth,  egEvent,  egList,  egUser,  patronSvc,
+        egGridFlatDataProvider) {
 
     $scope.initTab('search');
     $scope.focusMe = true;
     $scope.patrons = patronSvc.patrons;
+    
+    // our data provider is a modified flat data provider
+    var provider = egGridFlatDataProvider.instance({});
+    provider.get = function(index, count, onitem) {
+        angular.forEach(
+            $scope.patrons.items.slice(index, index + count),
+            function(item) { onitem(item) }
+        );
+    };
+    provider.itemFieldValue = function(item, column) {
+        return provider.nestedItemFieldValue(item, column);
+    };
+    $scope.patronSearchGridProvider = provider;
 
     // typeahead doesn't filter correctly with full hash objects, so
     // trim them down to just name and id.  This would allow us to use
@@ -357,6 +373,7 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egEnv,
         ).then(null, null, function(user) {
             patronSvc.localFlesh(user);
             $scope.patrons.items[$scope.patrons.items.length] = user;
+            $scope.patronSearchGridProvider.increment();
         });
     };
 
index c9ef91b..a76d6f6 100644 (file)
@@ -29,8 +29,8 @@ angular.module('egGridMod',
 
             // egList containting our tabular data is provided for us
             // and managed externally.
-            dataProvider : '=',
-            
+            itemsProvider : '=',
+
             // if true, hide the sortPriority options in the
             // grid configuration UI.  This is primarily used by
             // UIs where the data is ephemeral and can only be
@@ -52,12 +52,10 @@ angular.module('egGridMod',
         },
 
         controller : [
-                    '$scope','egIDL','egAuth','egNet',
-                    'egGridFlatDataProvider','egGridColumnsProvider',
-                    '$filter','$window',
-            function($scope,  egIDL,  egAuth,  egNet,  
-                    egGridFlatDataProvider,  egGridColumnsProvider,
-                    $filter,  $window) {
+                    '$scope','egIDL','egAuth','egNet', 'egGridFlatDataProvider',
+                    'egGridColumnsProvider', '$filter','$window',
+            function($scope,  egIDL,  egAuth,  egNet,  egGridFlatDataProvider,  
+                egGridColumnsProvider, $filter,  $window) {
 
             var grid = this;
 
@@ -67,7 +65,7 @@ angular.module('egGridMod',
                 grid.items = [];
                 grid.selected = {}; // idField-based
                 grid.totalCount = -1;
-                grid.dataProvider = $scope.dataProvider;
+                grid.dataProvider = $scope.itemsProvider;
                 grid.idlClass = $scope.idlClass;
                 grid.mainLabel = $scope.mainLabel;
                 grid.indexField = $scope.idField;
@@ -88,7 +86,17 @@ angular.module('egGridMod',
                     grid.columnsProvider.compileAutoColumns();
                 }
 
-                if (!grid.dataProvider) {
+                if (grid.dataProvider) {
+                    
+                    // refresh the grid contents each time the data 
+                    // provider's revision changes
+                    $scope.$watch(
+                        function() { return grid.dataProvider.revision() },
+                        function() { grid.collect() }
+                    );
+
+                } else {
+
                     grid.selfManagedData = true;
                     grid.dataProvider = egGridFlatDataProvider.instance({
                         idlClass : grid.idlClass,
@@ -157,9 +165,9 @@ angular.module('egGridMod',
             grid.indexValue = function(item) {
                 if (angular.isObject(item)) {
                     if (item !== null) {
-                        if (grid.indexFieldAsFunction) 
+                        if (angular.isFunction(item[grid.indexField]))
                             return item[grid.indexField]();
-                        return item[grid.indexField];
+                        return item[grid.indexField]; // flat data
                     }
                 }
                 // passed a non-object; assume it's an index
@@ -446,11 +454,12 @@ angular.module('egGridMod',
             name  : '@', // required; unique name
             path  : '@', // optional; flesh path
             label : '@', // optional; display label
-            flex  : '@', // optoinal; default flex width
+            flex  : '@', // optional; default flex width
+            display : '=' // optional; hide column by default
         },
         template : '<div></div>', // NOOP template
         link : function(scope, element, attrs, egGridCtrl) {
-            egGridCtrl.addColumn(scope);
+            egGridCtrl.columnsProvider.add(scope);
         }
     };
 })
@@ -595,8 +604,8 @@ angular.module('egGridMod',
 // Factory service for egGridDataManager instances, which are
 // responsible for collecting flattened grid data.
 .factory('egGridFlatDataProvider', 
-           ['egNet','egAuth',
-    function(egNet,  egAuth) {
+           ['$filter','egNet','egAuth','egIDL',
+    function($filter , egNet , egAuth , egIDL) {
 
         function FlatDataProvider(args) {
             var gridData = this;
@@ -605,6 +614,15 @@ angular.module('egGridMod',
             gridData.query = args.query;
             gridData.columnsProvider = args.columnsProvider;
             gridData.sort = [];
+            gridData._revision = 0;
+
+            gridData.revision = function() {
+                return gridData._revision;
+            }
+
+            gridData.increment = function() {
+                gridData._revision++;
+            }
 
             gridData.get = function(index, count, onresponse) {
 
@@ -631,9 +649,49 @@ angular.module('egGridMod',
             }
 
             gridData.itemFieldValue = function(item, column) {
-                // all of our data is flattened
+                // all of our data is flat
                 return item[column.name];
             }
+
+            // utility function which may be useful for other grid data 
+            // providers.
+            // given an object and a dot-separated path to a field,
+            // extract the value of the field.  The path can refer
+            // to function names or object attributes.  If the final
+            // value is an IDL field, run the value through its
+            // corresponding output filter.
+            gridData.nestedItemFieldValue = function(obj, column) {
+                if (obj === null || obj === undefined || obj === '') return '';
+                if (!column.path) return obj;
+
+                var idlField, cls, clsobj;
+                var parts = column.path.split('.');
+
+                angular.forEach(parts, function(step, idx) {
+                    // object is not fleshed to the expected extent
+                    if (!obj || typeof obj != 'object') {
+                        obj = '';
+                        return;
+                    }
+
+                    cls = obj.classname;
+                    if (cls && (clsobj = egIDL.classes[cls])) {
+                        idlField = clsobj.fields.filter(
+                            function(f) { return f.name == step })[0];
+                        obj = obj[step]();
+                    } else {
+                        if (angular.isFunction(obj[step])) {
+                            obj = obj[step]();
+                        } else {
+                            obj = obj[step];
+                        }
+                    }
+                });
+
+                if (obj === null || obj === undefined || obj === '') return '';
+                if (!idlField) return obj;
+                return $filter('egGridValueFilter')(obj, column);
+            }
         }
 
         return {