From 679459b83b78ee5c910566d94507c935b3f7fbda Mon Sep 17 00:00:00 2001 From: Alexander Lin Date: Sun, 4 Dec 2016 22:19:42 -0800 Subject: [PATCH 1/3] Adds free-text filter to equipment page Closes #8241 --- website/client-old/css/inventory.styl | 4 ++ .../js/controllers/inventoryCtrl.js | 45 +++++++++++++++++++ .../views/options/inventory/equipment.jade | 14 ++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/website/client-old/css/inventory.styl b/website/client-old/css/inventory.styl index 9ba3728bd9..d792ccf014 100644 --- a/website/client-old/css/inventory.styl +++ b/website/client-old/css/inventory.styl @@ -31,6 +31,10 @@ vertical-align: bottom margin-right: 20px +.equipment-search + max-width: 300px + margin-right: 20px + .well.use-costume-info margin-top: 10px p:first-child diff --git a/website/client-old/js/controllers/inventoryCtrl.js b/website/client-old/js/controllers/inventoryCtrl.js index 312c6d1c9c..4bbf57e06b 100644 --- a/website/client-old/js/controllers/inventoryCtrl.js +++ b/website/client-old/js/controllers/inventoryCtrl.js @@ -85,6 +85,42 @@ habitrpg.controller("InventoryCtrl", }) }, true); + var equipmentSearch = function(equipment, term) { + if (!equipment) return; + if (!angular.isString(term) || term.length == 0) { + return equipment; + } + termMatcher = new RegExp(term, 'i'); + + var result = []; + for (var i = 0; i < equipment.length; i++) { + if (termMatcher.test(equipment[i].text())) { + result.push(equipment[i]); + } + } + return result; + }; + + $scope.$watchGroup(['gearByClass', 'gearByType', 'user.equipmentQuery'], function(updatedVals) { + var gearByClass = updatedVals[0]; + var gearByType = updatedVals[1]; + var equipmentQuery = updatedVals[2]; + $scope.filteredGearByClass = {}; + $scope.filteredGearByType = {}; + angular.forEach(gearByClass, function(value, key) { + var searchResult = equipmentSearch(value, equipmentQuery); + if (searchResult.length > 0) { + $scope.filteredGearByClass[key] = searchResult; + } + }); + angular.forEach(gearByType, function(value, key) { + var searchResult = equipmentSearch(value, equipmentQuery); + if (searchResult.length > 0) { + $scope.filteredGearByType[key] = searchResult; + } + }); + }); + $scope.chooseEgg = function(egg){ if ($scope.selectedEgg && $scope.selectedEgg.key == egg) { return $scope.selectedEgg = null; // clicked same egg, unselect @@ -420,3 +456,12 @@ habitrpg.controller("InventoryCtrl", } } ]); + +habitrpg.controller("InventorySearchCtrl", + ['$scope', 'User', + function($scope, User) { + $scope.updateEquipmentQuery = function() { + User.user.equipmentQuery = $scope.equipmentQuery; + } + } +]); diff --git a/website/views/options/inventory/equipment.jade b/website/views/options/inventory/equipment.jade index 1075501d00..1872e4d801 100644 --- a/website/views/options/inventory/equipment.jade +++ b/website/views/options/inventory/equipment.jade @@ -31,16 +31,22 @@ mixin equipmentButton(type) popover-append-to-body='true') mixin equipmentList(equipmentType) - menu.pets-menu(label='{{::label}}', ng-show='gearByClass[klass]', ng-if='groupingChoice === "klass"', + menu.pets-menu(label='{{::label}}', ng-show='filteredGearByClass[klass]', ng-if='groupingChoice === "klass"', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}') - div(ng-repeat='item in gearByClass[klass] | orderBy: order') + div(ng-repeat='item in filteredGearByClass[klass] | orderBy: order') +equipmentButton(equipmentType) - menu.pets-menu(label='{{::label}}', ng-show='gearByType[type]', ng-if='groupingChoice === "equipmentType"', + menu.pets-menu(label='{{::label}}', ng-show='filteredGearByType[type]', ng-if='groupingChoice === "equipmentType"', ng-repeat='(type,label) in {headAccessory:env.t("headAccessoryCapitalized"), head:env.t("headgearCapitalized"), eyewear:env.t("eyewear"), weapon:env.t("weaponCapitalized"), shield:env.t("offhandCapitalized"), armor:env.t("armorCapitalized"), body:env.t("body"), back:env.t("back")}') - div(ng-repeat='item in gearByType[type] | orderBy: order', ng-show='item.klass !== "base"') + div(ng-repeat='item in filteredGearByType[type] | orderBy: order', ng-show='item.klass !== "base"') +equipmentButton(equipmentType) .container-fluid + .row + .col-md-6 + .input-group.equipment-search(ng-controller="InventorySearchCtrl") + .input-group-addon + .glyphicon.glyphicon-search + input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery', ng-change='updateEquipmentQuery()', ng-model-options='{ debounce: 250 }') .row .col-md-6.border-right(ng-controller="SortableInventoryController") h3.equipment-title.hint(popover-trigger='mouseenter', From 3e92bb22faf43da56d2c28548261c299c524dd89 Mon Sep 17 00:00:00 2001 From: Alexander Lin Date: Wed, 7 Dec 2016 00:04:42 -0800 Subject: [PATCH 2/3] Refactor, add spec tests for equipment search --- .../spec/controllers/inventoryCtrlSpec.js | 80 +++++++++++++++++++ .../js/controllers/inventoryCtrl.js | 37 +++++---- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/test/client-old/spec/controllers/inventoryCtrlSpec.js b/test/client-old/spec/controllers/inventoryCtrlSpec.js index 955b6f4f0c..5639fb6dc5 100644 --- a/test/client-old/spec/controllers/inventoryCtrlSpec.js +++ b/test/client-old/spec/controllers/inventoryCtrlSpec.js @@ -450,4 +450,84 @@ describe('Inventory Controller', function() { expect(scope.hasAllTimeTravelerItemsOfType('mounts')).to.eql(true); })); }); + + describe('Gear search filter', function() { + var wrap = function(text) { + return {'text': function() {return text;}}; + } + + var toText = function(list) { + return _.map(list, function(ele) { return ele.text(); }); + } + + var gearByClass, gearByType; + + beforeEach(function() { + scope.$digest(); + gearByClass = {'raw': [wrap('kale'), wrap('sashimi')], + 'cooked': [wrap('chicken'), wrap('potato')]}; + + gearByType = {'veg': [wrap('kale'), wrap('potato')], + 'not': [wrap('chicken'), wrap('sashimi')]}; + scope.gearByClass = gearByClass; + scope.gearByType = gearByType; + scope.equipmentFilterQuery.query = 'a'; + }); + + it('filters nothing if equipmentQuery is nothing', function() { + scope.equipmentFilterQuery.query = ''; + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken', 'sashimi']); + }); + + it('filters out gear if class gear changes', function() { + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato']); + + scope.gearByClass['raw'].push(wrap('zucchini')); + scope.gearByClass['cooked'].push(wrap('pizza')); + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato', 'pizza']); + }); + + it('filters out gear if typed gear changes', function() { + scope.$digest(); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi']); + + scope.gearByType['veg'].push(wrap('zucchini')); + scope.gearByType['not'].push(wrap('pizza')); + + scope.$digest(); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi', 'pizza']); + }); + + it('filters out gear if filter query changes', function() { + scope.equipmentFilterQuery.query = 'c'; + scope.$digest(); + + expect(toText(scope.filteredGearByClass['raw'])).to.eql([]); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken']); + expect(toText(scope.filteredGearByType['veg'])).to.eql([]); + expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken']); + }); + + it('returns the right filtered gear', function() { + var equipment = [wrap('spicy tuna'), wrap('dragon'), wrap('rainbow'), wrap('caterpillar')]; + expect(toText(scope.equipmentSearch(equipment, 'ra'))).to.eql(['dragon', 'rainbow']); + }); + + it('returns the right filtered gear if the source gear has unicode', function() { + // blue hat, red hat, red shield + var equipment = [wrap('藍色軟帽'), wrap('紅色軟帽'), wrap('紅色盾牌')]; + // searching for 'red' gives red hat, red shield + expect(toText(scope.equipmentSearch(equipment, '紅色'))).to.eql(['紅色軟帽', '紅色盾牌']); + }); + }); }); diff --git a/website/client-old/js/controllers/inventoryCtrl.js b/website/client-old/js/controllers/inventoryCtrl.js index 4bbf57e06b..d7175a5832 100644 --- a/website/client-old/js/controllers/inventoryCtrl.js +++ b/website/client-old/js/controllers/inventoryCtrl.js @@ -8,6 +8,7 @@ habitrpg.controller("InventoryCtrl", $scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5} $scope.selectedPotion = null; // {index: 5, name: "Red", value: 3} + $scope.equipmentFilterQuery = {'query': ''}; _updateDropAnimalCount(user.items); @@ -85,7 +86,7 @@ habitrpg.controller("InventoryCtrl", }) }, true); - var equipmentSearch = function(equipment, term) { + $scope.equipmentSearch = function(equipment, term) { if (!equipment) return; if (!angular.isString(term) || term.length == 0) { return equipment; @@ -101,25 +102,33 @@ habitrpg.controller("InventoryCtrl", return result; }; - $scope.$watchGroup(['gearByClass', 'gearByType', 'user.equipmentQuery'], function(updatedVals) { - var gearByClass = updatedVals[0]; - var gearByType = updatedVals[1]; - var equipmentQuery = updatedVals[2]; + $scope.updateEquipment = function(gearByClass, gearByType, equipmentQuery) { $scope.filteredGearByClass = {}; $scope.filteredGearByType = {}; - angular.forEach(gearByClass, function(value, key) { - var searchResult = equipmentSearch(value, equipmentQuery); + _.forEach(gearByClass, function(value, key) { + var searchResult = $scope.equipmentSearch(value, equipmentQuery); if (searchResult.length > 0) { $scope.filteredGearByClass[key] = searchResult; } }); - angular.forEach(gearByType, function(value, key) { - var searchResult = equipmentSearch(value, equipmentQuery); + _.forEach(gearByType, function(value, key) { + var searchResult = $scope.equipmentSearch(value, equipmentQuery); if (searchResult.length > 0) { $scope.filteredGearByType[key] = searchResult; } }); - }); + } + + $scope.$watch(function(){ + return ['gearByClass', 'gearByType', 'equipmentFilterQuery'].map(angular.bind($scope, $scope.$eval)); + }, function(updatedVals) { + var gearByClass = updatedVals[0]; + var gearByType = updatedVals[1]; + var equipmentFilterQuery = updatedVals[2]; + $scope.updateEquipment(gearByClass, gearByType, equipmentFilterQuery.query); + }, true); + + $scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentFilterQuery.query); $scope.chooseEgg = function(egg){ if ($scope.selectedEgg && $scope.selectedEgg.key == egg) { @@ -456,12 +465,12 @@ habitrpg.controller("InventoryCtrl", } } ]); - habitrpg.controller("InventorySearchCtrl", - ['$scope', 'User', - function($scope, User) { + ['$scope', + function($scope) { + $scope.equipmentFilterQuery.query = ''; $scope.updateEquipmentQuery = function() { - User.user.equipmentQuery = $scope.equipmentQuery; + $scope.equipmentFilterQuery.query = $scope.equipmentQuery; } } ]); From 4e303cc592652831ce18bd48d65eb8f983d5ab79 Mon Sep 17 00:00:00 2001 From: Alexander Lin Date: Sat, 10 Dec 2016 02:14:30 -0800 Subject: [PATCH 3/3] Clean up code --- .../spec/controllers/inventoryCtrlSpec.js | 6 +++--- .../js/controllers/inventoryCtrl.js | 19 +++++-------------- .../views/options/inventory/equipment.jade | 4 ++-- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/test/client-old/spec/controllers/inventoryCtrlSpec.js b/test/client-old/spec/controllers/inventoryCtrlSpec.js index 5639fb6dc5..92f4d2818f 100644 --- a/test/client-old/spec/controllers/inventoryCtrlSpec.js +++ b/test/client-old/spec/controllers/inventoryCtrlSpec.js @@ -471,11 +471,11 @@ describe('Inventory Controller', function() { 'not': [wrap('chicken'), wrap('sashimi')]}; scope.gearByClass = gearByClass; scope.gearByType = gearByType; - scope.equipmentFilterQuery.query = 'a'; + scope.equipmentQuery.query = 'a'; }); it('filters nothing if equipmentQuery is nothing', function() { - scope.equipmentFilterQuery.query = ''; + scope.equipmentQuery.query = ''; scope.$digest(); expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']); @@ -509,7 +509,7 @@ describe('Inventory Controller', function() { }); it('filters out gear if filter query changes', function() { - scope.equipmentFilterQuery.query = 'c'; + scope.equipmentQuery.query = 'c'; scope.$digest(); expect(toText(scope.filteredGearByClass['raw'])).to.eql([]); diff --git a/website/client-old/js/controllers/inventoryCtrl.js b/website/client-old/js/controllers/inventoryCtrl.js index d7175a5832..6644921f77 100644 --- a/website/client-old/js/controllers/inventoryCtrl.js +++ b/website/client-old/js/controllers/inventoryCtrl.js @@ -8,7 +8,7 @@ habitrpg.controller("InventoryCtrl", $scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5} $scope.selectedPotion = null; // {index: 5, name: "Red", value: 3} - $scope.equipmentFilterQuery = {'query': ''}; + $scope.equipmentQuery = {'query': ''}; _updateDropAnimalCount(user.items); @@ -120,15 +120,15 @@ habitrpg.controller("InventoryCtrl", } $scope.$watch(function(){ - return ['gearByClass', 'gearByType', 'equipmentFilterQuery'].map(angular.bind($scope, $scope.$eval)); + return ['gearByClass', 'gearByType', 'equipmentQuery'].map(angular.bind($scope, $scope.$eval)); }, function(updatedVals) { var gearByClass = updatedVals[0]; var gearByType = updatedVals[1]; - var equipmentFilterQuery = updatedVals[2]; - $scope.updateEquipment(gearByClass, gearByType, equipmentFilterQuery.query); + var equipmentQuery = updatedVals[2]; + $scope.updateEquipment(gearByClass, gearByType, equipmentQuery.query); }, true); - $scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentFilterQuery.query); + $scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentQuery.query); $scope.chooseEgg = function(egg){ if ($scope.selectedEgg && $scope.selectedEgg.key == egg) { @@ -465,12 +465,3 @@ habitrpg.controller("InventoryCtrl", } } ]); -habitrpg.controller("InventorySearchCtrl", - ['$scope', - function($scope) { - $scope.equipmentFilterQuery.query = ''; - $scope.updateEquipmentQuery = function() { - $scope.equipmentFilterQuery.query = $scope.equipmentQuery; - } - } -]); diff --git a/website/views/options/inventory/equipment.jade b/website/views/options/inventory/equipment.jade index 1872e4d801..380de2d55c 100644 --- a/website/views/options/inventory/equipment.jade +++ b/website/views/options/inventory/equipment.jade @@ -43,10 +43,10 @@ mixin equipmentList(equipmentType) .container-fluid .row .col-md-6 - .input-group.equipment-search(ng-controller="InventorySearchCtrl") + .input-group.equipment-search .input-group-addon .glyphicon.glyphicon-search - input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery', ng-change='updateEquipmentQuery()', ng-model-options='{ debounce: 250 }') + input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery.query', ng-model-options='{ debounce: 250 }') .row .col-md-6.border-right(ng-controller="SortableInventoryController") h3.equipment-title.hint(popover-trigger='mouseenter',