web staff autogrid; porting items out experiment
authorBill Erickson <berick@esilibrary.com>
Mon, 24 Mar 2014 21:25:28 +0000 (17:25 -0400)
committerBill Erickson <berick@esilibrary.com>
Mon, 24 Mar 2014 21:25:28 +0000 (17:25 -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_items_out_table.tt2
Open-ILS/src/templates/staff/parts/t_autogrid.tt2
Open-ILS/src/templates/staff/test/index.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/services/autogrid.js [deleted file]
Open-ILS/web/js/ui/default/staff/services/grid.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/list.js

index 076ba8e..e94c3bb 100644 (file)
@@ -7,6 +7,7 @@
 
 [% 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/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 a62d91b..65721dc 100644 (file)
@@ -1,61 +1,43 @@
-[%
-COLUMNS = [
-{label => l('Circ ID'), name => 'id', display => 1},
-{label => l('Barcode'), name => 'target_copy.barcode' display => 1},
-{label => l('Due Date'), name => 'due_date' display => 1},
-{label => l('Checkout/Renewal Library'), 
-  name => 'circ_lib.shortname' display => 1},
-{label => l('Renewals Remaining'), name => 'renewal_remaining' display => 1},
-{label => l('Fines Stopped'), name => 'stop_fines' display => 1},
-{label => l('Title'), 
-  name => 'target_copy.call_number.record.simple_record.title', display => 1},
-]
-%]
 
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-items_out.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-]);
-">
-</div>
+<eg-grid 
+  idl-class="circ"
+  persist-key="eg.staff.circ.patron.items_out"
+  eg-list="items_out"
+  id-field="id">
+  <eg-grid-field name="id"></eg-grid-field>
+  <eg-grid-field 
+    name="target_copy.barcode" 
+    path="target_copy.barcode" 
+    flex="3"
+    label="[% l('Barcode') %]">
+  </eg-grid-field>
+  <eg-grid-field 
+    name="due_date"
+    label="[% l('Due Date') %]">
+  </eg-grid-field>
+  <eg-grid-field 
+    name="circ_lib.shortname" 
+    path="circ_lib.shortname"
+    label="[% l('Circ Lib') %]">
+  </eg-grid-field>
+  <eg-grid-field name="renewal_remaining"></eg-grid-field>
+  <eg-grid-field name="stop_fines"></eg-grid-field>
+  <eg-grid-field 
+    name="title" 
+    flex="5"
+    label="[% l('Title') %]" 
+    path="target_copy.call_number.record.simple_record.title">
+  </eg-grid-field>
+</eg-grid>
 
-<div class="row" ng-show="!loading && !items_out.count()">
-  <div class="col-md-10 col-md-offset-1">
-    <div class="alert alert-info">[% l('No Items To Display') %]</div>
-  </div>
-</div>
+<!-- TODO: row-level styling, alerts, etc. (e.g. overdue) -->
+
+<!--
+<tr ng-repeat="circ in items_out.items | reverse track by $index"
+  ng-click="onRowClick($event, circ)"
+  ng-class="{
+    selected : items_out.selected[circ.id()], 
+    'patron-summary-alert' : circIsOverdue(circ)
+  }">
+-->
 
-<div class="row" ng-show="items_out.count()">
-  <div class="col-md-12">
-    <table class="list table table-hover table-condensed">
-      <thead>
-        <tr>
-          <th>#</th>
-          <th><a href='' ng-click="items_out.toggleSelectAll()">&#x2713;</a></th>
-          <th ng-repeat="col in items_out.allColumns" 
-            ng-show="items_out.displayColumns[col.name]">
-            {{col.label}}
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr ng-repeat="circ in items_out.items | reverse track by $index"
-          ng-click="onRowClick($event, circ)"
-          ng-class="{
-            selected : items_out.selected[circ.id()], 
-            'patron-summary-alert' : circIsOverdue(circ)
-          }">
-          <td>{{$index + 1}}</td>
-          <td><span ng-if="items_out.selected[circ.id()]">&#x2713;</span> 
-          <td ng-repeat="col in items_out.allColumns" 
-            ng-show="items_out.displayColumns[col.name]">
-            {{items_out.fieldValue(circ, col.name)}}
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
index 311236a..9f525f0 100644 (file)
@@ -1,7 +1,7 @@
 
 <!-- 
   Actions row.
