LP#1697954: Provide client-side sorting for grids that can use it
authorMike Rylander <mrylander@gmail.com>
Wed, 14 Jun 2017 15:32:36 +0000 (11:32 -0400)
committerMike Rylander <mrylander@gmail.com>
Fri, 30 Jun 2017 14:44:29 +0000 (10:44 -0400)
There are several grids (items out, checkin, checkout, item status, etc) that
could benefit from the ability to sort their items, but either the data
provider uses a complex data structure or an API call that doesn't offer
sorting, or the item list is populated by user input rather than a call to the
server.  In those cases, sorting is not available.  However, if we know that
all the data in the grid is in client memory, it would be reasonable to offer
a client-side sort option.

This commit does that by teaching the grid to accept a "clientsort" feature
and teaching arrayNotifier how to sort the items currently stored.  The sort
works over any mix of IDL objects, hashes, and flattened fields, supports
multisort, and pushes "nulls" to the end of the list.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Andrea Neiman <abneiman@equinoxinitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/web/js/ui/default/staff/services/grid.js

index 5fe4355..96c4a9e 100644 (file)
@@ -179,6 +179,7 @@ angular.module('egGridMod',
 
                 grid.columnsProvider = egGridColumnsProvider.instance({
                     idlClass : grid.idlClass,
+                    clientSort : (features.indexOf('clientsort') > -1 && features.indexOf('-clientsort') == -1),
                     defaultToHidden : (features.indexOf('-display') > -1),
                     defaultToNoSort : (features.indexOf('-sort') > -1),
                     defaultToNoMultiSort : (features.indexOf('-multisort') > -1),
@@ -210,6 +211,8 @@ angular.module('egGridMod',
                     });
                 }
 
+                grid.dataProvider.columnsProvider = grid.columnsProvider;
+
                 $scope.itemFieldValue = grid.dataProvider.itemFieldValue;
                 $scope.indexValue = function(item) {
                     return grid.indexValue(item)
@@ -1082,6 +1085,7 @@ angular.module('egGridMod',
         restrict : 'AE',
         scope : {
             flesher: '=', // optional; function that can flesh a linked field, given the value
+            comparator: '=', // optional; function that can sort the thing at the end of 'path' 
             name  : '@', // required; unique name
             path  : '@', // optional; flesh path
             ignore: '@', // optional; fields to ignore when path is a wildcard
@@ -1170,6 +1174,7 @@ angular.module('egGridMod',
         cols.columns = [];
         cols.stockVisible = [];
         cols.idlClass = args.idlClass;
+        cols.clientSort = args.clientSort;
         cols.defaultToHidden = args.defaultToHidden;
         cols.defaultToNoSort = args.defaultToNoSort;
         cols.defaultToNoMultiSort = args.defaultToNoMultiSort;
@@ -1362,6 +1367,7 @@ angular.module('egGridMod',
         cols.cloneFromScope = function(colSpec) {
             return {
                 flesher  : colSpec.flesher,
+                comparator  : colSpec.comparator,
                 name  : colSpec.name,
                 label : colSpec.label,
                 path  : colSpec.path,
@@ -1520,6 +1526,81 @@ angular.module('egGridMod',
             // the range defined by count and offset
             gridData.arrayNotifier = function(arr, offset, count) {
                 if (!arr || arr.length == 0) return $q.when();
+
+                if (gridData.columnsProvider.clientSort
+                    && gridData.sort
+                    && gridData.sort.length > 0
+                ) {
+                    var sorter_cache = [];
+                    arr.sort(function(a,b) {
+                        for (var si = 0; si < gridData.sort.length; si++) {
+                            if (!sorter_cache[si]) { // Build sort structure on first comparison, reuse thereafter
+                                var field = gridData.sort[si];
+                                var dir = 'asc';
+
+                                if (angular.isObject(field)) {
+                                    dir = Object.values(field)[0];
+                                    field = Object.keys(field)[0];
+                                }
+
+                                var path = gridData.columnsProvider.findColumn(field).path || field;
+                                var comparator = gridData.columnsProvider.findColumn(field).comparator ||
+                                    function (x,y) { if (x < y) return -1; if (x > y) return 1; return 0 };
+
+                                sorter_cache[si] = {
+                                    field       : path,
+                                    dir         : dir,
+                                    comparator  : comparator
+                                };
+                            }
+
+                            var sc = sorter_cache[si];
+
+                            var af,bf;
+
+                            if (a._isfieldmapper || angular.isFunction(a[sc.field])) {
+                                try {af = a[sc.field](); bf = b[sc.field]() } catch (e) {};
+                            } else {
+                                af = a[sc.field]; bf = b[sc.field];
+                            }
+                            if (af === undefined && sc.field.indexOf('.') > -1) { // assume an object, not flat path
+                                var parts = sc.field.split('.');
+                                af = a;
+                                bf = b;
+                                angular.forEach(parts, function (p) {
+                                    if (af) {
+                                        if (af._isfieldmapper || angular.isFunction(af[p])) af = af[p]();
+                                        else af = af[p];
+                                    }
+                                    if (bf) {
+                                        if (bf._isfieldmapper || angular.isFunction(bf[p])) bf = bf[p]();
+                                        else bf = bf[p];
+                                    }
+                                });
+                            }
+
+                            if (af === undefined) af = null;
+                            if (bf === undefined) bf = null;
+
+                            if (af === null && bf !== null) return 1;
+                            if (bf === null && af !== null) return -1;
+
+                            if (!(bf === null && af === null)) {
+                                var partial = sc.comparator(af,bf);
+                                if (partial) {
+                                    if (sc.dir == 'desc') {
+                                        if (partial > 0) return -1;
+                                        return 1;
+                                    }
+                                    return partial;
+                                }
+                            }
+                        }
+
+                        return 0;
+                    });
+                }
+
                 if (count) arr = arr.slice(offset, offset + count);
                 var def = $q.defer();
                 // promise notifications are only witnessed when delivered