Adding Magic Search codebase to Horizon

Magic Search is getting integrated into the Horizon codebase as agreed
at the Tokyo summit.  This Magic Search patch replaces the XStatic
package with its own module, and otherwise works as the current XStatic
package (changes will be made to accommodate Searchlight features in
following patches).  This is not simply a copy of what is in GitHub,
because there were no tests and little documentation.

The code underwent significant restructuring to accommodate the linter and
other standards used in Horizon.

Change-Id: I9a2b0f3fed1955680a534e8d284da2c8ee68ef16
Implements: blueprint integrate-magic-search
This commit is contained in:
Matt Borland 2015-11-13 09:01:26 -07:00 committed by Matt Borland
parent 597dbcecc4
commit 85d44f9f56
18 changed files with 1908 additions and 531 deletions

View File

@ -100,9 +100,6 @@ module.exports = function (config) {
*/
'!(horizon)/**/!(*.spec|*.mock).js',
// Magic search requires late ordering due to overriding.
xstaticPath + 'magic_search/data/magic_search.js',
/**
* Then, list files for mocks with `mock.js` extension. The order
* among them should not be significant.

View File

@ -15,7 +15,7 @@
'use strict';
angular
.module('MagicSearch')
.module('horizon.framework.widgets.magic-search')
.directive('hzMagicSearchBar', hzMagicSearchBar);
hzMagicSearchBar.$inject = ['horizon.framework.widgets.basePath'];

View File

@ -23,7 +23,7 @@
beforeEach(module('templates'));
beforeEach(module('smart-table'));
beforeEach(module('horizon.framework'));
beforeEach(module('MagicSearch'));
beforeEach(module('horizon.framework.widgets.magic-search'));
beforeEach(inject(function ($injector) {
$compile = $injector.get('$compile');

View File

@ -1,212 +0,0 @@
(function () {
'use strict';
angular
.module('MagicSearch')
.directive('magicOverrides', magicOverrides);
/**
* @ngdoc directive
* @name MagicSearch:magicOverrides
* @description
* A directive to modify and extend Magic Search functionality for use in
* Horizon.
*
* 1. The base Magic Search widget makes Foundation (a responsive front-end
* framework) specific calls in showMenu and hideMenu. In Horizon we use
* Bootstrap, therefore we need to override those methods.
*
* 2. Added 'facetsChanged' listener so we can notify the base Magic Search
* widget that new facets are available. Use this if your table has dynamic
* facets.
*
* 3. Due to the current inconsistencies in the APIs, where some support
* filtering and others do not, we wanted a way to distinguish client-side
* filtering (searching the visible subset) vs server-side filtering
* (another server filter query).
*
* To support this distinction we overrode the methods 'removeFacet' and
* 'initfacets' to emit a 'checkFacets' event. Implementers can add property
* 'isServer' to facets (what triggers the facet icon and color difference).
*
* Each table that incorporates Magic Search is responsible for adding
* property 'isServer' to their facets as they have the intimate knowledge
* of the API supplying the table data. The setting of this property needs
* to be done in the Magic Search supporting JavaScript for each table.
*
* Example:
* Set property 'isServer' on facets that you want to render as server
* facet (server icon, lighter grey color). Note: If the property
* 'isServer' is not set, then facet renders with client icon and darker
* grey color.
*
* scope.$on('checkFacets', function (event, currentSearch) {
* angular.forEach(currentSearch, function (facet) {
* if (apiVersion < 3) {
* facet.isServer = true;
* }
* });
* });
*
* 4. Overrode 'initfacets' to fix refresh/bookmark issue where facets
* menu wasn't removing facets that were already on URL.
*
* @restrict A
* @scope
*
* @example
* ```
* <div class="magic-search" magic-overrides>
* ```
*/
function magicOverrides() {
var directive = {
restrict: 'A',
controller: MagicOverridesController
};
MagicOverridesController.$inject = [
'$element',
'$scope',
'$timeout',
'$window'
];
return directive;
function MagicOverridesController($element, $scope, $timeout, $window) {
/**
* showMenu and hideMenu depend on Foundation's dropdown. They need
* to be modified to work with another dropdown implementation.
* For Bootstrap, they are not needed at all.
*/
$scope.showMenu = function () {
$timeout(function () {
$scope.isMenuOpen = true;
});
};
$scope.hideMenu = function () {
$timeout(function () {
$scope.isMenuOpen = false;
});
};
$scope.isMenuOpen = false;
/**
* Add ability to update facet
* Broadcast event when facet options are returned via AJAX.
* Should magic_search.js absorb this?
*/
var facetsChangedWatcher = $scope.$on('facetsChanged', function () {
$timeout(function () {
$scope.currentSearch = [];
$scope.initSearch();
});
});
$scope.$on('$destroy', function () {
facetsChangedWatcher();
});
function getFacets(currentFacets) {
if (angular.isUndefined(currentFacets)) {
var initialFacets = $window.location.search;
if (initialFacets.indexOf('?') === 0) {
initialFacets = initialFacets.slice(1);
}
return initialFacets.split('&');
} else {
return currentFacets.map(function(facet) {
return facet.name;
});
}
}
/**
* Override magic_search.js 'initFacets' to fix browser refresh issue
* and to emit('checkFacets') to flag facets as 'isServer'
*/
$scope.initFacets = function(currentFacets) {
var facets = getFacets(currentFacets);
if (facets.length > 1 || (facets[0] && facets[0].length > 0)) {
$timeout(function () {
$scope.strings.prompt = '';
});
}
angular.forEach(facets, function(facet) {
var facetParts = facet.split('=');
angular.forEach($scope.facetsObj, function (value) {
if (value.name === facetParts[0]) {
if (angular.isUndefined(value.options)) {
$scope.currentSearch.push({
'name': facet,
'label': [value.label, facetParts[1]]
});
/**
* for refresh case, need to remove facets that were
* bookmarked/current when browser refresh was clicked
*/
$scope.deleteFacetEntirely(facetParts);
} else {
angular.forEach(value.options, function (option) {
if (option.key === facetParts[1]) {
$scope.currentSearch.push({
'name': facet,
'label': [value.label, option.label]
});
if (value.singleton === true) {
$scope.deleteFacetEntirely(facetParts);
} else {
$scope.deleteFacetSelection(facetParts);
}
}
});
}
}
});
});
if (angular.isDefined($scope.textSearch)) {
$scope.currentSearch.push({
'name': 'text=' + $scope.textSearch,
'label': [$scope.strings.text, $scope.textSearch]
});
}
$scope.filteredObj = $scope.facetsObj;
// broadcast to check facets for server-side
$scope.$emit('checkFacets', $scope.currentSearch);
};
/**
* Override magic_search.js 'removeFacet' to emit('checkFacets')
* to flag facets as 'isServer' after removing facet and
* either update filter or search
*/
$scope.removeFacet = function ($index) {
var removed = $scope.currentSearch[$index].name;
$scope.currentSearch.splice($index, 1);
if (angular.isUndefined($scope.facetSelected)) {
$scope.emitQuery(removed);
} else {
$scope.resetState();
$element.find('.search-input').val('');
}
if ($scope.currentSearch.length === 0) {
$scope.strings.prompt = $scope.promptString;
}
// re-init to restore facets cleanly
$scope.facetsObj = $scope.copyFacets($scope.facetsSave);
var currentSearch = angular.copy($scope.currentSearch);
$scope.currentSearch = [];
$scope.initFacets(currentSearch);
};
$scope.emitQuery();
}
}
})();

View File

