From 6ef1e3db07cd8b732f79abd671f2e435d7758758 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 8 Apr 2014 12:40:39 -0400 Subject: [PATCH] web staff : patron search, more grid integration Signed-off-by: Bill Erickson --- .../src/templates/staff/circ/patron/t_search.tt2 | 40 +-- .../staff/circ/patron/t_search_results.tt2 | 13 +- Open-ILS/src/templates/staff/parts/t_autogrid.tt2 | 10 +- .../web/js/ui/default/staff/circ/patron/app.js | 286 ++++++--------------- Open-ILS/web/js/ui/default/staff/services/grid.js | 158 +++++++++--- 5 files changed, 233 insertions(+), 274 deletions(-) diff --git a/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 index f6dcb0cfb4..29fe17be33 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 @@ -7,23 +7,23 @@
-
-
+ ng-model="searchArgs.family_name" placeholder="[% l('Last Name') %]"/>
+ ng-model="searchArgs.first_given_name" placeholder="[% l('First Name') %]"/>
@@ -31,7 +31,7 @@
-
@@ -50,64 +50,64 @@
+ ng-model="searchArgs.second_given_name" placeholder="[% l('Middle Name') %]"/>
+ ng-model="searchArgs.alias" placeholder="[% l('Alias') %]"/>
+ ng-model="searchArgs.usrname" placeholder="[% l('Username') %]"/>
+ ng-model="searchArgs.email" placeholder="[% l('Email') %]"/>
+ ng-model="searchArgs.ident" placeholder="[% l('Identification') %]"/>
+ ng-model="searchArgs.id" placeholder="[% l('Database ID') %]"/>
+ ng-model="searchArgs.phone" placeholder="[% l('Phone') %]"/>
+ ng-model="searchArgs.street1" placeholder="[% l('Street 1') %]"/>
+ ng-model="searchArgs.street2" placeholder="[% l('Street 2') %]"/>
+ ng-model="searchArgs.city" placeholder="[% l('City') %]"/>
-
-
@@ -115,7 +115,7 @@
@@ -123,7 +123,7 @@
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_search_results.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_search_results.tt2 index 312055ca45..21663828b6 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_search_results.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_search_results.tt2 @@ -1,18 +1,17 @@ - - - - + + + + - + diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 index 3dca865304..d343921cf0 100644 --- a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 +++ b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 @@ -164,7 +164,7 @@ style="flex:{{grid.indexFlex + grid.selectorFlex}}">
[% l('Expand') %]
[% l('Shrink') %]
-
[% l('Sort') %]
+
[% l('Sort') %]
-
- +
+
+ +
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 3d82f6dea4..eddf008bb6 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 @@ -230,20 +230,29 @@ 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', + '$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, - egGridFlatDataProvider) { + $filter, egIDL, egNet, egAuth, egEvent, egList, egUser, + patronSvc , egGridDataProvider) { $scope.initTab('search'); $scope.focusMe = true; - $scope.args = $location.search(); + $scope.searchArgs = {}; - console.log('PatronSearchCtrl'); - - // our data provider is a modified flat data provider - var provider = egGridFlatDataProvider.instance({}); + if (patronSvc.lastSearch) { + // populate the search form with our cached search info + angular.forEach(patronSvc.lastSearch.search, function(val, key) { + $scope.searchArgs[key] = val.value; + }); + } + + var provider = egGridDataProvider.instance({}); + provider.initialize = function() { + return {offset : 0}; + } + + // show the user summary for the first selected user provider.select = function(items) { if (items[0]) { var user = items[0]; @@ -252,61 +261,81 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egEnv, } provider.get = function(offset, count, onitem) { - console.log('get ' + $location.search() + ' : ' + provider._revision); var deferred = $q.defer(); - /* - if (args.id) { - retrieveUsers([args.id]); - egUser.get(id).then(function(user) { - patronSvc.localFlesh(user); - $scope.patrons.items[idx] = user; - }); - - } else { - sendSearch(args); - } - */ - - var search = compileSearch($location.search()); - - /* - if (Object.keys(search).length == 0 && - offset == patronSvc.cachedSearchOffset && - patronSvc.patrons.length) { - // accessing the page without a cached search - // see if we have a cached result set - angular.forEach(patronSvc.patrons, function(p) { onitem(p) }); - return; - } - */ - - //patronSvc.cachedSearchOffset = offset; - patronSvc.patrons = []; + var search = compileSearch($scope.searchArgs); + if (Object.keys(search) == 0) return $q.when(); var home_ou = search.home_ou; delete search.home_ou; var inactive = search.inactive; delete search.inactive; - console.debug('patron search ' + js2JSON(search)); + var fullSearch = { + search : search, + count : count, + sort : compileSort(), + inactive : inactive, + home_ou : home_ou, + offset : offset + }; + + if (patronSvc.lastSearch) { + // search repeated, return the cached results + if (angular.equals(fullSearch, patronSvc.lastSearch)) { + console.log('patron search returning ' + + patronSvc.patrons.length + ' cached results'); + + // notify has to happen after returning the promise + $timeout( + function() { + angular.forEach(patronSvc.patrons, function(user) { + deferred.notify(user); + }); + deferred.resolve(); + } + ); + return deferred.promise; + } + } else { + patronSvc.lastSearch = fullSearch; + } + + if (fullSearch.search.id) { + // search by user id performs a direct ID lookup + var userId = fullSearch.search.id.value; + $timeout( + function() { + egUser.get(userId).then(function(user) { + patronSvc.localFlesh(user); + patronSvc.patrons = [user]; + deferred.notify(user); + deferred.resolve(); + }); + } + ); + return deferred.promise; + } + + patronSvc.patrons = []; egNet.request( 'open-ils.actor', 'open-ils.actor.patron.search.advanced.fleshed', egAuth.token(), - search, - count, - compileSort(), - inactive, - home_ou, + fullSearch.search, + fullSearch.count, + fullSearch.sort, + fullSearch.inactive, + fullSearch.home_ou, egUser.defaultFleshFields, - offset + fullSearch.offset ).then( function() { deferred.resolve() }, null, // onerror function(user) { patronSvc.localFlesh(user); // inline + patronSvc.patrons.push(user); deferred.notify(user); } ); @@ -317,6 +346,7 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egEnv, provider.itemFieldValue = function(item, column) { return provider.nestedItemFieldValue(item, column); }; + $scope.patronSearchGridProvider = provider; // typeahead doesn't filter correctly with full hash objects, so @@ -349,10 +379,10 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egEnv, angular.forEach(args, function(val, key) { if (!val) return; if (key == 'profile') { - //search.profile = {value : args.profile.id, group : 0}; - search.profile = {value : search.profile, group : 0}; + search.profile = {value : args.profile.id, group : 0}; + //search.profile = {value : search.profile, group : 0}; } else if (key == 'home_ou') { - //search.home_ou = args.home_ou.id; // passed separately + search.home_ou = args.home_ou.id; // passed separately } else if (key == 'inactive') { search.inactive = val; } else { @@ -397,168 +427,20 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egEnv, } ); - console.log('sort = ' + js2JSON(sort)); return sort; } - // alt form which receives fleshed user objects - /* - function sendSearch(args) { - search = compileSearch(args); - - var home_ou = search.home_ou; - delete search.home_ou; - var inactive = search.inactive; - delete search.inactive; - - console.debug('patron search ' + js2JSON(search)); - egNet.request( - 'open-ils.actor', - 'open-ils.actor.patron.search.advanced.fleshed', - egAuth.token(), - search, - 50 // - compileSort(), - inactive, - home_ou, - egUser.defaultFleshFields - - ).then(null, null, function(user) { - patronSvc.localFlesh(user); - $scope.patrons.items[$scope.patrons.items.length] = user; + // search form submit action; tells the results grid to + // refresh itself. + $scope.search = function(args) { // args === $scope.searchArgs + if (args && Object.keys(args).length) $scope.patronSearchGridProvider.increment(); - }); - }; - */ - - // fetch users by id and add them to the patrons list - /* - function retrieveUsers(ids) { - angular.forEach(ids, function(id, idx) { - // capture idx to maintain search results order - egUser.get(id).then(function(user) { - patronSvc.localFlesh(user); - $scope.patrons.items[idx] = user; - }); - }); } - */ - - // collect form args fire patron search - $scope.search = function(args) { - if (args && Object.keys(args).length) { - //$scope.searchArgs = args; - //$scope.patronSearchGridProvider.increment(); - - if (args.profile) args.profile = args.profile.id; - if (args.home_ou) args.home_ou = args.home_ou.id; - - $location.search(args); - - /* - $scope. - $scope.patrons.reset(); - if (args.id) { - retrieveUsers([args.id]); - } else { - sendSearch(args); - } - */ - } - } - - // manage table row selection - /* - $scope.onPatronClick = function($event, user) { - $scope.lastSelected = user; - - // control-click / command-click (mac) selects - // or deselects a row without altering other rows - if ($event.ctrlKey || $event.metaKey) { - $scope.patrons.toggleOneSelection(user.id()); - - // middle-click opens new tab for the patron - } else if ($event.which == 2) { - - var url = $location.absUrl().replace( - /patron\/search.*$/, - 'patron/' + user.id() + '/checkout' - ); - $window.open(url); - - } else { - // vanilla click selects the patron as the current default - $scope.patrons.selectOne(user.id()); - patronSvc.setDefault(null, user); - } - } - */ + // TODO: move this into the (forthcoming) grid row activate action $scope.onPatronDblClick = function($event, user) { $location.path('/circ/patron/' + user.id() + '/checkout'); } - - // opens a new tab for each selected user at /checkout - // TODO: Chrome will only open one tab per user action (click, - // etc.). subsequent tabs open new windows (blocked by default). - // The only way around this I'm seeing is to use a chrome extension - // http://stackoverflow.com/questions/16749907/window-open-behaviour-in-chrome-tabs-windows - // for now, skip this feature and support control-click to open - // multiple patrons instead. - /* - $scope.openSelectedPatrons = function() { - angular.forEach( - $scope.patrons.selectedItems(), - function(patron) { - var url = $location.absUrl(); - url = url.replace(/patron\/search.*$/, - 'patron/' + patron.id() + '/checkout'); - $window.open(url); - } - ); - } - */ - - // handled up/down arrow events while the patrons results table is focused. - // disabled for now, since there are some UI issues to work out first: - // 1. up/down while a browser scroll bar is visible causes the browser to - // scroll, which makes sense, but is a little jarring. An overflow/scroll - // container would be better -- requires a non-table solution.. TODO - // 2. if table hover Bootstrap css is used, even though the currently - // selected row changes with arrow up/down, the mouse continues to - // hover in its original position, making the hovered row appear to be - // selected (style-wise) even when it's not. Disabling table-hover - // CSS works, but table-hover is useful, so... - - /* TODO: MOVE ME INTO GRID.js - $scope.navigateResults = function($event) { - // we can't select the next/previous user if we don't know - // which user was selected last. this should never happen, though. - if (!$scope.lastSelected) return; - - var user; - if ($event.which == 40) { // down arrow - angular.forEach( - $scope.patrons.items, - function(item, idx) { - if (item.id() == $scope.lastSelected.id()) - user = $scope.patrons.items[idx+1]; - } - ) - } else if ($event.which == 38) { // up arrow - angular.forEach( - $scope.patrons.items, - function(item, idx) { - if (item.id() == $scope.lastSelected.id()) - user = $scope.patrons.items[idx-1]; - } - ) - } - - if (user) $scope.onPatronClick($event, user); - } - */ - }]) /** * Manages patron summary view @@ -567,7 +449,7 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egEnv, ['$scope','$q','egNet','egAuth','egEvent','patronSvc', function($scope, $q, egNet, egAuth, egEvent, patronSvc) { // may not need this ctrl at all, since all data - // come directly from the scope + // comes directly from the scope }]) /** diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js index eb64e7a899..1d1dbb00e1 100644 --- a/Open-ILS/web/js/ui/default/staff/services/grid.js +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -32,7 +32,10 @@ angular.module('egGridMod', itemsProvider : '=', // comma-separated list of supported or disabled grid features - // TODO: examples + // supported features: + // -display : columns are hidden by default + // -sort : columns are unsortable by default + // -multisort : sort priorities config disabled by default features : '@', initialOffset : '=', @@ -60,7 +63,7 @@ angular.module('egGridMod', var grid = this; grid.init = function() { - grid.offset = $scope.initialOffset || 0; + grid.offset = 0; grid.limit = 25; grid.items = []; grid.selected = {}; // idField-based @@ -80,7 +83,8 @@ angular.module('egGridMod', grid.columnsProvider = egGridColumnsProvider.instance({ idlClass : grid.idlClass, defaultToHidden : (grid.features.indexOf('-display') > -1), - defaultToNoSort : (grid.features.indexOf('-sort') > -1) + defaultToNoSort : (grid.features.indexOf('-sort') > -1), + defaultToNoMultiSort : (grid.features.indexOf('-multisort') > -1) }); if ($scope.autoFields) { @@ -112,6 +116,16 @@ angular.module('egGridMod', }); } + // this allows the caller to pass in initializtion + // values, like offset, for cases when the caller may + // be caching grid data between route loads. + var conf = grid.dataProvider.initialize(); + if (conf) { + angular.forEach(conf, function(val, key) { + if (val !== null) grid[key] = val; + }); + } + grid.compileSort(); $scope.grid = grid; } @@ -348,8 +362,12 @@ angular.module('egGridMod', grid.toggleConfDisplay = function() { if (grid.showGridConf) { grid.showGridConf = false; - grid.compileSort(); - grid.collect(); + if (grid.columnsProvider.hasSortableColumn()) { + // only refresh the grid if the user has the + // ability to modify the sort priorities. + grid.compileSort(); + grid.collect(); + } } else { grid.showGridConf = true; } @@ -479,7 +497,14 @@ angular.module('egGridMod', // boolean fields are presented as value-less attributes angular.forEach( - ['visible', 'hidden', 'sortable', 'nonsortable'], + [ + 'visible', + 'hidden', + 'sortable', + 'nonsortable', + 'multisortable', + 'nonmultisortable', + ], function(field) { if (angular.isDefined(attrs[field])) scope[field] = true; @@ -499,6 +524,16 @@ angular.module('egGridMod', cols.idlClass = args.idlClass; cols.defaultToHidden = args.defaultToHidden; cols.defaultToNoSort = args.defaultToNoSort; + cols.defaultToNoMultiSort = args.defaultToNoMultiSort; + + // returns true if any columns are sortable + cols.hasSortableColumn = function() { + return cols.columns.filter( + function(col) { + return col.sortable || col.multisortable; + } + ).length > 0; + } cols.showAllColumns = function() { angular.forEach(cols.columns, function(column) { @@ -560,11 +595,13 @@ angular.module('egGridMod', path : colSpec.path, flex : Number(colSpec.flex) || 2, sort : Number(colSpec.sort) || 0, - sortable : colSpec.sortable, - nonsortable : colSpec.nonsortable, - visible : colSpec.visible, - hidden : colSpec.hidden, - datatype : colSpec.datatype + visible : colSpec.visible, + hidden : colSpec.hidden, + datatype : colSpec.datatype, + sortable : colSpec.sortable, + nonsortable : colSpec.nonsortable, + multisortable : colSpec.multisortable, + nonmultisortable : colSpec.nonmultisortable }; if (!column.name) column.name = column.path; @@ -576,6 +613,10 @@ angular.module('egGridMod', if (column.sortable || (!cols.defaultToNoSort && !column.nonsortable)) column.sortable = true; + if (column.multisortable || + (!cols.defaultToNoMultiSort && !column.nonmultisortable)) + column.multisortable = true; + cols.columns.push(column); if (fromIDL) return; @@ -636,57 +677,52 @@ angular.module('egGridMod', } }]) -// Factory service for egGridDataManager instances, which are -// responsible for collecting flattened grid data. -.factory('egGridFlatDataProvider', + +/* + * Generic data provider template class. This is basically an abstract + * class factory service whose instances can be locally modified to + * meet the needs of each individual grid. + */ +.factory('egGridDataProvider', ['$filter','egNet','egAuth','egIDL', function($filter , egNet , egAuth , egIDL) { - function FlatDataProvider(args) { + function GridDataProvider(args) { var gridData = this; - gridData.idlClass = args.idlClass; - gridData.query = args.query; - gridData.columnsProvider = args.columnsProvider; gridData.sort = []; gridData._revision = 0; + gridData.query = args.query; + gridData.idlClass = args.idlClass; + gridData.columnsProvider = args.columnsProvider; + + gridData.initialize = function() { + return {}; + } gridData.revision = function() { return gridData._revision; } + // incrementing the revision tells the grid that a data + // refresh is needed. gridData.increment = function() { gridData._revision++; } - gridData.get = function(index, count, onresponse) { - - // fetch data for all currently visible columns - var queryFields = {} - angular.forEach(gridData.columnsProvider.columns, function(col) { - if (gridData.columnsProvider.visible[col.name]) - queryFields[col.name] = col.path; - }); + // returns a promise whose notify() delivers items + gridData.get = function(index, count) { + console.error("egGridDataProvider.get() not implemented"); + } - return egNet.request( - 'open-ils.fielder', - 'open-ils.fielder.flattened_search', - egAuth.token(), gridData.idlClass, queryFields, - gridData.query, - { sort : gridData.sort, - limit : count, - offset : index - } - ); + // called when one or more items are selected in the grid + gridData.select = function(items) { } - gridData.itemFieldValue = function(item, column) { - // all of our data is flat + gridData.flatItemFieldValue = function(item, column) { 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 @@ -728,7 +764,47 @@ angular.module('egGridMod', return { instance : function(args) { - return new FlatDataProvider(args); + return new GridDataProvider(args); + } + }; + } +]) + + +// Factory service for egGridDataManager instances, which are +// responsible for collecting flattened grid data. +.factory('egGridFlatDataProvider', + ['egNet','egAuth','egGridDataProvider', + function(egNet , egAuth , egGridDataProvider) { + + return { + instance : function(args) { + var provider = egGridDataProvider.instance(args); + + provider.get = function(offset, count) { + + // find all of the currently visible columns + var queryFields = {} + angular.forEach(provider.columnsProvider.columns, + function(col) { + if (provider.columnsProvider.visible[col.name]) + queryFields[col.name] = col.path; + } + ); + + return egNet.request( + 'open-ils.fielder', + 'open-ils.fielder.flattened_search', + egAuth.token(), provider.idlClass, queryFields, + provider.query, + { sort : provider.sort, + limit : count, + offset : index + } + ); + } + provider.itemFieldValue = provider.flatItemFieldValue; + return provider; } }; } -- 2.11.0