LP#1732761: Batch item edit and multiple values per field
authorMike Rylander <mrylander@gmail.com>
Tue, 3 Jul 2018 20:57:27 +0000 (16:57 -0400)
committerKathy Lussier <klussier@masslnc.org>
Thu, 20 Sep 2018 16:20:21 +0000 (12:20 -0400)
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
Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
Open-ILS/src/templates/staff/share/t_listcounts.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
Open-ILS/web/js/ui/default/staff/services/grid.js
Open-ILS/web/js/ui/default/staff/services/ui.js

index 385c904..d50b450 100644 (file)
 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') %]";
 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}}') %]"
     [%# Note the "~" characters escape the gettext brackets %]
     s.COPY_NOTE_INITIALS = 
       "[% l('[_1] ~[ [_2] @ [_3] ~]', '{{value}}', '{{initials}}', '{{ws_ou}}') %]"
index ffbaeba..b133c7a 100644 (file)
                             </label>
                         </div>
                     </div>
                             </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>
                 <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>
 
                 </div>
             </div>
 
                         label="[% l('(Unset)') %]"
                         disable-test="cant_have_vols"
                     ></eg-org-selector>
                         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">
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.ref !== undefined}">
                     <div class="row">
                             </label>
                         </div>
                     </div>
                             </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>
 
                 </div>
             </div>
 
                         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>
                         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">
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.opac_visible !== undefined}">
                     <div class="row">
                             </label>
                         </div>
                     </div>
                             </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>
 
                 </div>
             </div>
 
                     >
                         <option value="">[% l('&lt;NONE&gt;') %]</option>
                     </select>
                     >
                         <option value="">[% l('&lt;NONE&gt;') %]</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>
                 <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>
 
                 </div>
             </div>
 
                         <option value="2" selected>[% l('Normal') %]</option>
                         <option value="3">[% l('Extended') %]</option>
                     </select>
                         <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>
                 <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>
 
                 </div>
             </div>
 
                         ng-options="t.code() as t.value() for t in circ_type_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</option>
                     </select>
                         ng-options="t.code() as t.value() for t in circ_type_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</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">
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.deposit !== undefined}">
                     <div class="row">
                             </label>
                         </div>
                     </div>
                             </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>
 
                 </div>
             </div>
 
                             </label>
                         </div>
                     </div>
                             </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>
                 <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>
 
                 </div>
             </div>
 
                         ng-options="a.id() as a.name() for a in age_protect_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</option>
                     </select>
                         ng-options="a.id() as a.name() for a in age_protect_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</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">
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.mint_condition !== undefined}">
                     <div class="row">
                             </label>
                         </div>
                     </div>
                             </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>
 
                 </div>
             </div>
 
                         <option value="2" selected>[% l('Normal') %]</option>
                         <option value="3">[% l('High') %]</option>
                     </select>
                         <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
                 </div>
                 <div class="col-md-6">
                     <button
                         ng-options="a.id() as a.name() for a in floating_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</option>
                     </select>
                         ng-options="a.id() as a.name() for a in floating_list">
                       <option value="">[% l('&lt;NONE&gt;') %]</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
                 </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 (file)
index 0000000..e32d061
--- /dev/null
@@ -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>
index ec42207..2efb600 100644 (file)
@@ -1121,6 +1121,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             var newval = $scope.working[field];
 
             if (typeof newval != 'undefined') {
             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();
                 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 = {
     }
 
     $scope.working = {
+        MultiMap: {},
         statcats: {},
         statcats_multi: {},
         statcat_filter: undefined
         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);
         }
 
             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;
 
         $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',
         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 = {};
                 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]()
                     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();
                             }
                                 if (v.id) v = v.id();
                                 else if (v.code) v = v.code();
                             }
+                            value_list.push(v);
                             value_hash[v] = 1;
                         }
                     });
 
                             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]());
                     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.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;
                         if (!angular.isObject(v)) {
                             if (typeof v != 'undefined')
                                 $scope.working[k] = undefined;
index 47690ca..a1f8d5f 100644 (file)
@@ -283,6 +283,10 @@ angular.module('egGridMod',
                     return grid.getSelectedItems()
                 }
 
                     return grid.getSelectedItems()
                 }
 
+                controls.selectItemsByValue = function(c,v) {
+                    return grid.selectItemsByValue(c,v)
+                }
+
                 controls.allItems = function() {
                     return $scope.items;
                 }
                 controls.allItems = function() {
                     return $scope.items;
                 }
@@ -743,6 +747,21 @@ angular.module('egGridMod',
                 $scope.selected[index] = true;
             }
 
                 $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
             // 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
index 8f38e1c..ed5f84f 100644 (file)
@@ -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.
  */
 /**
  * Nested org unit selector modeled as a Bootstrap dropdown button.
  */