@ -1,286 +0,0 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
"use strict";
// NOTE: The javascript file being tested here isn't the magic-search code
// as a whole, but instead the magic-overrides code.
describe('MagicSearch module', function () {
it('should be defined', function () {
expect(angular.module('MagicSearch')).toBeDefined();
});
});
describe('magic-overrides directive', function () {
var $window, $scope, $magicScope, $timeout;
beforeEach(module('templates'));
beforeEach(module('MagicSearch'));
beforeEach(module(function ($provide) {
$provide.value('$window', {
location: {
search: ''
}
});
}));
beforeEach(inject(function ($injector) {
$window = $injector.get('$window');
var $compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$timeout = $injector.get('$timeout');
$scope.filterStrings = {
cancel: gettext('Cancel'),
prompt: gettext('Prompt'),
remove: gettext('Remove'),
text: gettext('Text')
};
$scope.filterFacets = [
{
name: 'name',
label: gettext('Name'),
singleton: true
},
{
name: 'status',
label: gettext('Status'),
options: [
{ key: 'active', label: gettext('Active') },
{ key: 'shutdown', label: gettext('Shutdown') },
{ key: 'error', label: gettext('Error') }
]
},
{
name: 'flavor',
label: gettext('Flavor'),
singleton: true,
options: [
{ key: 'm1.tiny', label: gettext('m1.tiny') },
{ key: 'm1.small', label: gettext('m1.small') }
]
}
];
/* eslint-disable angular/window-service */
var markup =
'<magic-search ' +
'template="' + window.STATIC_URL + 'framework/widgets/magic-search/magic-search.html" ' +
'strings="filterStrings" ' +
'facets="{{ filterFacets }}">' +
'</magic-search>';
/* eslint-enable angular/window-service */
$compile(angular.element(markup))($scope);
$scope.$apply();
$magicScope = $scope.$$childTail; //eslint-disable-line angular/no-private-call
spyOn($magicScope, '$emit');
spyOn($magicScope, 'emitQuery');
spyOn($magicScope, 'deleteFacetEntirely').and.callThrough();
spyOn($magicScope, 'deleteFacetSelection').and.callThrough();
spyOn($magicScope, 'initSearch');
spyOn($magicScope, 'resetState');
}));
it('isMenuOpen should be initially false', function () {
expect($magicScope.isMenuOpen).toBe(false);
});
it('isMenuOpen should be true after showMenu called', function () {
$magicScope.showMenu();
$timeout.flush();
expect($magicScope.isMenuOpen).toBe(true);
});
it('isMenuOpen should be false after hideMenu called', function () {
$magicScope.showMenu();
$timeout.flush();
$magicScope.hideMenu();
$timeout.flush();
expect($magicScope.isMenuOpen).toBe(false);
});
it('initSearch should be called when facetsChanged broadcasted', function () {
$scope.$broadcast('facetsChanged');
$timeout.flush();
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.initSearch).toHaveBeenCalled();
});
it('currentSearch should be empty when URL has no search terms', function () {
expect($magicScope.currentSearch).toEqual([]);
});
describe('initFacets', function () {
it('currentSearch should have one item when URL has one search term', function () {
$window.location.search = '?name=myname';
$magicScope.initFacets();
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(1);
expect($magicScope.currentSearch[0].label).toEqual([ 'Name', 'myname' ]);
expect($magicScope.currentSearch[0].name).toBe('name=myname');
expect($magicScope.strings.prompt).toBe('');
// 'name' facet should be deleted (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'name', 'myname' ]);
});
it('currentSearch should have one item when given one search term', function () {
var currentFacets = [{name: 'name=myname'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(1);
expect($magicScope.currentSearch[0].label).toEqual([ 'Name', 'myname' ]);
expect($magicScope.currentSearch[0].name).toBe('name=myname');
// 'name' facet should be deleted (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'name', 'myname' ]);
});
it('currentSearch should have two items when given two search terms', function () {
var currentFacets = [{name: 'name=myname'}, {name: 'status=active'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
// only 'active' option should be removed from 'status' facet (not singleton)
expect($magicScope.currentSearch.length).toBe(2);
expect($magicScope.deleteFacetSelection).toHaveBeenCalledWith([ 'status', 'active' ]);
});
it('flavor facet should be removed if search term includes flavor', function () {
var currentFacets = [{name: 'flavor=m1.tiny'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
// entire 'flavor' facet should be removed even if some options left (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'flavor', 'm1.tiny' ]);
});
it('currentSearch should have one item when search is textSearch', function () {
$magicScope.textSearch = 'test';
$magicScope.initFacets([]);
$timeout.flush();
expect($magicScope.currentSearch[0].label).toEqual([ 'Text', 'test' ]);
expect($magicScope.currentSearch[0].name).toBe('text=test');
});
it('currentSearch should have textSearch and currentSearch', function () {
$magicScope.textSearch = 'test';
$magicScope.initFacets([{name: 'flavor=m1.tiny'}]);
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(2);
expect($magicScope.currentSearch[0].label).toEqual([ 'Flavor', 'm1.tiny' ]);
expect($magicScope.currentSearch[0].name).toBe('flavor=m1.tiny');
expect($magicScope.currentSearch[1].label).toEqual([ 'Text', 'test' ]);
expect($magicScope.currentSearch[1].name).toBe('text=test');
});
it('should call checkFacets when initFacets called', function () {
$magicScope.initFacets([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
});
});
describe('removeFacet', function () {
beforeEach(function () {
spyOn($magicScope, 'initFacets').and.callThrough();
});
it('should call emitQuery, initFacets and emit checkFacets on removeFacet', function () {
var initialSearch = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
$magicScope.currentSearch.push(initialSearch);
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.emitQuery).toHaveBeenCalledWith('name=myname');
expect($magicScope.initFacets).toHaveBeenCalledWith([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
expect($magicScope.strings.prompt).toBe('Prompt');
});
it('prompt text === "" if search terms left after removal of one', function () {
$magicScope.strings.prompt = '';
$magicScope.currentSearch.push({ name: 'name=myname', label: [ 'Name', 'myname' ] });
$magicScope.currentSearch.push({ name: 'status=active', label: [ 'Status', 'Active' ] });
$magicScope.removeFacet(0);
expect($magicScope.strings.prompt).toBe('');
});
it('should emit checkFacets on removeFacet if facetSelected', function () {
var initialSearch = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
$magicScope.currentSearch.push(initialSearch);
$magicScope.facetSelected = {
'name': 'status',
'label': [ 'Status', 'active' ]
};
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.resetState).toHaveBeenCalled();
expect($magicScope.initFacets).toHaveBeenCalledWith([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
});
it('should emit checkFacets and remember state on removeFacet if facetSelected', function () {
var search1 = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
var search2 = {
name: 'flavor=m1.tiny',
label: [ 'Flavor', 'm1.tiny' ]
};
$magicScope.currentSearch.push(search1);
$magicScope.currentSearch.push(search2);
$magicScope.facetSelected = {
'name': 'status',
'label': [ 'Status', 'active' ]
};
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([search2]);
expect($magicScope.resetState).toHaveBeenCalled();
expect($magicScope.initFacets).toHaveBeenCalledWith([search2]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', [search2]);
});
});
});
})();

View File

@ -0,0 +1,383 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
/**
* @fileOverview Magic Search JS
* @requires AngularJS
*
*/
angular.module('horizon.framework.widgets.magic-search')
.controller('MagicSearchController', magicSearchController);
magicSearchController.$inject = ['$scope', '$element', '$timeout', '$window',
'horizon.framework.widgets.magic-search.service'];
function magicSearchController($scope, $element, $timeout, $window, service) {
var ctrl = this;
var searchInput = $element.find('.search-input');
ctrl.mainPromptString = $scope.strings.prompt;
// currentSearch is the list of facets representing the current search
ctrl.currentSearch = [];
ctrl.isMenuOpen = false;
searchInput.on('keydown', keyDownHandler);
searchInput.on('keyup', keyUpHandler);
searchInput.on('keypress', keyPressHandler);
// enable text entry when mouse clicked anywhere in search box
$element.find('.search-main-area').on('click', searchMainClickHandler);
// when facet clicked, add 1st part of facet and set up options
ctrl.facetClicked = facetClickHandler;
// when option clicked, complete facet and send event
ctrl.optionClicked = optionClickHandler;
// remove facet and either update filter or search
ctrl.removeFacet = removeFacet;
// Controller-exposed Functions
// clear entire searchbar
ctrl.clearSearch = clearSearch;
// ctrl.textSearch is undefined, only used when a user free-enters text
// Used by the template.
ctrl.isMatchLabel = function(label) {
return angular.isArray(label);
};
// unusedFacetChoices is the list of facet types that have not been selected
ctrl.unusedFacetChoices = [];
// facetChoices is the list of all facet choices
ctrl.facetChoices = [];
initSearch(service.getSearchTermsFromQueryString($window.location.search));
emitQuery();
function initSearch(initialSearchTerms) {
// Initializes both the unused choices and the full list of facets
ctrl.facetChoices = service.getFacetChoicesFromFacetsParam($scope.facets_param);
// resets the facets
initFacets(initialSearchTerms);
}
function keyDownHandler($event) {
var key = service.getEventCode($event);
if (key === 9) { // prevent default when we can.
$event.preventDefault();
}
}
function tabKeyUp() {
if (angular.isUndefined(ctrl.facetSelected)) {
if (ctrl.filteredObj.length !== 1) {
return;
}
ctrl.facetClicked(0, '', ctrl.filteredObj[0].name);
setSearchInput('');
} else {
if (angular.isUndefined(ctrl.filteredOptions) ||
ctrl.filteredOptions.length !== 1) {
return;
}
ctrl.optionClicked(0, '', ctrl.filteredOptions[0].key);
resetState();
}
}
function escapeKeyUp() {
setMenuOpen(false);
resetState();
var textFilter = ctrl.textSearch;
if (angular.isUndefined(textFilter)) {
textFilter = '';
}
emitTextSearch(textFilter);
}
function enterKeyUp() {
var searchVal = searchInput.val();
// if tag search, treat as regular facet
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) {
var curr = ctrl.facetSelected;
curr.name = curr.name.split('=')[0] + '=' + searchVal;
curr.label[1] = searchVal;
ctrl.currentSearch.push(curr);
resetState();
emitQuery();
setMenuOpen(true);
} else {
// if text search treat as search
ctrl.currentSearch = ctrl.currentSearch.filter(notTextSearch);
ctrl.currentSearch.push(service.getTextFacet(searchVal, $scope.strings.text));
$scope.$apply();
setMenuOpen(false);
setSearchInput('');
emitTextSearch(searchVal);
ctrl.textSearch = searchVal;
}
ctrl.filteredObj = ctrl.unusedFacetChoices;
}
function notTextSearch(item) {
return item.name.indexOf('text') !== 0;
}
function defaultKeyUp() {
var searchVal = searchInput.val();
if (searchVal === '') {
ctrl.filteredObj = ctrl.unusedFacetChoices;
$scope.$apply();
emitTextSearch('');
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) {
resetState();
}
} else {
filterFacets(searchVal);
}
}
function keyUpHandler($event) { // handle ctrl-char input
if ($event.metaKey === true) {
return;
}
var key = service.getEventCode($event);
var handlers = { 9: tabKeyUp, 27: escapeKeyUp, 13: enterKeyUp };
if (handlers[key]) {
handlers[key]();
} else {
defaultKeyUp();
}
}
function keyPressHandler($event) { // handle character input
var searchVal = searchInput.val();
var key = service.getEventCode($event);
// Backspace, Delete, Enter, Tab, Escape
if (key !== 8 && key !== 46 && key !== 13 && key !== 9 && key !== 27) {
// This builds the search term as you go.
searchVal = searchVal + String.fromCharCode(key).toLowerCase();
}
if (searchVal === ' ') { // space and field is empty, show menu
setMenuOpen(true);
setSearchInput('');
return;
}
if (searchVal === '') {
ctrl.filteredObj = ctrl.unusedFacetChoices;
$scope.$apply();
emitTextSearch('');
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) {
resetState();
}
return;
}
// Backspace, Delete
if (key !== 8 && key !== 46) {
filterFacets(searchVal);
}
}
function filterFacets(searchVal) {
// try filtering facets/options.. if no facets match, do text search
var filtered = [];
var isTextSearch = angular.isUndefined(ctrl.facetSelected);
if (isTextSearch) {
ctrl.filteredObj = ctrl.unusedFacetChoices;
filtered = service.getMatchingFacets(ctrl.filteredObj, searchVal);
} else { // assume option search
ctrl.filteredOptions = ctrl.facetOptions;
if (angular.isUndefined(ctrl.facetOptions)) {
// no options, assume free form text facet
return;
}
filtered = service.getMatchingOptions(ctrl.filteredOptions, searchVal);
}
if (filtered.length > 0) {
setMenuOpen(true);
$timeout(function() {
ctrl.filteredObj = filtered;
}, 0.1);
} else if (isTextSearch) {
emitTextSearch(searchVal);
setMenuOpen(false);
}
}
function searchMainClickHandler($event) {
var target = angular.element($event.target);
if (target.is('.search-main-area')) {
searchInput.trigger('focus');
setMenuOpen(true);
}
}
function facetClickHandler($index) {
setMenuOpen(false);
var facet = ctrl.filteredObj[$index];
var label = facet.label;
if (angular.isArray(label)) {
label = label.join('');
}
var facetParts = facet.name && facet.name.split('=');
ctrl.facetSelected = service.getFacet(facetParts[0], facetParts[1], label, '');
if (angular.isDefined(facet.options)) {
ctrl.filteredOptions = ctrl.facetOptions = facet.options;
setMenuOpen(true);
}
setSearchInput('');
setPrompt('');
$timeout(function() {
searchInput.focus();
});
}
function optionClickHandler($index, $event, name) {
setMenuOpen(false);
var curr = ctrl.facetSelected;
curr.name = curr.name.split('=')[0] + '=' + name;
curr.label[1] = ctrl.filteredOptions[$index].label;
if (angular.isArray(curr.label[1])) {
curr.label[1] = curr.label[1].join('');
}
ctrl.currentSearch.push(curr);
resetState();
emitQuery();
setMenuOpen(true);
}
function emitTextSearch(val) {
$scope.$emit('textSearch', val, $scope.filter_keys);
}
function emitQuery(removed) {
var query = service.getQueryPattern(ctrl.currentSearch);
if (angular.isDefined(removed) && removed.indexOf('text') === 0) {
emitTextSearch('');
delete ctrl.textSearch;
} else {
$scope.$emit('searchUpdated', query);
if (ctrl.currentSearch.length > 0) {
// prune facets as needed from menus
var newFacet = ctrl.currentSearch[ctrl.currentSearch.length - 1].name;
var facetParts = service.getSearchTermObject(newFacet);
service.removeChoice(facetParts, ctrl.facetChoices, ctrl.unusedFacetChoices);
}
}
}
function clearSearch() {
if (ctrl.currentSearch.length > 0) {
ctrl.currentSearch = [];
ctrl.unusedFacetChoices = ctrl.facetChoices.map(service.getFacetChoice);
resetState();
$scope.$emit('searchUpdated', '');
emitTextSearch('');
}
}
function resetState() {
setSearchInput('');
ctrl.filteredObj = ctrl.unusedFacetChoices;
delete ctrl.facetSelected;
delete ctrl.facetOptions;
delete ctrl.filteredOptions;
if (ctrl.currentSearch.length === 0) {
setPrompt(ctrl.mainPromptString);
}
}
function setMenuOpen(bool) {
$timeout(function setMenuOpenTimeout() {
ctrl.isMenuOpen = bool;
});
}
function setSearchInput(val) {
$timeout(function setSearchInputTimeout() {
searchInput.val(val);
});
}
function setPrompt(str) {
$timeout(function setPromptTimeout() {
$scope.strings.prompt = str;
});
}
/**
* Add ability to update facet
* Broadcast event when facet options are returned via AJAX.
* Should magic_search.js absorb this?
*/
var facetsChangedWatcher = $scope.$on('facetsChanged', function () {
$timeout(function () {
initSearch([]);
});
});
$scope.$on('$destroy', function () {
facetsChangedWatcher();
});
function initFacets(searchTerms) {
var tmpFacetChoices = ctrl.facetChoices.map(service.getFacetChoice);
if (searchTerms.length > 1 || searchTerms[0] && searchTerms[0].length > 0) {
setPrompt('');
}
ctrl.currentSearch = service.getFacetsFromSearchTerms(searchTerms,
ctrl.textSearch, $scope.strings.text, tmpFacetChoices);
ctrl.filteredObj = ctrl.unusedFacetChoices =
service.getUnusedFacetChoices(tmpFacetChoices, searchTerms);
// broadcast to check facets for server-side
$scope.$emit('checkFacets', ctrl.currentSearch);
}
/**
* Override magic_search.js 'removeFacet' to emit('checkFacets')
* to flag facets as 'isServer' after removing facet and
* either update filter or search
* @param {number} index - the index of the facet to remove. Required.
*
* @returns {number} Doesn't return anything
*/
function removeFacet(index) {
var removed = ctrl.currentSearch[index].name;
ctrl.currentSearch.splice(index, 1);
if (angular.isUndefined(ctrl.facetSelected)) {
emitQuery(removed);
} else {
resetState();
}
if (ctrl.currentSearch.length === 0) {
setPrompt(ctrl.mainPromptString);
}
// re-init to restore facets cleanly
initFacets(ctrl.currentSearch.map(service.getName));
}
}
})();

View File

@ -0,0 +1,803 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
"use strict";
describe('MagiSearchController', function () {
var ctrl, scope, searchInput, $timeout, service;
function expectResetState() {
expect(ctrl.facetSelected).toBeUndefined();
expect(ctrl.facetOptions).toBeUndefined();
expect(ctrl.filteredOptions).toBeUndefined();
expect(ctrl.filteredObj).toBe(ctrl.unusedFacetChoices);
}
// Given an array of handlername:function items, return the
// function for the given name.
function getHandler(args, name) {
return args.reduce(function (last, curr) {
last[curr[0]] = curr[1];
return last;
}, {})[name];
}
beforeEach(module('horizon.framework.widgets.magic-search'));
beforeEach(inject(function($controller, _$timeout_, $window, $rootScope, $injector) {
$timeout = _$timeout_;
scope = $rootScope.$new();
scope.strings = { prompt: "Hello World!" };
scope.facets_param = [];
service = $injector.get('horizon.framework.widgets.magic-search.service');
searchInput = {
on: angular.noop, val: function() {
return '';
}, focus: angular.noop
};
spyOn(searchInput, 'on');
var $element = { find: function() {
return searchInput;
}};
ctrl = $controller('MagicSearchController', {
$scope: scope, $element: $element, $timeout: $timeout,
$window: $window
});
}));
it("defines the controller", function() {
expect(ctrl).toBeDefined();
});
describe("filterFacets", function() {
var execFilter;
beforeEach(function() {
var keyUpHandler = getHandler(searchInput.on.calls.allArgs(), 'keyup');
var evt = {
keyCode: -1, charCode: 10, preventDefault: angular.noop
};
execFilter = function() {
keyUpHandler(evt);
};
spyOn(searchInput, 'val').and.returnValue('hello');
ctrl.facetSelected = {};
ctrl.filteredObj = [];
});
it("sets the filteredObj if results to a text search", function() {
delete ctrl.facetSelected;
spyOn(service, 'getMatchingFacets').and.returnValue(['my', 'list']);
execFilter();
$timeout.flush();
expect(ctrl.filteredObj).toEqual(['my', 'list']);
});
it("closes the menu if no results to a text search", function() {
delete ctrl.facetSelected;
ctrl.isMenuOpen = true;
spyOn(service, 'getMatchingFacets').and.returnValue([]);
execFilter();
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(false);
});
it("sets filteredObj with results of an option search", function() {
ctrl.facetOptions = [];
ctrl.isMenuOpen = true;
spyOn(service, 'getMatchingOptions').and.returnValue(['my', 'list']);
execFilter();
$timeout.flush();
expect(ctrl.filteredObj).toEqual(['my', 'list']);
});
it("doesn't set filteredObj with no results of an option search", function() {
ctrl.facetOptions = [];
ctrl.isMenuOpen = true;
spyOn(service, 'getMatchingOptions').and.returnValue([]);
execFilter();
expect(ctrl.filteredObj).toEqual([]);
});
it("doesn't set filteredObj with no facet options", function() {
execFilter();
expect(ctrl.filteredObj).toEqual([]);
});
});
describe("clearSearch", function() {
it("does nothing when currentSearch is empty", function() {
spyOn(scope, '$emit');
ctrl.currentSearch = [];
ctrl.clearSearch();
expect(scope.$emit).not.toHaveBeenCalled();
});
it("clears the currentSearch when currentSearch is not empty", function() {
spyOn(scope, '$emit');
ctrl.currentSearch = ['a', 'b', 'c'];
scope.filter_keys = [1,2,3];
ctrl.clearSearch();
expect(scope.$emit).toHaveBeenCalledWith('searchUpdated', '');
expect(scope.$emit).toHaveBeenCalledWith('textSearch', '', [1,2,3]);
});
});
describe("keydown handler", function() {
var keyDownHandler;
var evt = {keyCode: 10, charCode: 10, preventDefault: angular.noop};
beforeEach(function() {
keyDownHandler = getHandler(searchInput.on.calls.allArgs(), 'keydown');
});
it("is defined", function() {
expect(keyDownHandler).toBeDefined();
});
it("does nothing with keys other than 9", function() {
spyOn(evt, 'preventDefault');
keyDownHandler(evt);
expect(evt.preventDefault).not.toHaveBeenCalled();
});
it("does call preventDefault with keycode of 9", function() {
evt.keyCode = 9;
spyOn(evt, 'preventDefault');
keyDownHandler(evt);
expect(evt.preventDefault).toHaveBeenCalled();
});
});
describe("keyup handler", function() {
var keyUpHandler, evt;
beforeEach(function() {
keyUpHandler = getHandler(searchInput.on.calls.allArgs(), 'keyup');
evt = {keyCode: 10, charCode: 10, preventDefault: angular.noop};
});
it("is defined", function() {
expect(keyUpHandler).toBeDefined();
});
it("doesn't emit anything if sent a metakey", function() {
evt.metaKey = true;
spyOn(scope, '$emit');
keyUpHandler(evt);
expect(scope.$emit).not.toHaveBeenCalled();
});
describe("'Escape' key", function() {
beforeEach(function() {
evt.keyCode = 27;
});
it("closes the menu", function() {
ctrl.isMenuOpen = true;
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(false);
});
it("closes the menu (using charCode)", function() {
ctrl.isMenuOpen = true;
delete evt.keyCode;
evt.charCode = 27;
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(false);
});
it("emits a textSearch event", function() {
ctrl.textSearch = 'waldo';
scope.filter_keys = 'abc';
spyOn(scope, '$emit');
keyUpHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', 'waldo', 'abc');
});
it("emits a textSearch event even if ctrl.textSearch undefined", function() {
delete ctrl.textSearch;
scope.filter_keys = 'abc';
spyOn(scope, '$emit');
keyUpHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', '', 'abc');
});
});
describe("'Tab' key", function() {
beforeEach(function() {
evt.keyCode = 9;
ctrl.facetSelected = {};
});
it("calls facetClicked when no facet selected and exactly one facet", function() {
spyOn(ctrl, 'facetClicked');
delete ctrl.facetSelected;
ctrl.filteredObj = [{name: 'waldo'}];
keyUpHandler(evt);
expect(ctrl.facetClicked).toHaveBeenCalledWith(0, '', 'waldo');
});
it("doesn't call facetClicked when no facet selected and not one facet", function() {
spyOn(ctrl, 'facetClicked');
delete ctrl.facetSelected;
ctrl.filteredObj = [{name: 'waldo'}, {name: 'warren'}];
keyUpHandler(evt);
expect(ctrl.facetClicked).not.toHaveBeenCalled();
});
it("calls optionClicked when a facet selected and one option", function() {
spyOn(ctrl, 'optionClicked');
ctrl.filteredOptions = [{key: 'thekey'}];
keyUpHandler(evt);
expect(ctrl.optionClicked).toHaveBeenCalledWith(0, '', 'thekey');
expectResetState();
});
it("doesn't call optionClicked when a facet selected and not one option", function() {
spyOn(ctrl, 'optionClicked');
ctrl.filteredOptions = [{key: 'thekey'}, {key: 'another'}];
keyUpHandler(evt);
expect(ctrl.optionClicked).not.toHaveBeenCalled();
});
it("sets searchInput to an empty string", function() {
spyOn(searchInput, 'val');
delete ctrl.facetSelected;
ctrl.filteredObj = [{name: 'waldo'}];
keyUpHandler(evt);
$timeout.flush();
expect(searchInput.val).toHaveBeenCalledWith('');
});
});
describe("'Enter' key", function() {
beforeEach(function() {
evt.keyCode = 13;
ctrl.facetSelected = {name: 'waldo=undefined', label: ['a']};
});
it("sets menu open if facet is selected", function() {
ctrl.isMenuOpen = false;
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(true);
});
it("sets menu closed if facet is not selected", function() {
ctrl.isMenuOpen = true;
delete ctrl.facetSelected;
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(false);
});
it("removes currentSearch item names starting with 'text'", function() {
delete ctrl.facetSelected;
spyOn(searchInput, 'val').and.returnValue('searchval');
scope.strings.text = 'stringtext';
ctrl.currentSearch = [{name: 'textstuff'}, {name: 'texting'},
{name: 'nontext'}, {name: 'nottext'}];
keyUpHandler(evt);
expect(ctrl.currentSearch).toEqual([{name: 'nontext'}, {name: 'nottext'},
{name: 'text=searchval', label: ['stringtext', 'searchval']}]);
});
});
describe("Any other key", function() {
beforeEach(function() {
evt.keyCode = -1;
});
it("performs a text search if the search is an empty string", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
scope.filter_keys = ['a', 'b', 'c'];
keyUpHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', '', ['a', 'b', 'c']);
});
it("resets state if facetSelected and no options", function() {
spyOn(searchInput, 'val').and.returnValue('');
scope.filter_keys = ['a', 'b', 'c'];
ctrl.facetSelected = {};
keyUpHandler(evt);
expectResetState();
});
it("filters if there is a search term", function() {
spyOn(searchInput, 'val').and.returnValue('searchterm');
spyOn(scope, '$emit');
scope.filter_keys = [1,2,3];
keyUpHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', 'searchterm', [1,2,3]);
});
});
});
describe("keypress handler", function() {
var keyPressHandler, evt;
beforeEach(function() {
keyPressHandler = getHandler(searchInput.on.calls.allArgs(), 'keypress');
evt = {which: 65, keyCode: 10, charCode: 10, preventDefault: angular.noop};
});
it("is defined", function() {
expect(keyPressHandler).toBeDefined();
});
it("searches for searchterm and 'e' if 'e' is typed", function() {
evt.which = 69;
spyOn(searchInput, 'val').and.returnValue('man');
spyOn(scope, '$emit');
scope.filter_keys = [1,2,3];
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', 'mane', [1,2,3]);
});
it("opens menu when searchVal is a space", function() {
spyOn(searchInput, 'val').and.returnValue(' ');
evt.which = 13; // not alter search
ctrl.isMenuOpen = false;
keyPressHandler(evt);
$timeout.flush();
ctrl.isMenuOpen = true;
});
it("opens menu when searchVal is an empty string", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
evt.which = 13; // not alter search
scope.filter_keys = [1,2,3];
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', '', [1,2,3]);
});
it("resets state when ctrl.facetSelected exists but has no options", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
evt.which = 13; // not alter search
scope.filter_keys = [1,2,3];
ctrl.facetSelected = {};
ctrl.facetOptions = {};
ctrl.filteredOptions = {};
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', '', [1,2,3]);
expectResetState();
});
it("filters when searchval has content and key is not delete/backspace", function() {
spyOn(searchInput, 'val').and.returnValue('searchterm');
spyOn(scope, '$emit');
evt.which = 13; // not alter search
scope.filter_keys = [1,2,3];
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith('textSearch', 'searchterm', [1,2,3]);
});
it("does not filter when key is backspace/delete", function() {
spyOn(searchInput, 'val').and.returnValue('searchterm');
spyOn(scope, '$emit');
evt.which = 8; // not alter search
keyPressHandler(evt);
expect(scope.$emit).not.toHaveBeenCalled();
});
});
describe("optionClicked", function() {
it("opens the menu", function() {
ctrl.isMenuOpen = false;
ctrl.facetSelected = {name: 'waldo', label: []};
ctrl.filteredOptions = [{label: 'meow'}];
ctrl.optionClicked(0);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(true);
});
it("resets state", function() {
ctrl.facetSelected = {name: 'waldo', label: []};
ctrl.filteredOptions = [{label: 'meow'}];
ctrl.optionClicked(0);
$timeout.flush();
expectResetState();
});
it("adds to the current search", function() {
ctrl.facetSelected = {name: 'waldo', label: ['a']};
ctrl.filteredOptions = [{label: 'meow'}];
ctrl.currentSearch = [];
var nothing;
ctrl.optionClicked(0, nothing, 'missing');
expect(ctrl.currentSearch).toEqual([{name: 'waldo=missing', label: ['a', 'meow']}]);
});
it("adds to the current search, concatting labels", function() {
ctrl.facetSelected = {name: 'waldo=undefined', label: ['a']};
ctrl.filteredOptions = [{label: ['me', 'ow']}];
ctrl.currentSearch = [];
var nothing;
ctrl.optionClicked(0, nothing, 'missing');
expect(ctrl.currentSearch).toEqual([{name: 'waldo=missing', label: ['a', 'meow']}]);
});
});
describe("removeFacet", function() {
it("clears the currentSearch", function() {
ctrl.currentSearch = [{}];
ctrl.removeFacet(0);
expect(ctrl.currentSearch).toEqual([]);
});
it("resets the main prompt if no more facets", function() {
ctrl.currentSearch = [{}];
scope.strings.prompt = 'aha';
ctrl.mainPromptString = 'bon jovi';
ctrl.removeFacet(0);
$timeout.flush();
expect(scope.strings.prompt).toEqual('bon jovi');
});
it("resets main prompt to blank if facets remain", function() {
ctrl.currentSearch = [{}, {name: 'waldo'}];
scope.strings.prompt = 'aha';
ctrl.mainPromptString = 'bon jovi';
ctrl.removeFacet(0);
$timeout.flush();
expect(scope.strings.prompt).toEqual('');
});
it("resets state if facet selected", function() {
ctrl.currentSearch = [{}];
ctrl.facetSelected = {};
ctrl.removeFacet(0);
expectResetState();
});
});
describe("facetClicked", function() {
it("closes the menu", function() {
ctrl.isMenuOpen = true;
ctrl.filteredObj = [{name: 'a=b', label: 'ok'}];
ctrl.facetClicked(0);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(false);
});
it("sets the prompt to an empty string", function() {
scope.strings.prompt = 'aha';
ctrl.filteredObj = [{name: 'a=b', label: 'ok'}];
ctrl.facetClicked(0);
$timeout.flush();
expect(scope.strings.prompt).toBe('');
});
it("sets focus on the search input", function() {
ctrl.filteredObj = [{name: 'a=b', label: 'ok'}];
spyOn(searchInput, 'focus');
ctrl.facetClicked(0);
$timeout.flush();
expect(searchInput.focus).toHaveBeenCalled();
});
it("sets facetSelected properly", function() {
ctrl.filteredObj = [{name: 'name=waldo', label: 'ok'}];
ctrl.facetClicked(0);
$timeout.flush();
expect(ctrl.facetSelected).toEqual({name: 'name=waldo', label: ['ok', '']});
});
it("sets facetSelected properly if the label is an array", function() {
ctrl.filteredObj = [{name: 'name=waldo', label: ['o', 'k']}];
ctrl.facetClicked(0);
$timeout.flush();
expect(ctrl.facetSelected).toEqual({name: 'name=waldo', label: ['ok', '']});
});
it("sets options if present in the filteredObj", function() {
ctrl.filteredObj = [{name: 'name=waldo', label: 'ok', options: [1,2,3]}];
ctrl.facetClicked(0);
$timeout.flush();
expect(ctrl.filteredOptions).toEqual([1,2,3]);
expect(ctrl.facetOptions).toEqual([1,2,3]);
});
it("opens the menu if options present in the filteredObj", function() {
ctrl.isMenuOpen = false;
ctrl.filteredObj = [{name: 'name=waldo', label: 'ok', options: [1,2,3]}];
ctrl.facetClicked(0);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(true);
});
});
});
// NOTE: The javascript file being tested here isn't the magic-search code
// as a whole, but instead the magic-overrides code.
describe('MagicSearch module', function () {
it('should be defined', function () {
expect(angular.module('horizon.framework.widgets.magic-search')).toBeDefined();
});
});
/*
xdescribe('magic-overrides directive', function () {
var $window, $scope, $magicScope, $timeout;
beforeEach(module('templates'));
beforeEach(module('horizon.framework.widgets.magic-search'));
beforeEach(module(function ($provide) {
$provide.value('$window', {
location: {
search: ''
}
});
}));
beforeEach(inject(function ($injector) {
$window = $injector.get('$window');
var $compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$timeout = $injector.get('$timeout');
$scope.filterStrings = {
cancel: gettext('Cancel'),
prompt: gettext('Prompt'),
remove: gettext('Remove'),
text: gettext('Text')
};
$scope.filterFacets = [
{
name: 'name',
label: gettext('Name'),
singleton: true
},
{
name: 'status',
label: gettext('Status'),
options: [
{ key: 'active', label: gettext('Active') },
{ key: 'shutdown', label: gettext('Shutdown') },
{ key: 'error', label: gettext('Error') }
]
},
{
name: 'flavor',
label: gettext('Flavor'),
singleton: true,
options: [
{ key: 'm1.tiny', label: gettext('m1.tiny') },
{ key: 'm1.small', label: gettext('m1.small') }
]
}
];
// eslint-disable angular/ng_window_service //
var markup =
'<magic-search ' +
'template="' + window.STATIC_URL + 'framework/widgets/magic-search/magic-search.html" ' +
'strings="filterStrings" ' +
'facets="{{ filterFacets }}">' +
'</magic-search>';
// eslint-enable angular/ng_window_service //
$scope.$apply();
$magicScope = $scope.$$childTail; //eslint-disable-line angular/ng_no_private_call
spyOn($magicScope, '$emit');
spyOn($magicScope, 'emitQuery');
spyOn($magicScope, 'deleteFacetEntirely').and.callThrough();
spyOn($magicScope, 'deleteFacetSelection').and.callThrough();
spyOn($magicScope, 'initSearch');
spyOn($magicScope, 'resetState');
}));
it('isMenuOpen should be initially false', function () {
expect($magicScope.isMenuOpen).toBe(false);
});
it('isMenuOpen should be true after showMenu called', function () {
$magicScope.showMenu();
$timeout.flush();
expect($magicScope.isMenuOpen).toBe(true);
});
it('isMenuOpen should be false after hideMenu called', function () {
$magicScope.showMenu();
$timeout.flush();
$magicScope.hideMenu();
$timeout.flush();
expect($magicScope.isMenuOpen).toBe(false);
});
it('initSearch should be called when facetsChanged broadcasted', function () {
$scope.$broadcast('facetsChanged');
$timeout.flush();
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.initSearch).toHaveBeenCalled();
});
it('currentSearch should be empty when URL has no search terms', function () {
expect($magicScope.currentSearch).toEqual([]);
});
describe('initFacets', function () {
it('currentSearch should have one item when URL has one search term', function () {
$window.location.search = '?name=myname';
$magicScope.initFacets();
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(1);
expect($magicScope.currentSearch[0].label).toEqual([ 'Name', 'myname' ]);
expect($magicScope.currentSearch[0].name).toBe('name=myname');
expect($magicScope.strings.prompt).toBe('');
// 'name' facet should be deleted (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'name', 'myname' ]);
});
it('currentSearch should have one item when given one search term', function () {
var currentFacets = [{name: 'name=myname'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(1);
expect($magicScope.currentSearch[0].label).toEqual([ 'Name', 'myname' ]);
expect($magicScope.currentSearch[0].name).toBe('name=myname');
// 'name' facet should be deleted (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'name', 'myname' ]);
});
it('currentSearch should have two items when given two search terms', function () {
var currentFacets = [{name: 'name=myname'}, {name: 'status=active'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
// only 'active' option should be removed from 'status' facet (not singleton)
expect($magicScope.currentSearch.length).toBe(2);
expect($magicScope.deleteFacetSelection).toHaveBeenCalledWith([ 'status', 'active' ]);
});
it('flavor facet should be removed if search term includes flavor', function () {
var currentFacets = [{name: 'flavor=m1.tiny'}];
$magicScope.initFacets(currentFacets);
$timeout.flush();
// entire 'flavor' facet should be removed even if some options left (singleton)
expect($magicScope.deleteFacetEntirely).toHaveBeenCalledWith([ 'flavor', 'm1.tiny' ]);
});
it('currentSearch should have one item when search is textSearch', function () {
$magicScope.textSearch = 'test';
$magicScope.initFacets([]);
$timeout.flush();
expect($magicScope.currentSearch[0].label).toEqual([ 'Text', 'test' ]);
expect($magicScope.currentSearch[0].name).toBe('text=test');
});
it('currentSearch should have textSearch and currentSearch', function () {
$magicScope.textSearch = 'test';
$magicScope.initFacets([{name: 'flavor=m1.tiny'}]);
$timeout.flush();
expect($magicScope.currentSearch.length).toBe(2);
expect($magicScope.currentSearch[0].label).toEqual([ 'Flavor', 'm1.tiny' ]);
expect($magicScope.currentSearch[0].name).toBe('flavor=m1.tiny');
expect($magicScope.currentSearch[1].label).toEqual([ 'Text', 'test' ]);
expect($magicScope.currentSearch[1].name).toBe('text=test');
});
it('should call checkFacets when initFacets called', function () {
$magicScope.initFacets([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
});
});
describe('removeFacet', function () {
beforeEach(function () {
spyOn($magicScope, 'initFacets').and.callThrough();
});
it('should call emitQuery, initFacets and emit checkFacets on removeFacet', function () {
var initialSearch = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
$magicScope.currentSearch.push(initialSearch);
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.emitQuery).toHaveBeenCalledWith('name=myname');
expect($magicScope.initFacets).toHaveBeenCalledWith([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
expect($magicScope.strings.prompt).toBe('Prompt');
});
it('prompt text === "" if search terms left after removal of one', function () {
$magicScope.strings.prompt = '';
$magicScope.currentSearch.push({ name: 'name=myname', label: [ 'Name', 'myname' ] });
$magicScope.currentSearch.push({ name: 'status=active', label: [ 'Status', 'Active' ] });
$magicScope.removeFacet(0);
expect($magicScope.strings.prompt).toBe('');
});
it('should emit checkFacets on removeFacet if facetSelected', function () {
var initialSearch = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
$magicScope.currentSearch.push(initialSearch);
$magicScope.facetSelected = {
'name': 'status',
'label': [ 'Status', 'active' ]
};
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([]);
expect($magicScope.resetState).toHaveBeenCalled();
expect($magicScope.initFacets).toHaveBeenCalledWith([]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', []);
});
it('should emit checkFacets and remember state on removeFacet if facetSelected', function () {
var search1 = {
name: 'name=myname',
label: [ 'Name', 'myname' ]
};
var search2 = {
name: 'flavor=m1.tiny',
label: [ 'Flavor', 'm1.tiny' ]
};
$magicScope.currentSearch.push(search1);
$magicScope.currentSearch.push(search2);
$magicScope.facetSelected = {
'name': 'status',
'label': [ 'Status', 'active' ]
};
$magicScope.removeFacet(0);
expect($magicScope.currentSearch).toEqual([search2]);
expect($magicScope.resetState).toHaveBeenCalled();
expect($magicScope.initFacets).toHaveBeenCalledWith([search2]);
expect($magicScope.$emit).toHaveBeenCalledWith('checkFacets', [search2]);
});
});
});
*/
})();

