web staff: autogrid experiments
authorBill Erickson <berick@esilibrary.com>
Wed, 12 Mar 2014 00:32:14 +0000 (20:32 -0400)
committerBill Erickson <berick@esilibrary.com>
Wed, 12 Mar 2014 00:32:14 +0000 (20:32 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/parts/t_autogrid.tt2
Open-ILS/src/templates/staff/test/t_autogrid.tt2
Open-ILS/web/js/ui/default/staff/services/autogrid.js
Open-ILS/web/js/ui/default/staff/services/ui.js
Open-ILS/web/js/ui/default/staff/test/app.js

index be9f99d..8a781b3 100644 (file)
@@ -1,12 +1,15 @@
 
 <style>
   /* TODO: move me */
+  .eg-grid-scroll {overflow-y:scroll; height: 600px}
+  .eg-grid-scroll > .row { margin-left: 0; margin-right: 0; }
   .eg-grid-header-row { font-weight: bold; }
   .eg-grid div.row {border-bottom: 2px solid #ddd}
   .eg-grid-content-row:nth-child(even) {background-color: rgb(248, 248, 248);}
+  .col-md-1 { overflow: hidden }
 </style>
 
-<div class="container-fluid eg-grid">
+<div class="container-fluid eg-grid" ng-class="{'eg-grid-scroll' : isScroll}">
   <div class="row eg-grid-action-row">
     <div class="col-md-1 col-md-offset-11 text-right">
       <div class="btn-group text-left">
@@ -20,7 +23,8 @@
     <div class="col-md-1 eg-grid-header-cell" 
         ng-repeat="column in dataList.allColumns"
         ng-show="dataList.displayColumns[column.name]">
-      {{column.label}}
+        <a href="javascript:;" 
+          ng-click="sortOn(column.name)">{{column.label}}</a>
     </div>
   </div>
   <div class="row eg-grid-content-row" 
@@ -28,7 +32,7 @@
     <div class="col-md-1 eg-grid-content-cell" 
           ng-repeat="column in dataList.allColumns"
           ng-show="dataList.displayColumns[column.name]">
-      {{dataList.fieldValue(item, column.name)}}
+      {{dataList.fieldValue(item, column.name) | egGridvalueFilter:column}}
     </div>
   </div>
 </div>
index 12729b2..946e2bb 100644 (file)
@@ -1,18 +1,28 @@
+<h1>AutoGrid Explicit Fields</h1>
 
-<h1>AutoGrid Test</h1>
-<div 
-  eg-grid 
+<eg-grid 
   idl-class="aou"
   sort="testGridSort"
-  query="testGridQuery"
-  auto-fields="true"
-  >
-  <!--
-  <div eg-grid-field name="shortname" label="[% l('Shortname Manual Label') %]"></div>
-  <div eg-grid-field name="name"></div>
-  <div eg-grid-field name="id"></div>
-  <div eg-grid-field name="depth"  path="ou_type.depth"></div>
-  <div eg-grid-field name="parent_ou_id"  path="parent_ou.id"></div>
-  <div eg-grid-field name="parent_ou_name" path="parent_ou.name" label="[% l('Parent Org') %]"></div>
+  query="testGridQuery">
+  <!-- 
+    eg-grid-field's require closing tags; not sure why 
+    you can also do <div eg-grid-tag attrs..></div>
   -->
-</div>
+  <eg-grid-field name="shortname" label="[% l('Shortname Manual Label') %]"></eg-grid-field>
+  <eg-grid-field name="name"></eg-grid-field>
+  <eg-grid-field name="id"></eg-grid-field>
+  <eg-grid-field name="depth"  path="ou_type.depth"></eg-grid-field>
+  <eg-grid-field name="parent_ou_id"  path="parent_ou.id"></eg-grid-field>
+  <eg-grid-field name="parent_ou_name" path="parent_ou.name" label="[% l('Parent Org') %]"></eg-grid-field>
+</eg-grid>
+
+<h1>AutoGrid w/ Auto Fields</h1>
+
+<eg-grid
+  persist-key="staff.test.grid.auto-fields"
+  idl-class="rmsr"
+  is-scroll="true"
+  sort="testGridSort"
+  query="testGridQuery"
+  auto-fields="true"/>
+
index dccfb0c..4632c4e 100644 (file)
@@ -1,17 +1,41 @@
 
-angular.module('egGridMod', ['egCoreMod', 'egListMod'])
+angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod'])
 
 .directive('egGrid', function() {
     return {
-        restrict : 'A',
+        restrict : 'AE',
         transclude : true,
         scope : {
             idlClass : '@',
+
+            // points to a structure in the calling scope which defines
+            // a PCRUD-compliant query.
             query : '=',
-            sort : '@',
-            autoFields : '='
+
+            // if true, grid columns are derived from all non-virtual
+            // fields on the base idlClass
+            autoFields : '=',
+
+            // optional, custom data retrieval function
+            dataFetcher : '=',
+
+            // grid preferences will be stored / retrieved with this key
+            persistKey : '@',
+
+            // if true, use the scroll CSS to force a vertical height 
+            // and scroll bar
+            isScroll : '='
+        },
+
+        link : function(scope, element, attrs) {     
+            // link() is called after page compilation, which means our
+            // eg-grid-field's have been parsed and loaded.  Now it's 
+            // safe to perform our initial page load.
+            scope.fetchData();
         },
-        templateUrl : '/eg/staff/parts/t_autogrid', // TODO: abs url
+
+        templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url
+
         controller : function($scope, $timeout, egIDL, egAuth, egNet, egList) { // TODO: reqs list
             var self = this;
 
@@ -21,14 +45,33 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod'])
 
             $scope.dataList = egList.create();
 
-            this.addField = function(fieldScope) {
+            // column-header click quick sort
+            $scope.sortOn = function(col_name) {
+                if ($scope.sort && $scope.sort.length &&
+                    $scope.sort[0] == col_name) {
+                    var blob = {};
+                    blob[col_name] = 'desc';
+                    $scope.sort = [blob];
+                } else {
+                    $scope.sort = [col_name];
+                }
+                $scope.fetchData();
+            }
+
+            /**
+             * Adds a column from an eg-grid-field or directly from 
+             * an IDL field via compileAutoFields.
+             */
+            this.addColumn = function(fieldSpec) {
                 var field = {
-                    name : fieldScope.name,
-                    label : fieldScope.label,
-                    path : fieldScope.path,
-                    display : (fieldScope.display === false) ? false : true
+                    name : fieldSpec.name,
+                    label : fieldSpec.label,
+                    path : fieldSpec.path,
+                    datatype : fieldSpec.datatype,
+                    display : (fieldSpec.display === false) ? false : true
                 };
-                self.applyFieldLabel(field);
+                if (!field.path) field.path = field.name;
+                field = self.absorbField(field);
                 $scope.dataList.addColumn(field);
             }
 
@@ -58,31 +101,33 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod'])
                                 }
                             }
                         }
+
+                        // once we exceeed the number of display fields,
+                        // the others are added as hidden fields, accessible
+                        // via the column picker.
                         if ($scope.dataList.allColumns.length >= self.maxFieldCount) {
                             console.log('setting to false ' + field.name);
                             field.display = false;
                         }
-                        self.addField(field);
+                        self.addColumn(field);
                     }
                 );
             }
 
-            this.applyFieldLabel = function(field) {
-                if (field.label) return; // label already applied
-
-                var class_obj = egIDL.classes[$scope.idlClass];
-                if (!field.path) field.path = field.name;
-                var path_parts = field.path.split(/\./);
+            // given a base class and a dotpath, find the IDL field
+            this.getIDLFieldFromPath = function(idlClass, path) {
+                var class_obj = egIDL.classes[idlClass];
+                var path_parts = path.split(/\./);
 
                 // note: use of for() is intentional for early exit
-                var field_obj;
+                var idl_field;
                 for (var path_idx in path_parts) {
                     var part = path_parts[path_idx];
 
                     // find the field object matching the path component
                     for (var field_idx in class_obj.fields) {
                         if (class_obj.fields[field_idx].name == part) {
-                            field_obj = class_obj.fields[field_idx];
+                            idl_field = class_obj.fields[field_idx];
                             break;
                         }
                     }
@@ -90,23 +135,74 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod'])
                     // unless we're at the end of the list, this field should
                     // link to another class.
 
-                    if (field_obj && field_obj['class'] && (
-                        field_obj.datatype == 'link' || 
-                        field_obj.datatype == 'org_unit')) {
-                        class_obj = egIDL.classes[field_obj['class']];
+                    if (idl_field && idl_field['class'] && (
+                        idl_field.datatype == 'link' || 
+                        idl_field.datatype == 'org_unit')) {
+                        class_obj = egIDL.classes[idl_field['class']];
                     } else {
                         if (path_idx < (path_parts.length - 1)) {
                             // we ran out of classes to hop through before
                             // we ran out of path components
-                            console.error("egGrid: invalid IDL path: " + field.path);
+                            console.error("egGrid: invalid IDL path: " + path);
                         }
                     }
                 }
 
-                field.label = field_obj ? field_obj.label : field.name;
+                return idl_field;
+            }
+
+            /**
+             * Looks for the matching IDL field to extract the label
+             * and datattype as needed.
+             * Creates a local copy of the field for our internal
+             * machinations.
+             */
+            this.absorbField = function(field) {
+
+                // start by cloning the field so we can flesh it out.
+                // note: aungular.copy won't work, because 'field' may
+                // be a $scope object.
+                var new_field = {
+                    name : field.name,
+                    label : field.label,
+                    path : field.path || field.name,
+                    display : (field.display === false) ? false : true
+                };
+
+                // lookup the matching IDL field
+                var idl_field = field.datatype ? field : 
+                    self.getIDLFieldFromPath($scope.idlClass, field.path);
+
+                // No matching IDL field.  Caller has gone commando.
+                // Nothing left to do.
+                if (!idl_field) return new_field;
+
+                new_field.datatype = idl_field.datatype;
+
+                if (field.label) {
+                    // caller-provided label
+                    new_field.label = field.label;
+                } else {
+                    if (idl_field.label) {
+                        new_field.label = idl_field.label;
+                    } else {
+                        new_field.label = new_field.name;
+                    }
+                }
+
+                return new_field;
             }
 
-            this.fetchData = function() {
+            /**
+             * For stock grids, makes a flattened_search call to retrieve
+             * the requested values.
+             * For non-stock grids, calls the external data fetcher
+             */
+            $scope.fetchData = function() {
+                $scope.dataList.resetPageData();
+
+                if (self.dataFetcher) 
+                    return self.dataFetcher();
 
                 if (!$scope.query) {
                     console.error("egGrid requires a query");
@@ -133,29 +229,13 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod'])
                     egAuth.token(), $scope.idlClass, queryFields,
                     $scope.query,
                     {   sort : $scope.sort,
-                        limit : 10, // TODO
+                        limit : 20, // TODO
                         offset : 0 // TODO
                     }
-                  ).then(null, null, function(item) {
-                      $scope.dataList.items.push(item);
-                    }
-                );
-            }
-
-            // Don't call fetchData until we know what the fields are
-            // TODO: this is a hack which polls to see if our eg-grid-field's
-            // have been processed.  There has to be a better way...
-            readycheck = 0;
-            this.checkReadyForDraw = function() {
-                if ($scope.autoFields || $scope.dataList.allColumns.length) {
-                    self.fetchData();
-                } else {
-                    if (++readycheck > 10000) return; // failsafe, no fields defined
-                    // check again after the next $digest loop
-                    $scope.$evalAsync(function() {self.checkReadyForDraw()});
-                }
+                ).then(null, null, function(item) {
+                    $scope.dataList.items.push(item);
+                });
             }
-            this.checkReadyForDraw();
         }
     };
 })
