From: Bill Erickson Date: Mon, 24 Mar 2014 21:25:28 +0000 (-0400) Subject: web staff autogrid; porting items out experiment X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=ffdc2df1844bdf5bf3528bd7b31b32a956042fe0;p=working%2FEvergreen.git web staff autogrid; porting items out experiment Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2 index 076ba8e607..e94c3bb06a 100644 --- a/Open-ILS/src/templates/staff/circ/patron/index.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2 @@ -7,6 +7,7 @@ [% BLOCK APP_JS %] + diff --git a/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 index a62d91b54f..65721dc8ff 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 @@ -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}, -] -%] - -
-
+ + + + + + + + + + + + + -
-
-
[% l('No Items To Display') %]
-
-
+ + + -
-
- - - - - - - - - - - - - - -
# - {{col.label}} -
{{$index + 1}} - - {{items_out.fieldValue(circ, col.name)}} -
-
-
diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 index 311236aa2a..9f525f0e73 100644 --- a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 +++ b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 @@ -1,7 +1,7 @@
@@ -38,9 +38,12 @@
+
[% l('No Items To Display') %]
+ -
+
@@ -114,7 +117,7 @@ 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}}
diff --git a/Open-ILS/src/templates/staff/test/index.tt2 b/Open-ILS/src/templates/staff/test/index.tt2 index 68d48020a8..0f360bdb1c 100644 --- a/Open-ILS/src/templates/staff/test/index.tt2 +++ b/Open-ILS/src/templates/staff/test/index.tt2 @@ -8,7 +8,7 @@ [% BLOCK APP_JS %] - + [% END %] diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js index 0d8c02bc53..ad9c96ad79 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js @@ -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 index e6e478497f..0000000000 --- a/Open-ILS/web/js/ui/default/staff/services/autogrid.js +++ /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 : '
', // 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 index 0000000000..90545c08da --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -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 : '
', // 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/list.js b/Open-ILS/web/js/ui/default/staff/services/list.js index 763a8adda6..32bee97fae 100644 --- a/Open-ILS/web/js/ui/default/staff/services/list.js +++ b/Open-ILS/web/js/ui/default/staff/services/list.js @@ -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 {