View File

@ -0,0 +1,46 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
/**
* @fileOverview Magic Search JS
* @requires AngularJS
*
*/
angular.module('horizon.framework.widgets.magic-search')
.directive('magicSearch', magicSearch);
magicSearch.$inject = [];
function magicSearch() {
return {
restrict: 'E',
scope: {
facets_param: '@facets',
filter_keys: '=filterKeys',
strings: '=strings'
},
templateUrl: function (scope, elem) {
return elem.template;
},
controller: 'MagicSearchController',
controllerAs: 'ctrl'
};
}
})();

View File

@ -1,11 +1,11 @@
<!--! Magic Searchbar -->
<div class="magic-search" magic-overrides>
<div class="magic-search">
<div class="search-bar">
<span class="fi-filter fa fa-search go"></span>
<span class="search-main-area">
<span class="item-list">
<span class="radius secondary item"
ng-repeat="facet in currentSearch" ng-cloak="cloak" ng-class="{'server-side-item': facet.isServer}">
ng-repeat="facet in ctrl.currentSearch" ng-cloak="cloak" ng-class="{'server-side-item': facet.isServer}">
<span data-toggle="tooltip" title="{$ ::strings.serverFacet $}"
ng-class="{'fa fa-server': facet.isServer}"></span>
<span data-toggle="tooltip" title="{$ ::strings.clientFacet $}"
@ -13,41 +13,41 @@
<span>
{$ ::facet.label[0] $}: <b>{$ ::facet.label[1] $}</b>
</span>
<a class="remove" ng-click="removeFacet($index, $event)" title="{$ ::strings.remove $}">
<a class="remove" ng-click="ctrl.removeFacet($index, $event)" title="{$ ::strings.remove $}">
<span class="fi-x fa fa-times"></span>
</a>
</span>
</span>
<span class="search-selected" ng-cloak="" ng-show="facetSelected">
{$ facetSelected.label[0] $}:
<span class="search-selected" ng-cloak="" ng-show="ctrl.facetSelected">
{$ ctrl.facetSelected.label[0] $}:
</span>
<!-- For bootstrap, the dropdown attribute is moved from input up to div. -->
<span class="search-entry" dropdown is-open="isMenuOpen">
<span class="search-entry" dropdown is-open="ctrl.isMenuOpen">
<input class="search-input" type="text" dropdown-toggle
placeholder="{$ strings.prompt $}" autocomplete="off" />
<ul class="facet-drop f-dropdown dropdown-menu" data-dropdown-content="">
<li ng-repeat="facet in filteredObj" ng-show="!facetSelected">
<a ng-click="facetClicked($index, $event, facet.name)"
ng-show="!isMatchLabel(facet.label)">{$ ::facet.label $}</a>
<a ng-click="facetClicked($index, $event, facet.name)"
ng-show="isMatchLabel(facet.label)">
<li ng-repeat="facet in ctrl.filteredObj" ng-show="!ctrl.facetSelected">
<a ng-click="ctrl.facetClicked($index, $event, facet.name)"
ng-show="!ctrl.isMatchLabel(facet.label)">{$ ::facet.label $}</a>
<a ng-click="ctrl.facetClicked($index, $event, facet.name)"
ng-show="ctrl.isMatchLabel(facet.label)">
{$ ::facet.label[0] $}<span class="match">{$ ::facet.label[1] $}</span>{$ ::facet.label[2] $}
</a>
</li>
<li ng-repeat="option in filteredOptions" ng-show="facetSelected">
<a ng-click="optionClicked($index, $event, option.key)"
ng-show="!isMatchLabel(option.label)">
<li ng-repeat="option in ctrl.filteredOptions" ng-show="ctrl.facetSelected">
<a ng-click="ctrl.optionClicked($index, $event, option.key)"
ng-show="!ctrl.isMatchLabel(option.label)">
{$ option.label $}
</a>
<a ng-click="optionClicked($index, $event, option.key)"
ng-show="isMatchLabel(option.label)">
<a ng-click="ctrl.optionClicked($index, $event, option.key)"
ng-show="ctrl.isMatchLabel(option.label)">
{$ ::option.label[0] $}<span class="match">{$ ::option.label[1] $}</span>{$ ::option.label[2] $}
</a>
</li>
</ul>
</span>
</span>
<a class="magic-search-clear" ng-click="clearSearch()" ng-show="currentSearch.length &gt; 0" title="{$ ::strings.cancel $}">
<a class="magic-search-clear" ng-click="ctrl.clearSearch()" ng-show="ctrl.currentSearch.length &gt; 0" title="{$ ::strings.cancel $}">
<span class="fi-x fa fa-times cancel"></span>
</a>
</div>