@@ -168,14 +248,36 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod'])
 .directive('egGridField', function() {
     return {
         require : '^egGrid',
-        restrict : 'A',
+        restrict : 'AE',
         transclude : true,
         scope : {name : '@', path : '@', label : '@'},
         template : '<div></div>', // NOOP template
         link : function(scope, element, attrs, egGridCtrl) {
-            egGridCtrl.addField(scope);
+            egGridCtrl.addColumn(scope);
         }
     };
-});
+})
+
+/**
+ * Translates bare IDL object values into display values.
+ * 1. Passes dates through the angular date filter
+ * 2. Translates bools to Booleans so the browser can display translated 
+ *    value.  (Though we could manually translate instead..)
+ * Others likely to follow...
+ */
+.filter('egGridvalueFilter', ['$filter', function($filter) {                         
+    return function(value, item) {                                             
+        switch(item.datatype) {                                                
+            case 'bool':                                                       
+                // Browser will translate true/false for us                    
+                return Boolean(value == 't');                                  
+            case 'timestamp':                                                  
+                // canned angular date filter FTW                              
+                return $filter('date')(value);                                 
+            default:                                                           
+                return value;                                                  
+        }                                                                      
+    }                                                                          
+}]);
 
 
index 4eebe7d..3670b53 100644 (file)
@@ -133,3 +133,18 @@ function($timeout, $parse) {
 
     return service;
 })
