Files
awx/awx/ui/client/src/shared/smart-search/smart-search.controller.js
Jared Tabor 29869f7093 Merge pull request #5186 from jaredevantabor/smart-search
Smart search, and invalid searches
2017-02-08 12:00:02 -08:00

331 lines
14 KiB
JavaScript

export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n',
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService, i18n) {
let path, relations,
defaults,
queryset,
stateChangeSuccessListener;
if($scope.defaultParams) {
defaults = $scope.defaultParams;
}
else {
// steps through the current tree of $state configurations, grabs default search params
defaults = _.find($state.$current.path, (step) => {
return step.params.hasOwnProperty(`${$scope.iterator}_search`);
}).params[`${$scope.iterator}_search`].config.value;
}
if($scope.querySet) {
queryset = _.cloneDeep($scope.querySet);
}
else {
queryset = $stateParams[`${$scope.iterator}_search`];
}
// build $scope.tags from $stateParams.QuerySet, build fieldset key
init();
function init() {
path = GetBasePath($scope.basePath) || $scope.basePath;
relations = getRelationshipFields($scope.dataset.results);
$scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]);
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
$scope.models = data.models;
$scope.options = data.options.data;
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
});
$scope.searchPlaceholder = $scope.disableSearch ? i18n._('Cannot search running job') : i18n._('Search');
function compareParams(a, b) {
for (let key in a) {
if (!(key in b) || a[key].toString() !== b[key].toString()) {
return false;
}
}
for (let key in b) {
if (!(key in a)) {
return false;
}
}
return true;
}
if(stateChangeSuccessListener) {
stateChangeSuccessListener();
}
stateChangeSuccessListener = $scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
// State has changed - check to see if this is a param change
if(fromState.name === toState.name) {
if(!compareParams(fromParams[`${$scope.iterator}_search`], toParams[`${$scope.iterator}_search`])) {
// Params are not the same - we need to update the search. This should only happen when the user
// hits the forward/back navigation buttons in their browser.
queryset = toParams[`${$scope.iterator}_search`];
qs.search(path, queryset).then((res) => {
$scope.dataset = res.data;
$scope.collection = res.data.results;
});
$scope.searchTerm = null;
$scope.searchTags = stripDefaultParams(queryset);
}
}
});
$scope.$on('$destroy', stateChangeSuccessListener);
$scope.$watch('disableSearch', function(disableSearch){
if(disableSearch) {
$scope.searchPlaceholder = i18n._('Cannot search running job');
}
else {
$scope.searchPlaceholder = i18n._('Search');
}
});
}
// Removes state definition defaults and pagination terms
function stripDefaultParams(params) {
let stripped =_.pick(params, (value, key) => {
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null;
});
let strippedCopy = _.cloneDeep(stripped);
if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){
for (var key in strippedCopy) {
if (strippedCopy.hasOwnProperty(key)) {
let value = strippedCopy[key];
if(_.isArray(value)){
let index = _.indexOf(value, defaults[key]);
value = value.splice(index, 1)[0];
}
}
}
stripped = strippedCopy;
}
return _(strippedCopy).map(qs.decodeParam).flatten().value();
}
// searchable relationships
function getRelationshipFields(dataset) {
let flat = _(dataset).map((value) => {
return _.keys(value.related);
}).flatten().uniq().value();
return flat;
}
function setDefaults(term) {
if ($scope.list.defaultSearchParams) {
return $scope.list.defaultSearchParams(term);
} else {
return {
search: encodeURIComponent(term)
};
}
}
$scope.toggleKeyPane = function() {
$scope.showKeyPane = !$scope.showKeyPane;
};
$scope.clearAll = function(){
let cleared = _.cloneDeep(defaults);
delete cleared.page;
queryset = cleared;
if(!$scope.querySet) {
$state.go('.', {[$scope.iterator + '_search']: queryset}, {notify: false});
}
qs.search(path, queryset).then((res) => {
if($scope.querySet) {
$scope.querySet = queryset;
}
$scope.dataset = res.data;
$scope.collection = res.data.results;
});
$scope.searchTags = stripDefaultParams(queryset);
};
// remove tag, merge new queryset, $state.go
$scope.remove = function(index) {
let tagToRemove = $scope.searchTags.splice(index, 1)[0],
termParts = SmartSearchService.splitTermIntoParts(tagToRemove),
removed;
let removeFromQuerySet = function(set) {
_.each(removed, (value, key) => {
if (Array.isArray(set[key])){
_.remove(set[key], (item) => item === value);
// If the array is now empty, remove that key
if(set[key].length === 0) {
delete set[key];
}
}
else {
delete set[key];
}
});
};
if (termParts.length === 1) {
removed = setDefaults(tagToRemove);
}
else {
let root = termParts[0].split(".")[0].replace(/^-/, '');
let encodeParams = {
term: tagToRemove
};
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
encodeParams.relatedSearchTerm = true;
}
else {
encodeParams.searchTerm = true;
}
removed = qs.encodeParam(encodeParams);
}
else {
removed = setDefaults(termParts[termParts.length-1]);
}
}
removeFromQuerySet(queryset);
if(!$scope.querySet) {
$state.go('.', {
[$scope.iterator + '_search']: queryset }, {notify: false}).then(function(){
// ISSUE: for some reason deleting a tag from a list in a modal does not
// remove the param from $stateParams. Here we'll manually check to make sure
// that that happened and remove it if it didn't.
removeFromQuerySet($stateParams[`${$scope.iterator}_search`]);
});
}
qs.search(path, queryset).then((res) => {
if($scope.querySet) {
$scope.querySet = queryset;
}
$scope.dataset = res.data;
$scope.collection = res.data.results;
});
$scope.searchTags = stripDefaultParams(queryset);
};
// add a search tag, merge new queryset, $state.go()
$scope.add = function(terms) {
let params = {},
origQueryset = _.clone(queryset);
// Remove leading/trailing whitespace if there is any
terms = (terms) ? terms.trim() : "";
if(terms && terms !== '') {
// Split the terms up
let splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
_.forEach(splitTerms, (term) => {
let termParts = SmartSearchService.splitTermIntoParts(term);
function combineSameSearches(a,b){
if (_.isArray(a)) {
return a.concat(b);
}
else {
if(a) {
return [a,b];
}
}
}
// if only a value is provided, search using default keys
if (termParts.length === 1) {
params = _.merge(params, setDefaults(term), combineSameSearches);
} else {
// Figure out if this is a search term
let root = termParts[0].split(".")[0].replace(/^-/, '');
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
}
else {
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches);
}
}
// Its not a search term or a related search term - treat it as a string
else {
params = _.merge(params, setDefaults(term), combineSameSearches);
}
}
});
queryset = _.merge(queryset, params, (objectValue, sourceValue, key, object) => {
if (object[key] && object[key] !== sourceValue){
if(_.isArray(object[key])) {
// Add the new value to the array and return
object[key].push(sourceValue);
return object[key];
}
else {
// Start the array of keys
return [object[key], sourceValue];
}
}
else {
// // https://lodash.com/docs/3.10.1#merge
// If customizer fn returns undefined merging is handled by default _.merge algorithm
return undefined;
}
});
// Go back to the first page after a new search
delete queryset.page;
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
// This transition will not reload controllers/resolves/views
// but will register new $stateParams[$scope.iterator + '_search'] terms
if(!$scope.querySet) {
$state.go('.', {
[$scope.iterator + '_search']: queryset }, {notify: false}).then(function(){
// ISSUE: same as above in $scope.remove. For some reason deleting the page
// from the queryset works for all lists except lists in modals.
delete $stateParams[$scope.iterator + '_search'].page;
});
}
qs.search(path, queryset).then((res) => {
if($scope.querySet) {
$scope.querySet = queryset;
}
$scope.dataset = res.data;
$scope.collection = res.data.results;
})
.catch(function() {
$scope.revertSearch(origQueryset);
});
$scope.searchTerm = null;
$scope.searchTags = stripDefaultParams(queryset);
}
};
$scope.revertSearch = function(queryToBeRestored) {
queryset = queryToBeRestored;
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
// This transition will not reload controllers/resolves/views
// but will register new $stateParams[$scope.iterator + '_search'] terms
if(!$scope.querySet) {
$state.go('.', {
[$scope.iterator + '_search']: queryset }, {notify: false});
}
qs.search(path, queryset).then((res) => {
if($scope.querySet) {
$scope.querySet = queryset;
}
$scope.dataset = res.data;
$scope.collection = res.data.results;
});
$scope.searchTerm = null;
$scope.searchTags = stripDefaultParams(queryset);
};
}
];