View File

@ -33,6 +33,6 @@
*
*/
angular
.module('MagicSearch', []);
.module('horizon.framework.widgets.magic-search', ['ui.bootstrap']);
})();

View File

@ -0,0 +1,324 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
/**
* @fileOverview Magic Search JS
* @requires AngularJS
*
* Common terminology:
* Search Term - A single unit of a search as a string. This is the
* minimal representation of a search.
* e.g. 'status=active'
*
* Search Term Object - An object representation of a search term,
* e.g. {field: 'status', value: 'active'}
*
* Facet - An object representing a unit of a search. Includes the
* search term from above. A search is composed of a list of facets.
* Note, this contains all the cross-referenced labels from the facet
* choice.
* e.g. {name: 'status=active', singleton=true,
* label: ['Status', 'Active']}
*
* Facet Choice - An object representing a type of facet that can be used
* in a search.
* e.g. {name: 'status', singleton=true, options: [...]}
*
* Option Choice - An object representing an option for a facet choice.
* e.g. {key: 'active', label: ['first', 'middle', 'last'] TODO check
*/
angular.module('horizon.framework.widgets.magic-search')
.factory('horizon.framework.widgets.magic-search.service', magicSearchService);
magicSearchService.$inject = [];
/**
* @ngdoc service
* @name horizon.framework.widgets.magic-search.service
*
* @returns the service.
*/
function magicSearchService() {
var service = {
getFacetChoice: getFacetChoice,
removeOptionChoice: removeOptionChoice,
removeFacetChoice: removeFacetChoice,
removeChoice: removeChoice,
getEventCode: getEventCode,
getFacet: getFacet,
getSearchTermsFromQueryString: getSearchTermsFromQueryString,
getFacetChoicesFromFacetsParam: getFacetChoicesFromFacetsParam,
getFacetsFromSearchTerms: getFacetsFromSearchTerms,
getSearchTermObject: getSearchTermObject,
getMatchingFacets: getMatchingFacets,
getMatchingOptions: getMatchingOptions,
getName: getName,
getTextFacet: getTextFacet,
getUnusedFacetChoices: getUnusedFacetChoices,
getQueryPattern: getQueryPattern
};
return service;
// The following functions are primarily used to assist with various
// map/reduce/filter uses in other functions.
function objectify(obj) {
return Object.create(obj);
}
function hasOptions(item) {
return angular.isDefined(item.options);
}
function getTextFacet(searchVal, label) {
return getFacet('text', searchVal, label, searchVal);
}
function getFacet(field, value, typeLabel, searchLabel) {
return {'name': field + '=' + value, 'label': [typeLabel, searchLabel]};
}
function getSearchTermsFromQueryString(queryString) {
return queryString.replace(/^\?/, '').split('&');
}
function getName(obj) {
return obj.name;
}
function getQueryPattern(searchTermList) {
return searchTermList.filter(isNotTextSearch).map(getName).join('&');
function isNotTextSearch(item) {
return item.name.indexOf('text') !== 0;
}
}
function matchesName(name) {
return function(facet) {
return name === facet.name;
};
}
function matchesKey(name) {
return function(option) {
return name === option.key;
};
}
function hasLabel(item) {
return angular.isDefined(item.label);
}
function getSearchTermObject(str) {
var parts = str.split('=');
return {type: parts[0], value: parts[1]};
}
// Given an item with a label, returns an array of three parts if the
// string is a substring of the label. Returns undefined if no match.
// e.g.: 'searchforme', 'for' -> ['search', 'for', 'me']
// Used to construct labels for options and facet choices based on
// search terms.
// TODO: not sure where the third element is used.
function itemToLabel(item, search) {
var idx = item.label.toLowerCase().indexOf(search);
if (idx > -1) {
return [item.label.substring(0, idx),
item.label.substring(idx, idx + search.length),
item.label.substring(idx + search.length)];
}
}
// Helper function to more obviously perform the function
// for the choice(s) in the facet list that match by name.
// In theory there should only be one, but that's not enforceable.
// The function should expect that the single parameter is the matching
// choice.
function execForMatchingChoice(facetChoices, name, func) {
facetChoices.filter(matchesName(name)).forEach(func);
}
// Exposed functions
function getEventCode(evt) {
return evt.which || evt.keyCode || evt.charCode;
}
function getFacetChoice(orig) {
var facetChoice = objectify(orig);
// if there are options, copy their objects as well. Expects a list.
if (angular.isDefined(orig.options)) {
facetChoice.options = orig.options.map(objectify);
}
return facetChoice;
}
// Translates options, returning only those that actually got labels
// (those that matched the search).
function getMatchingOptions(list, search) {
return list.map(processOption).filter(hasLabel);
function processOption(option) {
return {'key': option.key, 'label':itemToLabel(option, search)};
}
}
// Translates facets, returning only those that actually got labels
// (those that matched the search).
function getMatchingFacets(list, search) {
return list.map(processFacet).filter(hasLabel);
function processFacet(facet) {
return {'name':facet.name, 'label':itemToLabel(facet, search),
'options':facet.options};
}
}
function getFacetChoicesFromFacetsParam(param) {
if (angular.isString(param)) {
// Parse facets JSON and convert to a list of facets.
var tmp = param.replace(/__apos__/g, "\'")
.replace(/__dquote__/g, '\\"')
.replace(/__bslash__/g, "\\");
return angular.fromJson(tmp);
}
// Assume this is a usable javascript object
return param;
}
// Takes in search terms in the form of field=value, ...
// then returns a list of facets, complete with
// labels. Basically, a merge of data from the current search
// and the facet choices.
function getFacetsFromSearchTerms(searchTerms, textSearch, textSearchLabel, facetChoices) {
var buff = [];
searchTerms.map(getSearchTermObject).forEach(getFacetFromObj);
if (angular.isDefined(textSearch)) {
buff.push(getTextFacet(textSearch, textSearchLabel));
}
return buff;
function getFacetFromObj(searchTermObj) {
execForMatchingChoice(facetChoices, searchTermObj.type, addFacet);
function addFacet(facetChoice) {
if (angular.isUndefined(facetChoice.options)) {
buff.push(getFacet(searchTermObj.type, searchTermObj.value,
facetChoice.label, searchTermObj.value));
} else {
facetChoice.options.filter(matchesKey(searchTermObj.value)).forEach(function (option) {
buff.push(getFacet(searchTermObj.type, searchTermObj.value,
facetChoice.label, option.label));
});
}
}
}
}
// The rest of the functions have to do entirely with removing
// facets from the choices that are presented to the user.
// Retrieves Facet Choices and returns only those not used in the
// given facets (those not in the current search).
function getUnusedFacetChoices(facetChoices, facets) {
var unused = angular.copy(facetChoices);
facets.map(getSearchTermObject).forEach(processSearchTerm);
return unused;
function processSearchTerm(searchTerm) {
// finds any/all matching choices (should only be one)
execForMatchingChoice(unused, searchTerm.type, removeFoundChoice);
function removeFoundChoice(choice) {
if (angular.isUndefined(choice.options)) {
// for refresh case, need to remove facets that were
// bookmarked/current when browser refresh was clicked
removeFacetChoice(searchTerm.type, unused);
} else if (choice.options.some(matchesKey(searchTerm.value))) {
removeSingleChoice(choice, searchTerm, unused);
}
}
}
}
// remove entire facet choice
function removeFacetChoice(type, target) {
execForMatchingChoice(target.slice(), type, removeFacet);
function removeFacet(facet) {
target.splice(target.indexOf(facet), 1);
}
}
// Removes a choice from the target, based on the values in the search
// term object. If the choice is of type 'singleton', the entire
// facet choice is removed; otherwise it removes the choice's option.
function removeSingleChoice(facetChoice, searchTermObj, target) {
if (facetChoice.singleton === true) {
removeFacetChoice(searchTermObj.type, target);
} else {
removeOptionChoice(searchTermObj, target);
}
}
// Removes an item from the given target list; uses src (list of the types)
// so it may reference whether the type is a singleton or not.
function removeChoice(searchTerm, src, target) {
execForMatchingChoice(src, searchTerm.type, removeFacetOrOption);
function removeFacetOrOption(facet) {
removeSingleChoice(facet, searchTerm, target);
}
}
// Removes an option from a facet choice, based on a search term object.
function removeOptionChoice(searchTermObj, target) {
execForMatchingChoice(target.slice().filter(hasOptions),
searchTermObj.type, removeOption);
function removeOption(choice) {
// Slim down choices based on key match.
choice.options = choice.options.filter(keyNotMatch(searchTermObj.value));
// If there are no remaining options for this choice, remove the
// choice entirely.
if (choice.options.length === 0) {
// Manipulating a list that it's going through.
// This happens to work due to how it is iterated and searched
// but arguably is not ideal. We want to retain the original
// array object due to references elsewhere.
target.splice(target.indexOf(choice), 1);
}
function keyNotMatch(value) {
return function keyNotMatchBool(option) {
return option.key !== value;
};
}
}
}
}
})();

