Adding features to MagicSearch

The previous MagicSearch patch was a function-for-function copy of
the source at GitHub.  There are some useful features that were missing
from that.

This patch adds a 'clientFullTextSearch' property to the magic search bar so
the full-text search option may be turned off.

Change-Id: Id46b169c5ba18606d498701b6664fc6374083011
Implements: blueprint integrate-magic-search
This commit is contained in:
Matt Borland 2016-02-01 11:38:25 -07:00 committed by Matt Borland
parent 3088f77d09
commit ed9376fa32
6 changed files with 240 additions and 30 deletions

View File

@ -26,6 +26,7 @@
* @element
* @param {object} filterFacets Facets allowed for searching
* @param {object=} filterStrings Help content shown in search bar
* @param {object=} clientFullTextSearch if full text search is to be done on the client
* @description
* The `magicSearchBar` directive provides a template for a
* client side faceted search that utilizes Smart-Table's
@ -71,6 +72,13 @@
* <hz-magic-search-bar
* filter-facets="filterFacets">
* </hz-magic-search-bar>
*
* or
*
* <hz-magic-search-bar
* client-full-text-search="false"
* filter-strings="filterStrings">
* </hz-magic-search-bar>
* ```
*/
function hzMagicSearchBar(basePath) {
@ -80,7 +88,9 @@
restrict: 'E',
scope: {
filterStrings: '=?',
filterFacets: '='
filterFacets: '=',
clientFullTextSearch: '=?',
searchSettingsCallback: '=?'
},
templateUrl: basePath + 'magic-search/hz-magic-search-bar.html'
};
@ -90,22 +100,33 @@
//////////
function link(scope) {
scope.clientFullTextSearch = angular.isDefined(scope.clientFullTextSearch)
? scope.clientFullTextSearch
: true;
// if filterStrings is not defined, set defaults
var defaultFilterStrings = {
cancel: gettext('Cancel'),
prompt: gettext('Click here for filters.'),
remove: gettext('Remove'),
text: gettext('In current results')
text: (scope.clientFullTextSearch ?
gettext('Search in current results') :
gettext('Full Text Search'))
};
scope.filterStrings = scope.filterStrings ? scope.filterStrings : defaultFilterStrings;
scope.filterStrings = scope.filterStrings || defaultFilterStrings;
if (angular.isDefined(scope.searchSettingsCallback)) {
scope.showSettings = true;
} else {
scope.showSettings = false;
}
}
function compile(element) {
/**
* Need to set template here since MagicSearch template
* attribute is not interpolated. Can't hardcode the
* template location and need to use basePath.
*/
* Need to set template here since MagicSearch template
* attribute is not interpolated. Can't hardcode the
* template location and need to use basePath.
*/
var templateUrl = basePath + 'magic-search/magic-search.html';
element.find('magic-search').attr('template', templateUrl);
element.addClass('hz-magic-search-bar');

View File

@ -93,6 +93,47 @@
expect(searchBar.length).toBe(1);
});
it('sets the clientFullTextSearch to false', function () {
var markup = '<table st-table="rows">' +
'<thead>' +
' <tr>' +
' <th>' +
' <hz-magic-search-bar ' +
' client-full-text-search="false"' +
' filter-facets="filterFacets">' +
' </hz-magic-search-bar>' +
' </th>' +
' </tr>' +
'</thead>' +
'<tbody></tbody>' +
'</table>';
var $element = $compile(angular.element(markup))($scope);
$scope.$apply();
expect($element.find('magic-search').scope().clientFullTextSearch).toEqual(false);
});
it('sets the clientFullTextSearch to true', function () {
var markup = '<table st-table="rows">' +
'<thead>' +
' <tr>' +
' <th>' +
' <hz-magic-search-bar ' +
' filter-facets="filterFacets">' +
' </hz-magic-search-bar>' +
' </th>' +
' </tr>' +
'</thead>' +
'<tbody></tbody>' +
'</table>';
var $element = $compile(angular.element(markup))($scope);
$scope.$apply();
expect($element.find('magic-search').scope().clientFullTextSearch).toEqual(true);
});
it('use filterStrings defaults if not provided as attribute', function () {
var markup = '<table st-table="rows">' +
'<thead>' +

View File

@ -330,9 +330,13 @@
* Broadcast event when facet options are returned via AJAX.
* Should magic_search.js absorb this?
*/
var facetsChangedWatcher = $scope.$on('facetsChanged', function () {
var facetsChangedWatcher = $scope.$on('facetsChanged', function (event, data) {
$timeout(function () {
initSearch([]);
if (data && data.magicSearchQuery) {
initSearch(data.magicSearchQuery.split('&'));
} else {
initSearch(ctrl.currentSearch.map(function(x) { return x.name; }));
}
});
});
@ -347,7 +351,6 @@
}
ctrl.currentSearch = service.getFacetsFromSearchTerms(searchTerms,
ctrl.textSearch, $scope.strings.text, tmpFacetChoices);
ctrl.filteredObj = ctrl.unusedFacetChoices =
service.getUnusedFacetChoices(tmpFacetChoices, searchTerms);

View File

@ -214,7 +214,12 @@
var buff = [];
searchTerms.map(getSearchTermObject).forEach(getFacetFromObj);
if (angular.isDefined(textSearch)) {
buff.push(getTextFacet(textSearch, textSearchLabel));
var currentTextSearch = searchTerms.filter(function(searchField) {
return searchField.indexOf(textSearch) === 0;
});
if (currentTextSearch.length === 0) {
buff.push(getTextFacet(textSearch, textSearchLabel));
}
}
return buff;

View File

@ -18,7 +18,7 @@
.module('horizon.framework.widgets.magic-search')
.directive('stMagicSearch', stMagicSearch);
stMagicSearch.$inject = ['$timeout', '$window'];
stMagicSearch.$inject = ['$timeout'];
/**
* @ngdoc directive
@ -45,7 +45,7 @@
* </st-magic-search>
* ```
*/
function stMagicSearch($timeout, $window) {
function stMagicSearch($timeout) {
var directive = {
link: link,
require: '^stTable',
@ -55,6 +55,12 @@
return directive;
function link(scope, element, attr, tableCtrl) {
var clientFullTextSearch = (angular.isDefined(scope.clientFullTextSearch)
? scope.clientFullTextSearch
: true);
scope.currentServerSearchParams = {};
// Generate predicate object from dot notation string
function setPredObj(predicates, predObj, input) {
var lastPred = predicates.pop();
@ -66,26 +72,52 @@
predObj[lastPred] = input;
}
function setServerFacetSearch(scope, query) {
var currentServerSearchParams = angular.copy(scope.currentServerSearchParams);
currentServerSearchParams.magicSearchQuery = query;
checkAndEmit(scope, currentServerSearchParams);
}
function setServerTextSearch(scope, text) {
var currentServerSearchParams = angular.copy(scope.currentServerSearchParams);
currentServerSearchParams.queryString = text;
checkAndEmit(scope, currentServerSearchParams);
}
function checkAndEmit(scope, serverSearchParams) {
if (serverSearchParams !== scope.currentServerSearchParams) {
serverSearchParams.magicSearchQueryChanged =
!angular.equals(scope.currentServerSearchParams.magicSearchQuery,
serverSearchParams.magicSearchQuery);
serverSearchParams.queryStringChanged =
!angular.equals(scope.currentServerSearchParams.queryString,
serverSearchParams.queryString);
scope.currentServerSearchParams = serverSearchParams;
if (serverSearchParams.queryStringChanged || serverSearchParams.magicSearchQueryChanged) {
scope.$emit('serverSearchUpdated', angular.copy(scope.currentServerSearchParams));
}
}
}
// When user types a character, search the table
var textSearchWatcher = scope.$on('textSearch', function(event, text) {
// Timeout needed to prevent
// $apply already in progress error
$timeout(function() {
tableCtrl.search(text);
if (clientFullTextSearch) {
tableCtrl.search(text);
} else {
setServerTextSearch(scope, text);
}
});
});
// When user changes a facet, use API filter
var searchUpdatedWatcher = scope.$on('searchUpdated', function(event, query) {
// update url
var url = $window.location.href;
if (url.indexOf('?') > -1) {
url = url.split('?')[0];
}
if (query.length > 0) {
url = url + '?' + query;
}
$window.history.pushState(query, '', url);
setServerFacetSearch(scope, query);
// clear each time since Smart-Table
// search is cumulative

View File

@ -109,38 +109,146 @@
'<tbody>' +
' <tr ng-repeat="row in rows">' +
' <td>{{ row.name }}</td>' +
' <td>{{ row.status }}</td>' +
' </tr>' +
'</tbody>' +
'</table>';
$element = $compile(angular.element(markup))($scope);
$element = $compile(angular.element(markup));
$scope.$apply();
}));
it('should filter table to two rows if text searching with "active"', function () {
it('should filter table to two rows if text searching with "shutdown"', function () {
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('textSearch', 'shutdown');
$timeout.flush();
expect(element.find('tbody tr').length).toBe(2);
});
it('should skip text searching if clientFullTextSearch is false and raise events', function () {
spyOn($scope, '$emit').and.callThrough();
$scope.clientFullTextSearch = false;
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('textSearch', 'active');
$timeout.flush();
expect($element.find('tbody tr').length).toBe(2);
expect(element.find('tbody tr').length).toBe(6);
expect($scope.$emit).toHaveBeenCalledWith(
'serverSearchUpdated',
{
//magicSearchQuery: '',
magicSearchQueryChanged: false,
queryString: 'active',
queryStringChanged: true
}
);
});
it('should not raise serverSearchUpdated event if nothing has changed', function () {
spyOn($scope, '$emit').and.callThrough();
$scope.clientFullTextSearch = false;
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('textSearch', 'active');
$timeout.flush();
$scope.$broadcast('textSearch', 'active');
$timeout.flush();
expect(element.find('tbody tr').length).toBe(6);
/*
expect($scope.$emit).toHaveBeenCalledWith(
'serverSearchUpdated',
{
magicSearchQuery: '',
magicSearchQueryChanged: true,
queryStringChanged: false
}
);
*/
expect($scope.$emit).toHaveBeenCalledWith(
'serverSearchUpdated',
{
//magicSearchQuery: '',
magicSearchQueryChanged: false,
queryString: 'active',
queryStringChanged: true
}
);
// Originally expected to be 2.
expect($scope.$emit.calls.count()).toEqual(1);
});
it('should filter table to two rows if facet with static === "shutdown"', function () {
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('searchUpdated', 'status=shutdown');
$timeout.flush();
expect($element.find('tbody tr').length).toBe(2);
expect(element.find('tbody tr').length).toBe(2);
});
it('should filter table to 1 row if facet with name === "name 1"', function () {
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('searchUpdated', 'name=name 1');
$scope.$broadcast('textSearch', 'active');
$timeout.flush();
expect($element.find('tbody tr').length).toBe(1);
expect(element.find('tbody tr').length).toBe(1);
});
it('should not filter table if filter is server side', function () {
it('should not filter table if filter is server side and raise event', function () {
spyOn($scope, '$emit').and.callThrough();
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('searchUpdated', 'server_name=server 1');
$timeout.flush();
expect($element.find('tbody tr').length).toBe(6);
expect(element.find('tbody tr').length).toBe(6);
expect($scope.$emit).toHaveBeenCalledWith(
'serverSearchUpdated',
{
magicSearchQuery: 'server_name=server 1',
magicSearchQueryChanged: true,
queryStringChanged: false
}
);
});
it('should not raise serverSearchUpdated if filter has not changed', function () {
spyOn($scope, '$emit').and.callThrough();
var element = $element($scope);
$scope.$apply();
$scope.$broadcast('searchUpdated', 'server_name=server 1');
$timeout.flush();
$scope.$broadcast('searchUpdated', 'server_name=server 1');
$timeout.flush();
expect(element.find('tbody tr').length).toBe(6);
expect($scope.$emit).toHaveBeenCalledWith(
'serverSearchUpdated',
{
magicSearchQuery: 'server_name=server 1',
magicSearchQueryChanged: true,
queryStringChanged: false
}
);
// Original expectation was 2.
expect($scope.$emit.calls.count()).toEqual(1);
});
});