From b98f8efea5a1937704c94b3de534d9028fd09d40 Mon Sep 17 00:00:00 2001 From: Mike Rylander <mrylander@gmail.com> Date: Tue, 3 Jul 2018 16:57:27 -0400 Subject: [PATCH] LP#1732761: Batch item edit and multiple values per field Previous to this commit, the display of multiple different values for a field in the item attribute editor was simply to display no value. Here we add a UI component that presents the list of unique values, the number of selected copies that use each value, and the ability to select just those copies using a particular value by clicking on the desired value. Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Kathy Lussier <klussier@masslnc.org> Conflicts: Open-ILS/src/templates/staff/cat/volcopy/index.tt2 Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 --- Open-ILS/src/templates/staff/cat/volcopy/index.tt2 | 8 ++ .../templates/staff/cat/volcopy/t_attr_edit.tt2 | 54 +++++++++++++ .../src/templates/staff/share/t_listcounts.tt2 | 11 +++ .../web/js/ui/default/staff/cat/volcopy/app.js | 89 ++++++++++++++++++++++ Open-ILS/web/js/ui/default/staff/services/grid.js | 19 +++++ Open-ILS/web/js/ui/default/staff/services/ui.js | 44 +++++++++++ 6 files changed, 225 insertions(+) create mode 100644 Open-ILS/src/templates/staff/share/t_listcounts.tt2 diff --git a/Open-ILS/src/templates/staff/cat/volcopy/index.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/index.tt2 index 385c904226..d50b450980 100644 --- a/Open-ILS/src/templates/staff/cat/volcopy/index.tt2 +++ b/Open-ILS/src/templates/staff/cat/volcopy/index.tt2 @@ -15,6 +15,14 @@ angular.module('egCoreMod').run(['egStrings', function(s) { s.VOL_COPY_TEMPLATE_SUCCESS_SAVE = "[% l('Saved holdings template(s)') %]"; s.VOL_COPY_TEMPLATE_SUCCESS_DELETE = "[% l('Deleted holdings template') %]"; + s.SHORT = "[% l('Short') %]"; + s.LOW = "[% l('Low') %]"; + s.NORMAL = "[% l('Normal') %]"; + s.EXTENDED = "[% l('Extended') %]"; + s.HIGH = "[% l('High') %]"; + s.UNSET = "[% l('UNSET') %]"; + s.YES = "[% l('Yes') %]"; + s.NO = "[% l('No') %]"; [%# Note the "~" characters escape the gettext brackets %] s.COPY_NOTE_INITIALS = "[% l('[_1] ~[ [_2] @ [_3] ~]', '{{value}}', '{{initials}}', '{{ws_ou}}') %]" diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 index ffbaebaba7..b133c7ae31 100644 --- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 +++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 @@ -120,12 +120,18 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.circulate.length > 1 && working.circulate === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.circulate" render="labelYesNo" on-select="select_by_circulate"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.status !== undefined}"> <select class="form-control" ng-disabled="!defaults.attributes.status" ng-model="working.status" ng-options="s.id() as s.name() disable when magic_status_list.indexOf(s.id(),0) > -1 for s in status_list"> </select> + <div class="container" ng-show="working.MultiMap.status.length > 1 && working.status === undefined"> + <eg-list-counts label="[% l('Multiple statuses') %]" list="working.MultiMap.status" render="statusName" on-select="select_by_status"></eg-list-counts> + </div> </div> </div> @@ -149,6 +155,9 @@ label="[% l('(Unset)') %]" disable-test="cant_have_vols" ></eg-org-selector> + <div class="container" ng-show="working.MultiMap.circ_lib.length > 1 && working.circ_lib === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.circ_lib" render="orgShortname" on-select="select_by_circ_lib"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.ref !== undefined}"> <div class="row"> @@ -165,6 +174,9 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.ref.length > 1 && working.ref === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.ref" render="labelYesNo" on-select="select_by_ref"></eg-list-counts> + </div> </div> </div> @@ -185,6 +197,9 @@ ng-disabled="!defaults.attributes.location" ng-model="working.location" ng-options="l.id() as i18n.ou_qualified_location_name(l) for l in location_list" ></select> + <div class="container" ng-show="working.MultiMap.location.length > 1 && working.location === undefined"> + <eg-list-counts label="[% l('Multiple locations') %]" list="working.MultiMap.location" render="locationName" on-select="select_by_location"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.opac_visible !== undefined}"> <div class="row"> @@ -201,6 +216,9 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.opac_visible.length > 1 && working.opac_visible === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.opac_visible" render="labelYesNo" on-select="select_by_opac_visible"></eg-list-counts> + </div> </div> </div> @@ -223,9 +241,15 @@ > <option value="">[% l('<NONE>') %]</option> </select> + <div class="container" ng-show="working.MultiMap.circ_modifier.length > 1 && working.circ_modifier === undefined"> + <eg-list-counts label="[% l('Multiple modifiers') %]" list="working.MultiMap.circ_modifier" render="circmodName" on-select="select_by_circ_modifier"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.price !== undefined}"> <input class="form-control" ng-disabled="!defaults.attributes.price" str-to-float ng-model="working.price" type="number" step="0.01"/> + <div class="container" ng-show="working.MultiMap.price.length > 1 && working.price === undefined"> + <eg-list-counts label="[% l('Multiple prices') %]" list="working.MultiMap.price" on-select="select_by_price"></eg-list-counts> + </div> </div> </div> @@ -247,9 +271,15 @@ <option value="2" selected>[% l('Normal') %]</option> <option value="3">[% l('Extended') %]</option> </select> + <div class="container" ng-show="working.MultiMap.loan_duration.length > 1 && working.loan_duration === undefined"> + <eg-list-counts label="[% l('Multiple durations') %]" list="working.MultiMap.loan_duration" render="durationLabel" on-select="select_by_loan_duration"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.cost !== undefined}"> <input class="form-control" ng-disabled="!defaults.attributes.cost" str-to-float ng-model="working.cost" type="number" step="0.01"/> + <div class="container" ng-show="working.MultiMap.cost.length > 1 && working.cost === undefined"> + <eg-list-counts label="[% l('Multiple costs') %]" list="working.MultiMap.cost" on-select="select_by_cost"></eg-list-counts> + </div> </div> </div> @@ -271,6 +301,9 @@ ng-options="t.code() as t.value() for t in circ_type_list"> <option value="">[% l('<NONE>') %]</option> </select> + <div class="container" ng-show="working.MultiMap.circ_as_type.length > 1 && working.circ_as_type === undefined"> + <eg-list-counts label="[% l('Multiple types') %]" list="working.MultiMap.circ_as_type" render="circTypeValue" on-select="select_by_circ_as_type"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.deposit !== undefined}"> <div class="row"> @@ -287,6 +320,9 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.deposit.length > 1 && working.deposit === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.deposit" render="labelYesNo" on-select="select_by_deposit"></eg-list-counts> + </div> </div> </div> @@ -317,9 +353,15 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.holdable.length > 1 && working.holdable === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.holdable" render="labelYesNo" on-select="select_by_holdable"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.deposit_amount !== undefined}"> <input class="form-control" ng-disabled="!defaults.attributes.deposit_amount" str-to-float ng-model="working.deposit_amount" type="number" step="0.01"/> + <div class="container" ng-show="working.MultiMap.deposit_amount.length > 1 && working.deposit_amount === undefined"> + <eg-list-counts label="[% l('Multiple amounts') %]" list="working.MultiMap.deposit_amount" on-select="select_by_deposit_amount"></eg-list-counts> + </div> </div> </div> @@ -341,6 +383,9 @@ ng-options="a.id() as a.name() for a in age_protect_list"> <option value="">[% l('<NONE>') %]</option> </select> + <div class="container" ng-show="working.MultiMap.age_protect.length > 1 && working.age_protect === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.age_protect" render="ageprotectName" on-select="select_by_age_protect"></eg-list-counts> + </div> </div> <div class="col-md-6" ng-class="{'bg-success': working.mint_condition !== undefined}"> <div class="row"> @@ -357,6 +402,9 @@ </label> </div> </div> + <div class="container" ng-show="working.MultiMap.mint_condition.length > 1 && working.mint_condition === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.mint_condition" render="labelYesNo" on-select="select_by_mint_condition"></eg-list-counts> + </div> </div> </div> @@ -375,6 +423,9 @@ <option value="2" selected>[% l('Normal') %]</option> <option value="3">[% l('High') %]</option> </select> + <div class="container" ng-show="working.MultiMap.fine_level.length > 1 && working.fine_level === undefined"> + <eg-list-counts label="[% l('Multiple levels') %]" list="working.MultiMap.fine_level" render="fineLabel" on-select="select_by_fine_level"></eg-list-counts> + </div> </div> <div class="col-md-6"> <button @@ -409,6 +460,9 @@ ng-options="a.id() as a.name() for a in floating_list"> <option value="">[% l('<NONE>') %]</option> </select> + <div class="container" ng-show="working.MultiMap.floating.length > 1 && working.floating === undefined"> + <eg-list-counts label="[% l('Multiple values') %]" list="working.MultiMap.floating" render="floatingName" on-select="select_by_floating"></eg-list-counts> + </div> </div> <div class="col-md-6"> <button diff --git a/Open-ILS/src/templates/staff/share/t_listcounts.tt2 b/Open-ILS/src/templates/staff/share/t_listcounts.tt2 new file mode 100644 index 0000000000..e32d061a53 --- /dev/null +++ b/Open-ILS/src/templates/staff/share/t_listcounts.tt2 @@ -0,0 +1,11 @@ +<div class="btn-group" uib-dropdown> + <button type="button" class="btn btn-default" uib-dropdown-toggle> + {{label}} + <span class="caret"></span> + </button> + <ul uib-dropdown-menu class="uib-dropdown-menu"> + <li ng-repeat="item in count_hash"> + <a href ng-click="selectValue(item.original)">{{item.value}} ({{item.count}})</a> + </li> + </ul> +</div> diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js index ec422072b6..2efb600b4e 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js @@ -1121,6 +1121,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , var newval = $scope.working[field]; if (typeof newval != 'undefined') { + delete $scope.working.MultiMap[field]; if (angular.isObject(newval)) { // we'll use the pkey if (newval.id) newval = newval.id(); else if (newval.code) newval = newval.code(); @@ -1152,6 +1153,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , } $scope.working = { + MultiMap: {}, statcats: {}, statcats_multi: {}, statcat_filter: undefined @@ -1312,6 +1314,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , }); } }); + delete $scope.working.MultiMap[k]; egCore.hatch.setItem('cat.copy.last_template', n); } @@ -1395,6 +1398,87 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , $scope.add_vols_copies = false; $scope.is_fast_add = false; + // Generate some functions for selecting items by column value in the working grid + angular.forEach( + ['circulate','status','circ_lib','ref','location','opac_visible','circ_modifier','price', + 'loan_duration','cost','circ_as_type','deposit','holdable','deposit_amount','age_protect', + 'mint_condition','fine_level','floating'], + function (field) { + $scope['select_by_' + field] = function (x) { + $scope.workingGridControls.selectItemsByValue(field,x); + } + } + ); + + var truthy = /^t|1/; + $scope.labelYesNo = function (x) { + return truthy.test(x) ? egCore.strings.YES : egCore.strings.NO; + } + + $scope.orgShortname = function (x) { + return egCore.org.get(x).shortname(); + } + + $scope.statusName = function (x) { + var s = $scope.status_list.filter(function(y) { + return y.id() == x; + }); + + return s[0].name(); + } + + $scope.locationName = function (x) { + var s = $scope.location_list.filter(function(y) { + return y.id() == x; + }); + + return $scope.i18n.ou_qualified_location_name(s[0]); + } + + $scope.durationLabel = function (x) { + return [egCore.strings.SHORT, egCore.strings.NORMAL, egCore.strings.EXTENDED][-1 + x] + } + + $scope.fineLabel = function (x) { + return [egCore.strings.LOW, egCore.strings.NORMAL, egCore.strings.HIGH][-1 + x] + } + + $scope.circTypeValue = function (x) { + if (x === null) return egCore.strings.UNSET; + var s = $scope.circ_type_list.filter(function(y) { + return y.code() == x; + }); + + return s[0].value(); + } + + $scope.ageprotectName = function (x) { + if (x === null) return egCore.strings.UNSET; + var s = $scope.age_protect_list.filter(function(y) { + return y.id() == x; + }); + + return s[0].name(); + } + + $scope.floatingName = function (x) { + if (x === null) return egCore.strings.UNSET; + var s = $scope.floating_list.filter(function(y) { + return y.id() == x; + }); + + return s[0].name(); + } + + $scope.circmodName = function (x) { + if (x === null) return egCore.strings.UNSET; + var s = $scope.circ_modifier_list.filter(function(y) { + return y.code() == x; + }); + + return s[0].name(); + } + egNet.request( 'open-ils.actor', 'open-ils.actor.anon_cache.get_value', @@ -1600,6 +1684,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , angular.forEach(Object.keys($scope.defaults.attributes), function (attr) { var value_hash = {}; + var value_list = []; angular.forEach(item_list, function (item) { if (item[attr]) { var v = item[attr]() @@ -1607,10 +1692,13 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , if (v.id) v = v.id(); else if (v.code) v = v.code(); } + value_list.push(v); value_hash[v] = 1; } }); + $scope.working.MultiMap[attr] = value_list; + if (Object.keys(value_hash).length == 1) { if (attr == 'circ_lib') { $scope.working[attr] = egCore.org.get(item_list[0][attr]()); @@ -2401,6 +2489,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , $scope.clearWorking = function () { angular.forEach($scope.working, function (v,k,o) { + $scope.working.MultiMap[k] = []; if (!angular.isObject(v)) { if (typeof v != 'undefined') $scope.working[k] = undefined; 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 47690ca913..a1f8d5fee0 100644 --- a/Open-ILS/web/js/ui/default/staff/services/grid.js +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -283,6 +283,10 @@ angular.module('egGridMod', return grid.getSelectedItems() } + controls.selectItemsByValue = function(c,v) { + return grid.selectItemsByValue(c,v) + } + controls.allItems = function() { return $scope.items; } @@ -743,6 +747,21 @@ angular.module('egGridMod', $scope.selected[index] = true; } + // selects items by a column value, first clearing selected list. + // we overwrite the object so that we can watch $scope.selected + grid.selectItemsByValue = function(column, value) { + $scope.selected = {}; + angular.forEach($scope.items, function(item) { + var col_value; + if (angular.isFunction(item[column])) + col_value = item[column](); + else + col_value = item[column]; + + if (value == col_value) $scope.selected[grid.indexValue(item)] = true + }); + } + // selects or deselects an item, without affecting the others. // returns true if the item is selected; false if de-selected. // we overwrite the object so that we can watch $scope.selected diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js index 8f38e1cd8f..ed5f84f007 100644 --- a/Open-ILS/web/js/ui/default/staff/services/ui.js +++ b/Open-ILS/web/js/ui/default/staff/services/ui.js @@ -1003,6 +1003,50 @@ function($uibModal , $interpolate , egCore) { }; }) +.directive('egListCounts', function() { + return { + restrict: 'E', + replace: true, + scope: { + label: "@", + list: "=", // list of things + render: "=", // function to turn thing into string; default to stringification + onSelect: "=" // function to fire when option selected. passed one copy of the selected value + }, + templateUrl: './share/t_listcounts', + controller: ['$scope','$timeout', + function( $scope , $timeout ) { + + $scope.isopen = false; + $scope.count_hash = {}; + + $scope.renderer = $scope.render ? $scope.render : function (x) { return ""+x }; + + $scope.$watchCollection('list',function() { + $scope.count_hash = {}; + angular.forEach($scope.list, function (item) { + var str = $scope.renderer(item); + if (!$scope.count_hash[str]) { + $scope.count_hash[str] = { + count : 1, + value : str, + original : item + }; + } else { + $scope.count_hash[str].count++; + } + }); + }); + + $scope.selectValue = function (item) { + if ($scope.onSelect) $scope.onSelect(item); + } + + } + ] + }; +}) + /** * Nested org unit selector modeled as a Bootstrap dropdown button. */ -- 2.11.0