View File

@ -0,0 +1,329 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
"use strict";
describe('magic-search service', function () {
var service;
beforeEach(module("horizon.framework.widgets.magic-search"));
beforeEach(inject(function($injector) {
service = $injector.get('horizon.framework.widgets.magic-search.service');
}));
it('should have been defined', function () {
expect(service).toBeDefined();
});
describe("getFacet", function() {
it("produces the expected output", function() {
expect(service.getFacet('abc', 'def', 'type-label', 'option-label'))
.toEqual({'name': 'abc=def', 'label': ['type-label', 'option-label']});
});
});
describe("getTextFacet", function() {
it("produces the expected output", function() {
expect(service.getTextFacet('abc', 'type-label'))
.toEqual({'name': 'text=abc', 'label': ['type-label', 'abc']});
});
});
describe('getEventCode', function() {
it("looks in evt.which", function() {
var evt = {which: 42};
expect(service.getEventCode(evt)).toBe(42);
});
it("looks in evt.keyCode", function() {
var evt = {keyCode: 42};
expect(service.getEventCode(evt)).toBe(42);
});
it("looks in evt.charCode", function() {
var evt = {charCode: 42};
expect(service.getEventCode(evt)).toBe(42);
});
it("returns undefined if no code set", function() {
var evt = {};
expect(service.getEventCode(evt)).toBeUndefined();
});
});
describe('getFacetChoice', function() {
it("copies facets", function() {
var input = {a: 'apple'};
var output = service.getFacetChoice(input);
expect(output.a).toBe('apple');
});
it("copies facets' options", function() {
var input = {a: 'apple', options: [{b: 'badwolf'}]};
var output = service.getFacetChoice(input);
expect(output.a).toBe('apple');
expect(output.options[0].b).toBe('badwolf');
});
});
describe('getQueryPattern', function() {
it("returns an empty query if nothing in input", function() {
expect(service.getQueryPattern([])).toBe('');
});
it("returns proper values", function() {
expect(service.getQueryPattern([{name: 'mytext'}])).toBe('mytext');
expect(service.getQueryPattern([{name: 'nothing'}, {name: 'mytext'}]))
.toBe('nothing&mytext');
});
it("doesn't process items with name starting with 'text'", function() {
expect(service.getQueryPattern([{name: 'nothing'}, {name: 'mytext'},
{name: 'textbad'}])).toBe('nothing&mytext');
});
});
describe('removeFacetChoice', function() {
it("removes items with the given name", function() {
var target = [{name: 'me'}, {name: 'me'}, {name: 'notme'}];
var remove = "me";
service.removeFacetChoice(remove, target);
expect(target).toEqual([{name: 'notme'}]);
});
});
describe("getMatchingOptions", function() {
it("filters properly", function() {
var list = [{key: "wallie", label: "findwaldonow"}, {label: "monster"}];
var search = "waldo";
var result = service.getMatchingOptions(list, search);
expect(result).toEqual([{key: 'wallie', label: ['find', 'waldo', 'now']}]);
});
});
describe("getFacetChoicesFromFacetsParam", function() {
it("returns any object passed if not a string", function() {
expect(service.getFacetChoicesFromFacetsParam({my: "thing"})).toEqual({my: "thing"});
});
it("processes a basic JSON string", function() {
expect(service.getFacetChoicesFromFacetsParam('{"my": "thing"}')).toEqual({my: "thing"});
});
it("processes a JSON strings, translating characters", function() {
expect(service.getFacetChoicesFromFacetsParam('{"my": "\\\\thing\'s"}'))
.toEqual({my: "\\thing's"});
});
});
describe("getSearchTermsFromQueryString", function() {
it("returns split of values if no leading question mark", function() {
var input = "this&is&amazing";
expect(service.getSearchTermsFromQueryString(input)).toEqual(['this', 'is', 'amazing']);
});
it("returns split of values if leading question mark", function() {
var input = "?this&is&amazing";
expect(service.getSearchTermsFromQueryString(input)).toEqual(['this', 'is', 'amazing']);
});
});
describe("getName", function() {
it("extracts the name", function() {
expect(service.getName({name: 'Joe'})).toEqual('Joe');
});
});
describe("getFacetsFromSearchTerms", function() {
var types, searchTerm;
beforeEach(function() {
types = [{name: 'a', label: 'Apple'}, {name: 'b'}, {name: 'c'}];
});
it("returns nothing if given nothing", function() {
expect(service.getFacetsFromSearchTerms([], searchTerm, 'txt', types)).toEqual([]);
});
it("returns nothing if nothing matching", function() {
var input = ["z=zebra"];
expect(service.getFacetsFromSearchTerms(input, searchTerm, 'txt', types)).toEqual([]);
});
it("returns proper facet if given a match", function() {
var input = ["a=apple"];
expect(service.getFacetsFromSearchTerms(input, searchTerm, 'txt', types))
.toEqual([{name: 'a=apple', label:['Apple','apple']}]);
});
it("returns proper facet if given a match with options", function() {
var input = ["a=gala"];
types[0].options = [{key: 'gala', label: 'Gala'},{key: 'honeycrisp', label: 'Honeycrisp'}];
expect(service.getFacetsFromSearchTerms(input, searchTerm, 'txt', types))
.toEqual([{name: 'a=gala', label:['Apple','Gala']}]);
});
it("appends textSearch facet if given a match and a textSearch", function() {
var input = ["a=apple"];
searchTerm = 'searchme';
expect(service.getFacetsFromSearchTerms(input, searchTerm, 'txt', types))
.toEqual([{name: 'a=apple', label:['Apple','apple']},
{name: 'text=searchme', label: ['txt', 'searchme']}]);
});
});
describe("getMatchingFacets", function() {
it("filters properly", function() {
var list = [{name: "wallie", label: "findwaldonow", options: []}, {label: "monster"}];
var search = "waldo";
var result = service.getMatchingFacets(list, search);
expect(result).toEqual([{name: 'wallie', label: ['find', 'waldo', 'now'], options: []}]);
});
});
describe("removeChoice", function() {
it("deletes the singletons", function() {
var facet = {name: "deleteme", singleton: true, options: []};
var src = [facet, {name: "ok"}];
var target = [facet, {name: "can't get me"}];
var name = {type: "deleteme", value: {}};
service.removeChoice(name, src, target);
expect(target).toEqual([{name: "can't get me"}]);
});
it("deletes the non-singletons", function() {
var facet = {name: "deleteme", singleton: false, options: []};
var src = [facet];
var target = [facet];
var name = {type: "deleteme", value: {}};
service.removeChoice(name, src, target);
expect(target).toEqual([]);
});
});
describe('removeOptionChoice', function() {
it("removes nothing if no matches", function() {
var facets = [{name: 'one', options: [{}]}, {name: 'two'}];
var facetParts = ['nomatch', {}];
service.removeOptionChoice(facetParts, facets);
expect(facets).toEqual([{name: 'one', options: [{}]}, {name: 'two'}]);
});
it("removes nothing if matches but no options", function() {
var facets = [{name: 'one', options: [{}]}, {name: 'two'}];
var facetParts = ['two', {}];
service.removeOptionChoice(facetParts, facets);
expect(facets).toEqual([{name: 'one', options: [{}]}, {name: 'two'}]);
});
it("removes item if matches and has empty options", function() {
var facets = [{name: 'one', options: []}, {name: 'two'}];
var remove = {type: 'one', value: {}};
service.removeOptionChoice(remove, facets);
expect(facets).toEqual([{name: 'two'}]);
});
it("removes option if key matches but doesn't remove item if options remain", function() {
var facets = [{name: 'one', options: [{key: 'keymatch'},
{key: 'notmatch'},{key: 'another'}]}, {name: 'two'}];
var remove = {type: 'one', value: 'keymatch'};
service.removeOptionChoice(remove, facets);
expect(facets).toEqual([{name: 'one',
options: [{key: 'notmatch'}, {key: 'another'}]}, {name: 'two'}]);
});
it("removes all options if key matches", function() {
var facets = [{name: 'one', options: [{key: 'keymatch'},
{key: 'keymatch'},{key: 'another'}]}, {name: 'two'}];
var remove = {type: 'one', value: 'keymatch'};
service.removeOptionChoice(remove, facets);
expect(facets).toEqual([{name: 'one', options: [{key: 'another'}]}, {name: 'two'}]);
});
it("removes all facets if name matches", function() {
var facets = [{name: 'one', options: []}, {name: 'one', options: []}, {name: 'two'}];
var remove = {type: 'one', value: {}};
service.removeOptionChoice(remove, facets);
expect(facets).toEqual([{name: 'two'}]);
});
it("does nothing if no options defined for a facet", function() {
var facets = [{name: 'one', options: []}, {name: 'one'}, {name: 'two'}];
var remove = {type: 'one', value: {}};
service.removeOptionChoice(remove, facets);
expect(facets).toEqual([{name: 'one'}, {name: 'two'}]);
});
});
describe("getUnusedFacetChoices", function() {
it("does nothing with unmatched facet name", function() {
var facets = ["one=thing", "two=thing"];
var choices = [{name: "something"}];
var unused = service.getUnusedFacetChoices(choices, facets);
expect(unused).toEqual([{name: "something"}]);
});
it("removes facet with matched facet name and no options", function() {
var facets = ["something=thing", "two=thing"];
var choices = [{name: "something"}];
var unused = service.getUnusedFacetChoices(choices, facets);
expect(unused).toEqual([]);
});
it("removes facet with matched facet name and options", function() {
var facets = ["something=thing", "two=thing"];
var choices = [{name: "something", options: [{key: 'thing'}]},
{name: "something", options: [{key: 'other'}]},{name: "other"}];
var unused = service.getUnusedFacetChoices(choices, facets);
expect(unused).toEqual([ {name: "something", options: [{key: 'other'}]},
{name: "other"}]);
});
it("removes option with matched facet name and options", function() {
var facets = ["something=thing", "two=thing"];
var choices = [{name: "something", options: [{key: 'thing'}, {key: 'other'}]},
{name: "something", options: [{key: 'other'}]},{name: "other"}];
var unused = service.getUnusedFacetChoices(choices, facets);
expect(unused).toEqual([ {name: "something", options: [{key: 'other'}]},
{name: "something", options: [{key: 'other'}]}, {name: "other"}]);
});
it("removes facet with matched facet name and options if a singleton", function() {
var facets = ["something=thing", "another=thing"];
var choices = [{name: "something", singleton: true,
options: [{key: 'thing'}, {key: 'other'}]},
{name: "another", options: [{key: 'thing'}, {key: 'more'}]},{name: "other"}];
var unused = service.getUnusedFacetChoices(choices, facets);
expect(unused).toEqual([ {name: "another", options: [{key: 'more'}]},
{name: "other"}]);
});
});
});
})();