-  This sits right above the grid and contains the column picker, etc.
+  This sits above the grid and contains the column picker, etc.
 -->
 <div class="eg-grid-row eg-grid-action-row">
 
   </div>
 </div>
 
+<div ng-show="list.items.length == 0" 
+  class="alert alert-info">[% l('No Items To Display') %]</div>
+
 <!-- Grid -->
-<div class="eg-grid" 
-  ng-class="{'eg-grid-as-conf' : showGridConf,'eg-grid-scroll' : isScroll}">
+<div class="eg-grid" ng-show="list.items.length > 0"
+  ng-class="{'eg-grid-as-conf' : showGridConf, 'eg-grid-scroll' : isScroll}">
 
   <!-- import embedded eg-grid-field defs via no-op transclude -->
   <div ng-transclude></div>
           ng-repeat="column in list.allColumns"
           style="flex:{{column.flex}}"
           ng-show="list.displayColumns[column.name]">
-        {{list.fieldValue(item, column.name) | egGridvalueFilter:column}}
+        {{list.fieldValue(item, column.path) | egGridvalueFilter:column}}
       </div>
     </div>
   </div>
index 68d4802..0f360bd 100644 (file)
@@ -8,7 +8,7 @@
 [% 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/ui.js"></script>
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/autogrid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/test/app.js"></script>
 [% END %]
 
index 0d8c02b..ad9c96a 100644 (file)
@@ -8,7 +8,7 @@
  */
 
 angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 
-    'egCoreMod', 'egUiMod', 'egListMod', 'egUserMod'])
+    'egCoreMod', 'egUiMod', 'egListMod', 'egGridMod', 'egUserMod'])
 
 .config(function($routeProvider, $locationProvider) {
     $locationProvider.html5Mode(true);
diff --git a/Open-ILS/web/js/ui/default/staff/services/autogrid.js b/Open-ILS/web/js/ui/default/staff/services/autogrid.js
deleted file mode 100644 (file)
index e6e4784..0000000
+++ /dev/null
@@ -1,367 +0,0 @@
-
-angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'])
-
-.directive('egGrid', function() {
-    return {
-        restrict : 'AE',
-        transclude : true,
-        scope : {
-            // IDL class hint (e.g. "aou")
-            idlClass : '@',
-
-            // points to a structure in the calling scope which defines
-            // a PCRUD-compliant query.
-            query : '=',
-
-            // if true, grid columns are derived from all non-virtual
-            // fields on the base idlClass
-            autoFields : '@',
-
-            // 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 : '@',
-
-            // field whose value is unique and may be used for item
-            // reference / lookup.  This will usually be someting like
-            // "id".  This is not needed when using autoFields, since we
-            // can determine the primary key directly from the IDL.
-            idField : '@',
-
-            // egList containting our tabular data is provided for us
-            // and managed externally.
-            egList : '=',
-            
-            // 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
-            // single-display-column sorted.
-            disableSortPriority : '@'
-        },
-
-        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: avoid abs url
-
-        controller : // TODO: reqs list
-            function($scope, $timeout, $modal, egIDL, egAuth, egNet, egList) { 
-            var self = this;
-
-            // TODO: dynamic
-            this.limit = 20; 
-            this.ofset = 0;
-
-            $scope.list = $scope.egList || egList.create();
-
-            $scope.$watch('isScroll', function(newValue, oldValue) {
-                console.log('isScroll changed to ' + newValue) });
-
-            // 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();
-            }
-
-            // maps numeric sort priority to flattener sort blob
-            // e.g. 
-            // name = 1; code = -2; type = 3
-            // compiles to:
-            // [{name : "asc"}, {code : "desc"}, {type : "asc"}]
-            this.compileSort = function() {
-
-                var sortList = $scope.list.allColumns.filter(
-                    function(col) { return Number(col.sortPriority) != 0 }
-                ).sort( 
-                    function(a, b) { 
-                        if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority))
-                            return -1;
-                        return 1;
-                    }
-                );
-
-                $scope.sort = sortList.map(function(col) {
-                    var blob = {};
-                    blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc';
-                    return blob;
-                });
-            },
-
-            $scope.modifyColumnFlex = function(column, val) {
-                column.flex += val;
-                // prevent flex:0;  use hiding instead
-                if (column.flex < 1)
-                    column.flex = 1;
-            }
-
-            $scope.toggleGridConf = function() {
-                if ($scope.showGridConf) {
-                    $scope.showGridConf = false;
-                    self.compileSort(); 
-
-                    // config done; 
-                    // reload data in case sort priorities changed.
-                    $scope.fetchData();
-                } else {
-                    $scope.showGridConf = true;
-                }
-            }
-
-            /**
-             * Adds a column from an eg-grid-field or directly from 
-             * an IDL field via compileAutoFields.
-             */
-            this.addColumn = function(fieldSpec) {
-
-                var field = {
-                    name : fieldSpec.name,
-                    label : fieldSpec.label,
-                    path : fieldSpec.path,
-                    flex : fieldSpec.flex,
-                    datatype : fieldSpec.datatype,
-                    display : (fieldSpec.display !== false)
-                };
-                if (!field.path) field.path = field.name;
-                field = self.absorbField(field);
-                $scope.list.addColumn(field);
-            }
-
-            /**
-             * Caller wants to display all fields for the selected IDL class
-             * Find the fields and, when a field is a link, fetch the label
-             * from the "selector" field as well.
-             */
-            this.compileAutoFields = function() {
-                if ($scope.list.allColumns.length) return;
-
-                $scope.idField = $scope.idField || 
-                    egIDL.classes[$scope.idlClass].pkey;
-
-                angular.forEach(
-                    egIDL.classes[$scope.idlClass].fields.sort(
-                        function(a, b) { return a.name < b.name ? -1 : 1 }),
-                    function(field) {
-                        if (field.virtual) return;
-                        if (field.datatype == 'link' || field.datatype == 'org_unit') {
-                            // if the field is a link and the linked class has a
-                            // "selector" field specified, use the selector field
-                            // as the display field for the grid.
-                            // flattener will take care of the fleshing.
-                            if (field['class']) {
-                                var selectorField = egIDL.classes[field['class']].fields
-                                    .filter(function(f) { return Boolean(f.selector) })[0];
-                                if (selectorField) {
-                                    field.path = field.name + '.' + selectorField.selector;
-                                }
-                            }
-                        }
-                        self.addColumn(field);
-                    }
-                );
-            }
-
-            // 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 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) {
-                            idl_field = class_obj.fields[field_idx];
-                            break;
-                        }
-                    }
-
-                    // unless we're at the end of the list, this field should
-                    // link to another 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: " + path);
-                        }
-                    }
-                }
-
-                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,
-                    flex : Number(field.flex) || 2,
-                    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;
-            }
-
-            /**
-             * 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() {
-
-                // when a list is provided, data management is 
-                // handled externally.
-                if ($scope.egList) return;
-
-                $scope.list.resetPageData();
-
-                if (!$scope.query) {
-                    console.error("egGrid requires a query");
-                    return;
-                }
-
-                if (!$scope.idlClass) {
-                    console.error("egGrid requires an idlClass");
-                    return;
-                }
-
-                if ($scope.autoFields)
-                    self.compileAutoFields();
-
-                $scope.list.indexField = $scope.idField;
-
-                var queryFields = {}
-                angular.forEach($scope.list.allColumns, function(field) {
-                    if ($scope.list.displayColumns[field.name])
-                        queryFields[field.name] = field.path || field.name;
-                });
-
-                egNet.request(
-                    'open-ils.fielder',
-                    'open-ils.fielder.flattened_search',
-                    egAuth.token(), $scope.idlClass, queryFields,
-                    $scope.query,
-                    {   sort : $scope.sort,
-                        limit : self.limit,
-                        offset : self.offset
-                    }
-                ).then(null, null, function(item) {
-                    $scope.list.items.push(item);
-                });
-            }
-
-            $scope.handleRowClick = function($event, item) {
-                var index = $scope.list.indexValue(item);
-                if ($event.ctrlKey || $event.metaKey /* mac command */) {
-                    $scope.list.toggleOneSelection(index);
-                } else {
-                    $scope.list.selectOne(index);
-                }
-            }
-
-            $scope.itemIsSelected = function(item) {
-                return $scope.list.selected[
-                    $scope.list.indexValue(item)
-                ];
-            }
-        }
-    };
-})
-
-/**
- * eg-grid-field : used for collecting custom field data from the templates.
- * This directive does not direct display, it just passes data up to the 
- * parent grid.
- */
-.directive('egGridField', function() {
-    return {
-        require : '^egGrid',
-        restrict : 'AE',
-        transclude : true,
-        scope : {
-            name  : '@', // required; unique name
-            path  : '@', // optional; flesh path
-            label : '@', // optional; display label
-            flex  : '@', // optoinal; default flex width
-        },
-        template : '<div></div>', // NOOP template
-        link : function(scope, element, attrs, egGridCtrl) {
-            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;                                                  
-        }                                                                      
-    }                                                                          
-}]);
-
-
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
new file mode 100644 (file)
index 0000000..90545c0
--- /dev/null
@@ -0,0 +1,368 @@
+
+angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'])
+
+.directive('egGrid', function() {
+    return {
+        restrict : 'AE',
+        transclude : true,
+        scope : {
+            // IDL class hint (e.g. "aou")
+            idlClass : '@',
+
+            // points to a structure in the calling scope which defines
+            // a PCRUD-compliant query.
+            query : '=',
+
+            // if true, grid columns are derived from all non-virtual
+            // fields on the base idlClass
+            autoFields : '@',
+
+            // 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 : '@',
+
+            // field whose value is unique and may be used for item
+            // reference / lookup.  This will usually be someting like
+            // "id".  This is not needed when using autoFields, since we
+            // can determine the primary key directly from the IDL.
+            idField : '@',
+
+            // egList containting our tabular data is provided for us
+            // and managed externally.
+            egList : '=',
+            
+            // 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
+            // single-display-column sorted.
+            disableSortPriority : '@'
+        },
+
+        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: avoid abs url
+
+        controller : // TODO: reqs list
+            function($scope, $timeout, $modal, egIDL, egAuth, egNet, egList) { 
+            var self = this;
+
+            // TODO: dynamic
+            this.limit = 20; 
+            this.ofset = 0;
+
+            $scope.list = $scope.egList || egList.create();
+
+            $scope.$watch('isScroll', function(newValue, oldValue) {
+                console.log('isScroll changed to ' + newValue) });
+
+            // 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();
+            }
+
+            // maps numeric sort priority to flattener sort blob
+            // e.g. 
+            // name = 1; code = -2; type = 3
+            // compiles to:
+            // [{name : "asc"}, {code : "desc"}, {type : "asc"}]
+            this.compileSort = function() {
+
+                var sortList = $scope.list.allColumns.filter(
+                    function(col) { return Number(col.sortPriority) != 0 }
+                ).sort( 
+                    function(a, b) { 
+                        if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority))
+                            return -1;
+                        return 1;
+                    }
+                );
+
+                $scope.sort = sortList.map(function(col) {
+                    var blob = {};
+                    blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc';
+                    return blob;
+                });
+            },
+
+            $scope.modifyColumnFlex = function(column, val) {
+                column.flex += val;
+                // prevent flex:0;  use hiding instead
+                if (column.flex < 1)
+                    column.flex = 1;
+            }
+
+            $scope.toggleGridConf = function() {
+                if ($scope.showGridConf) {
+                    $scope.showGridConf = false;
+                    self.compileSort(); 
+
+                    // config done; 
+                    // reload data in case sort priorities changed.
+                    $scope.fetchData();
+                } else {
+                    $scope.showGridConf = true;
+                }
+            }
+
+            /**
+             * Adds a column from an eg-grid-field or directly from 
+             * an IDL field via compileAutoFields.
+             */
+            this.addColumn = function(fieldSpec) {
+
+                var field = {
+                    name : fieldSpec.name,
+                    label : fieldSpec.label,
+                    path : fieldSpec.path,
+                    flex : fieldSpec.flex,
+                    datatype : fieldSpec.datatype,
+                    display : (fieldSpec.display !== false)
+                };
+                if (!field.path) field.path = field.name;
+                if (!field.name) field.name = field.path;
+                field = self.absorbField(field);
+                $scope.list.addColumn(field);
+            }
+
+            /**
+             * Caller wants to display all fields for the selected IDL class
+             * Find the fields and, when a field is a link, fetch the label
+             * from the "selector" field as well.
+             */
+            this.compileAutoFields = function() {
+                if ($scope.list.allColumns.length) return;
+
+                $scope.idField = $scope.idField || 
+                    egIDL.classes[$scope.idlClass].pkey;
+
+                angular.forEach(
+                    egIDL.classes[$scope.idlClass].fields.sort(
+                        function(a, b) { return a.name < b.name ? -1 : 1 }),
+                    function(field) {
+                        if (field.virtual) return;
+                        if (field.datatype == 'link' || field.datatype == 'org_unit') {
+                            // if the field is a link and the linked class has a
+                            // "selector" field specified, use the selector field
+                            // as the display field for the grid.
+                            // flattener will take care of the fleshing.
+                            if (field['class']) {
+                                var selectorField = egIDL.classes[field['class']].fields
+                                    .filter(function(f) { return Boolean(f.selector) })[0];
+                                if (selectorField) {
+                                    field.path = field.name + '.' + selectorField.selector;
+                                }
+                            }
+                        }
+                        self.addColumn(field);
+                    }
+                );
+            }
+
+            // 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 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) {
+                            idl_field = class_obj.fields[field_idx];
+                            break;
+                        }
+                    }
+
+                    // unless we're at the end of the list, this field should
+                    // link to another 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: " + path);
+                        }
+                    }
+                }
+
+                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,
+                    flex : Number(field.flex) || 2,
+                    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;
+            }
+
+            /**
+             * 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() {
+
+                // when a list is provided, data management is 
+                // handled externally.
+                if ($scope.egList) return;
+
+                $scope.list.resetPageData();
+
+                if (!$scope.query) {
+                    console.error("egGrid requires a query");
+                    return;
+                }
+
+                if (!$scope.idlClass) {
+                    console.error("egGrid requires an idlClass");
+                    return;
+                }
+
+                if ($scope.autoFields)
+                    self.compileAutoFields();
+
+                $scope.list.indexField = $scope.idField;
+
+                var queryFields = {}
+                angular.forEach($scope.list.allColumns, function(field) {
+                    if ($scope.list.displayColumns[field.name])
+                        queryFields[field.name] = field.path || field.name;
+                });
+
+                egNet.request(
+                    'open-ils.fielder',
+                    'open-ils.fielder.flattened_search',
+                    egAuth.token(), $scope.idlClass, queryFields,
+                    $scope.query,
+                    {   sort : $scope.sort,
+                        limit : self.limit,
+                        offset : self.offset
+                    }
+                ).then(null, null, function(item) {
+                    $scope.list.items.push(item);
+                });
+            }
+
+            $scope.handleRowClick = function($event, item) {
+                var index = $scope.list.indexValue(item);
+                if ($event.ctrlKey || $event.metaKey /* mac command */) {
+                    $scope.list.toggleOneSelection(index);
+                } else {
+                    $scope.list.selectOne(index);
+                }
+            }
+
+            $scope.itemIsSelected = function(item) {
+                return $scope.list.selected[
+                    $scope.list.indexValue(item)
+                ];
+            }
+        }
+    };
+})
+
+/**
+ * eg-grid-field : used for collecting custom field data from the templates.
+ * This directive does not direct display, it just passes data up to the 
+ * parent grid.
+ */
+.directive('egGridField', function() {
+    return {
+        require : '^egGrid',
+        restrict : 'AE',
+        transclude : true,
+        scope : {
+            name  : '@', // required; unique name
+            path  : '@', // optional; flesh path
+            label : '@', // optional; display label
+            flex  : '@', // optoinal; default flex width
+        },
+        template : '<div></div>', // NOOP template
+        link : function(scope, element, attrs, egGridCtrl) {
+            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 763a8ad..32bee97 100644 (file)
@@ -51,6 +51,7 @@ angular.module('egListMod', ['egCoreMod'])
         this.selected = {};
 
         this.indexValue = function(item) {
+            if (!item) return null;
             if (this.indexFieldAsFunction) {
                 return item[this.indexField]();
             } else {