+
+/*
+ * http://stackoverflow.com/questions/15731634/how-do-i-handle-right-click-events-in-angular-js
+ */
+.directive('ngRightClick', function($parse) {
+    return function(scope, element, attrs) {
+        var fn = $parse(attrs.ngRightClick);
+        element.bind('contextmenu', function(event) {
+            scope.$apply(function() {
+                event.preventDefault();
+                fn(scope, {$event:event});
+            });
+        });
+    };
+});
index 28ca01c..b9917c8 100644 (file)
@@ -15,23 +15,18 @@ angular.module('egTestApp', ['ngRoute', 'ui.bootstrap',
     //$routeProvider.otherwise({redirectTo : '/circ/patron/search'});
 })
 
-.controller('egTestCtrl', ['$scope', function($scope) {
-    /*
-    $scope.$on('$viewContentLoaded', function() {
-        console.debug("viewContentLoaded");
-    });
-    */
+.controller('egTestCtrl', ['$scope', '$rootScope', '$timeout',
+    function($scope, $rootScope, $timeout) {
 }])
 
 .controller('TestGridCtrl', 
        ['$scope', 
 function($scope) {
+    var self = this;
 
     console.log('TestGridCtrl');
     $scope.testGridQuery = {id : {'<>' : null}};
     $scope.testGridSort = ['depth', 'parent_ou_id', 'name']
-
-    //$scope.fmClass="aou";
 }]);