View File

@ -15,7 +15,7 @@
'use strict';
angular
.module('MagicSearch')
.module('horizon.framework.widgets.magic-search')
.directive('stMagicSearch', stMagicSearch);
stMagicSearch.$inject = ['$timeout', '$window'];

View File

@ -23,7 +23,7 @@
beforeEach(module('templates'));
beforeEach(module('smart-table'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('MagicSearch'));
beforeEach(module('horizon.framework.widgets.magic-search'));
beforeEach(module(function ($provide) {
$provide.value('$window', {
location: {

View File

@ -14,7 +14,7 @@
'horizon.framework.widgets.action-list',
'horizon.framework.widgets.metadata',
'horizon.framework.widgets.toast',
'MagicSearch'
'horizon.framework.widgets.magic-search'
])
.config(config);

View File

@ -38,7 +38,6 @@ import xstatic.pkg.jquery_quicksearch
import xstatic.pkg.jquery_tablesorter
import xstatic.pkg.jquery_ui
import xstatic.pkg.jsencrypt
import xstatic.pkg.magic_search
import xstatic.pkg.mdi
import xstatic.pkg.rickshaw
import xstatic.pkg.roboto_fontface
@ -101,9 +100,6 @@ def get_staticfiles_dirs(webroot='/'):
('horizon/lib/jsencrypt',
xstatic.main.XStatic(xstatic.pkg.jsencrypt,
root_url=webroot).base_dir),
('horizon/lib/magic_search',
xstatic.main.XStatic(xstatic.pkg.magic_search,
root_url=webroot).base_dir),
('horizon/lib/mdi',
xstatic.main.XStatic(xstatic.pkg.mdi,
root_url=webroot).base_dir),

View File

@ -60,8 +60,6 @@
<script src='{{ STATIC_URL }}{{ file }}'></script>
{% endfor %}
<script src="{{ STATIC_URL }}horizon/lib/magic_search/magic_search.js"></script>
{% block custom_js_files %}{% endblock %}
{% endcompress %}

View File

@ -55,7 +55,6 @@ XStatic-JQuery.quicksearch>=2.0.3.1 # MIT License
XStatic-JQuery.TableSorter>=2.14.5.1 # MIT License
XStatic-jquery-ui>=1.10.1 # MIT License
XStatic-JSEncrypt>=2.0.0.2 # MIT License
XStatic-Magic-Search>=0.2.5.1 # Apache 2.0 License
XStatic-mdi==1.1.70.1 # SIL OPEN FONT LICENSE Version 1.1
XStatic-Rickshaw>=1.5.0 # BSD License (prior)
XStatic-roboto-fontface>=0.4.3.2 # Apache 2.0 License