diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index 943f63818b..7d07139c4c 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -44,19 +44,16 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear key = this.replaceDefaultFlags(key); if (!Array.isArray(values)) { - values = this.replaceEncodedTokens(values); - - return `${key}=${values}`; + values = [values]; } return values .map(value => { value = this.replaceDefaultFlags(value); value = this.replaceEncodedTokens(value); - - return `${key}=${value}`; + return [key, value] }) - .join('&'); + }, // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL encodeQueryset(params) { @@ -69,15 +66,33 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear result += '&'; } - return result += this.encodeTerms(value, key); + const encodedTermString = this.encodeTerms(value, key) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + + return result += encodedTermString; }, '?'); }, + // like encodeQueryset, but return an actual unstringified API-consumable http param object + encodeQuerysetObject(params) { + return _.reduce(params, (obj, value, key) => { + const encodedTerms = this.encodeTerms(value, key); + + for (let encodedIndex in encodedTerms) { + const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; + obj[encodedKey] = obj[encodedKey] || []; + obj[encodedKey].push(encodedValue) + } + + return obj; + }, {}); + }, // encodes a ui smart-search param to a django-friendly param // operand:key:comparator:value => {operand__key__comparator: value} - encodeParam(params){ + encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){ // Assumption here is that we have a key and a value so the length // of the paramParts array will be 2. [0] is the key and [1] the value - let paramParts = SmartSearchService.splitTermIntoParts(params.term); + let paramParts = SmartSearchService.splitTermIntoParts(term); let keySplit = paramParts[0].split('.'); let exclude = false; let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; @@ -88,16 +103,16 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear let paramString = exclude ? "not__" : ""; let valueString = paramParts[1]; if(keySplit.length === 1) { - if(params.searchTerm && !lessThanGreaterThan) { - if(params.singleSearchParam) { + if(searchTerm && !lessThanGreaterThan) { + if(singleSearchParam) { paramString += keySplit[0] + '__icontains'; } else { paramString += keySplit[0] + '__icontains_DEFAULT'; } } - else if(params.relatedSearchTerm) { - if(params.singleSearchParam) { + else if(relatedSearchTerm) { + if(singleSearchParam) { paramString += keySplit[0]; } else { @@ -131,8 +146,8 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear } } - if(params.singleSearchParam) { - return {[params.singleSearchParam]: paramString + "=" + valueString}; + if(singleSearchParam) { + return {[singleSearchParam]: paramString + "=" + valueString}; } else { return {[paramString] : encodeURIComponent(valueString)}; @@ -189,7 +204,14 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear return decodeParamString(value); } }, - + convertToSearchTags(obj) { + const tags = []; + for (let key in obj) { + const value = obj[key]; + tags.push(this.decodeParam(value, key)); + } + return tags; + }, // encodes a django queryset for ui-router's URLMatcherFactory // {operand__key__comparator: value, } => 'operand:key:comparator:value;...' // value.isArray expands to: diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index e7dd435307..5a59bbde81 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -1,422 +1,422 @@ -export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigService', '$transitions', - function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, configService, $transitions) { +function SmartSearchController ( + $scope, + $state, + $stateParams, + $transitions, + configService, + GetBasePath, + i18n, + qs, + SmartSearchService +) { + const searchKey = `${$scope.iterator}_search`; + const optionsKey = `${$scope.list.iterator}_options`; - let path, - defaults, - queryset, - transitionSuccessListener; + let path; + let defaults; + let queryset; + let transitionSuccessListener; - configService.getConfig() - .then(config => init(config)); + configService.getConfig() + .then(config => init(config)); - function init(config) { - let version; + function init (config) { + let version; - try { - version = config.version.split('-')[0]; - } catch (err) { - version = 'latest'; - } + try { + [version] = config.version.split('-'); + } catch (err) { + version = 'latest'; + } - $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; + $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; + $scope.searchPlaceholder = i18n._('Search'); - 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) => { - if(step && step.params && step.params.hasOwnProperty(`${$scope.iterator}_search`)){ - return step.params.hasOwnProperty(`${$scope.iterator}_search`); - } - }).params[`${$scope.iterator}_search`].config.value; - } + if ($scope.defaultParams) { + defaults = $scope.defaultParams; + } else { + // steps through the current tree of $state configurations, grabs default search params + const stateConfig = _.find($state.$current.path, step => _.has(step, `params.${searchKey}`)); + defaults = stateConfig.params[searchKey].config.value; + } - if($scope.querySet) { - queryset = _.cloneDeep($scope.querySet); - } - else { - queryset = $state.params[`${$scope.iterator}_search`]; - } + if ($scope.querySet) { + queryset = _.cloneDeep($scope.querySet); + } else { + queryset = $state.params[searchKey]; + } - path = GetBasePath($scope.basePath) || $scope.basePath; - generateSearchTags(); - qs.initFieldset(path, $scope.djangoModel).then((data) => { + path = GetBasePath($scope.basePath) || $scope.basePath; + generateSearchTags(); + + qs.initFieldset(path, $scope.djangoModel) + .then((data) => { $scope.models = data.models; $scope.options = data.options.data; if ($scope.list) { - $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(transitionSuccessListener) { - transitionSuccessListener(); - } - - transitionSuccessListener = $transitions.onSuccess({}, function(trans) { - // State has changed - check to see if this is a param change - if(trans.from().name === trans.to().name) { - if(!compareParams(trans.params('from')[`${$scope.iterator}_search`], trans.params('to')[`${$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 = trans.params('to')[`${$scope.iterator}_search`]; - qs.search(path, queryset).then((res) => { - $scope.dataset = res.data; - $scope.collection = res.data.results; - $scope.$emit('updateDataset', res.data); - }); - - $scope.searchTerm = null; - generateSearchTags(); - } + $scope.$emit(optionsKey, data.options); } }); - $scope.$on('$destroy', transitionSuccessListener); + 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; + } - $scope.$watch('disableSearch', function(disableSearch){ - if(disableSearch) { - $scope.searchPlaceholder = i18n._('Cannot search running job'); - } - else { - $scope.searchPlaceholder = i18n._('Search'); + if (transitionSuccessListener) { + transitionSuccessListener(); + } + + transitionSuccessListener = $transitions.onSuccess({}, trans => { + // State has changed - check to see if this is a param change + if (trans.from().name === trans.to().name) { + if (!compareParams(trans.params('from')[searchKey], trans.params('to')[searchKey])) { + // Params are not the same - we need to update the search. This should only + // happen when the user hits the forward/back browser navigation buttons. + queryset = trans.params('to')[searchKey]; + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + $scope.$emit('updateDataset', res.data); + }); + + $scope.searchTerm = null; + generateSearchTags(); } + } + }); + + $scope.$on('$destroy', transitionSuccessListener); + $scope.$watch('disableSearch', disableSearch => { + if (disableSearch) { + $scope.searchPlaceholder = i18n._('Cannot search running job'); + } else { + $scope.searchPlaceholder = i18n._('Search'); + } + }); + } + + function generateSearchTags () { + $scope.searchTags = []; + + const querysetCopy = angular.copy(queryset); + + if ($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) { + const searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20'); + delete querysetCopy[$scope.singleSearchParam]; + + $.each(searchParam, (index, param) => { + const paramParts = decodeURIComponent(param).split(/=(.+)/); + const reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]); + $scope.searchTags.push(reconstructedSearchString); }); } - function generateSearchTags() { - $scope.searchTags = []; + $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + } - let querysetCopy = angular.copy(queryset); - - if($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) { - let searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20'); - delete querysetCopy[$scope.singleSearchParam]; - - $.each(searchParam, function(index, param) { - let paramParts = decodeURIComponent(param).split(/=(.+)/); - let reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]); - $scope.searchTags.push(reconstructedSearchString); - }); + function revertSearch (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('.', { [searchKey]: queryset }); + } + qs.search(path, queryset).then((res) => { + if ($scope.querySet) { + $scope.querySet = queryset; } + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); - $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + $scope.searchTerm = null; + + generateSearchTags(); + } + + $scope.toggleKeyPane = () => { + $scope.showKeyPane = !$scope.showKeyPane; + }; + + function searchWithoutKey (term, singleSearchParam = null) { + if (singleSearchParam) { + return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; + } + return { search: encodeURIComponent(term) }; + } + + function isAnsibleFactSearchTerm (termParts) { + const rootField = termParts[0].split('.')[0].replace(/^-/, ''); + return rootField === 'ansible_facts'; + } + + function isRelatedField (termParts) { + const rootField = termParts[0].split('.')[0].replace(/^-/, ''); + const listName = $scope.list.name; + const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`; + + const isRelatedSearchTermField = (_.contains($scope.models[listName].related, rootField)); + const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field'); + + return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField); + } + + function getSearchInputQueryset ({ terms, singleSearchParam }) { + let params = {}; + + // remove leading/trailing whitespace + terms = (terms) ? terms.trim() : ''; + let splitTerms; + + if (singleSearchParam === 'host_filter') { + splitTerms = SmartSearchService.splitFilterIntoTerms(terms); + } else { + splitTerms = SmartSearchService.splitSearchIntoTerms(terms); } - function revertSearch(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 }); + const combineSameSearches = (a, b) => { + if (!a) { + return undefined; } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = res.data; - $scope.collection = res.data.results; - }); - $scope.searchTerm = null; - - generateSearchTags(); - } - - function searchWithoutKey(term) { - if($scope.singleSearchParam) { - return { - [$scope.singleSearchParam]: "search=" + encodeURIComponent(term) - }; + if (_.isArray(a)) { + return a.concat(b); } - return { - search: encodeURIComponent(term) - }; - } - $scope.toggleKeyPane = function() { - $scope.showKeyPane = !$scope.showKeyPane; + if (singleSearchParam) { + return `${a}%20and%20${b}`; + } + + return [a, b]; }; - // add a search tag, merge new queryset, $state.go() - $scope.addTerm = function(terms) { - let params = {}, - origQueryset = _.clone(queryset); - - // Remove leading/trailing whitespace if there is any - terms = (terms) ? terms.trim() : ""; - - if(terms && terms !== '') { - let splitTerms; - - if ($scope.singleSearchParam === 'host_filter') { - splitTerms = SmartSearchService.splitFilterIntoTerms(terms); - } else { - 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) { - if($scope.singleSearchParam) { - return a + "%20and%20" + b; - } - else { - return [a,b]; - } - } - } - } - - if($scope.singleSearchParam) { - if (termParts.length === 1) { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } - else { - let root = termParts[0].split(".")[0].replace(/^-/, ''); - if(_.has($scope.models[$scope.list.name].base, root) || root === "ansible_facts") { - if(_.has($scope.models[$scope.list.name].base[root], "type") && $scope.models[$scope.list.name].base[root].type === 'field'){ - // Intent is to land here for searching on the base model. - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - else { - // Intent is to land here when performing ansible_facts searches - params = _.merge(params, qs.encodeParam({term: term, searchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - } - else if(_.contains($scope.models[$scope.list.name].related, root)) { - // Intent is to land here for related searches - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - // Its not a search term or a related search term - treat it as a string - else { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } - } - } - - else { - // if only a value is provided, search using default keys - if (termParts.length === 1) { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } else { - // Figure out if this is a search term - let root = termParts[0].split(".")[0].replace(/^-/, ''); - if(_.has($scope.models[$scope.list.name].base, root)) { - if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); - } - else { - params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches); - } - } - // The related fields need to also be checked for related searches. - // The related fields for the search are retrieved from the API - // options endpoint, and are stored in the $scope.model. FYI, the - // Django search model is what sets the related fields on the model. - else if(_.contains($scope.models[$scope.list.name].related, root)) { - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); - } - // Its not a search term or a related search term - treat it as a string - else { - params = _.merge(params, searchWithoutKey(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 { - if($scope.singleSearchParam) { - if(!object[key]) { - return sourceValue; - } - else { - let singleSearchParamKeys = object[key].split("%20and%20"); - - if(_.includes(singleSearchParamKeys, sourceValue)) { - return object[key]; - } - else { - return object[key] + "%20and%20" + sourceValue; - } - } - } - // 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 }).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() { - revertSearch(origQueryset); - }); - - $scope.searchTerm = null; - - generateSearchTags(); - } - }; - - // remove tag, merge new queryset, $state.go - $scope.removeTerm = 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 { - if ($scope.singleSearchParam && set[$scope.singleSearchParam] && set[$scope.singleSearchParam].includes("%20and%20")) { - let searchParamParts = set[$scope.singleSearchParam].split("%20and%20"); - // The value side of each paramPart might have been encoded in SmartSearch.splitFilterIntoTerms - _.each(searchParamParts, (paramPart, paramPartIndex) => { - searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); - }); - var index = searchParamParts.indexOf(value); - if (index !== -1) { - searchParamParts.splice(index, 1); - } - set[$scope.singleSearchParam] = searchParamParts.join("%20and%20"); - } else { - delete set[key]; - } - } - }); - }; + _.each(splitTerms, term => { + const termParts = SmartSearchService.splitTermIntoParts(term); + let termParams; if (termParts.length === 1) { - removed = searchWithoutKey(tagToRemove); + termParams = searchWithoutKey(term, singleSearchParam); + } else if (isAnsibleFactSearchTerm(termParts)) { + termParams = qs.encodeParam({ term, singleSearchParam }); + } else if (isRelatedField(termParts)) { + termParams = qs.encodeParam({ term, singleSearchParam, related: true }); } else { - let root = termParts[0].split(".")[0].replace(/^-/, ''); - let encodeParams = { - term: tagToRemove, - singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false - }; - if($scope.models[$scope.list.name]) { - if($scope.singleSearchParam) { - removed = qs.encodeParam(encodeParams); - } - else if(_.has($scope.models[$scope.list.name].base, root)) { - if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { - encodeParams.relatedSearchTerm = true; - } - else { - encodeParams.searchTerm = true; - } - removed = qs.encodeParam(encodeParams); - } - else if(_.contains($scope.models[$scope.list.name].related, root)) { - encodeParams.relatedSearchTerm = true; - removed = qs.encodeParam(encodeParams); - } - else { - removed = searchWithoutKey(termParts[termParts.length-1]); - } - } - else { - removed = searchWithoutKey(termParts[termParts.length-1]); - } + termParams = qs.encodeParam({ term, singleSearchParam }); } - removeFromQuerySet(queryset); - if (!$scope.querySet) { - $state.go('.', { - [$scope.iterator + '_search']: queryset }).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; - } + params = _.merge(params, termParams, combineSameSearches); + }); - $scope.dataset = res.data; - $scope.collection = res.data.results; - - generateSearchTags(); - }); - }; - - $scope.clearAllTerms = function(){ - let cleared = _.cloneDeep(defaults); - delete cleared.page; - queryset = cleared; - if(!$scope.querySet) { - $state.go('.', {[$scope.iterator + '_search']: queryset}); - } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = res.data; - $scope.collection = res.data.results; - }); - $scope.searchTags = qs.stripDefaultParams(queryset, defaults); - }; + return params; } + + function mergeQueryset (qset, additional, singleSearchParam) { + const merged = _.merge({}, qset, additional, (objectValue, sourceValue, key, object) => { + if (!(object[key] && object[key] !== sourceValue)) { + // // https://lodash.com/docs/3.10.1#each + // If this returns undefined merging is handled by default _.merge algorithm + return undefined; + } + + if (_.isArray(object[key])) { + object[key].push(sourceValue); + return object[key]; + } + + if (singleSearchParam) { + if (!object[key]) { + return sourceValue; + } + + const singleSearchParamKeys = object[key].split('%20and%20'); + + if (_.includes(singleSearchParamKeys, sourceValue)) { + return object[key]; + } + + return `${object[key]}%20and%20${sourceValue}`; + } + + // Start the array of keys + return [object[key], sourceValue]; + }); + + return merged; + } + + $scope.addTerms = terms => { + const { singleSearchParam } = $scope; + const origQueryset = _.clone(queryset); + + // Remove leading/trailing whitespace if there is any + terms = (terms) ? terms.trim() : ''; + + if (!(terms && terms !== '')) { + return; + } + + const searchInputQueryset = getSearchInputQueryset({ terms, singleSearchParam }); + queryset = mergeQueryset(queryset, searchInputQueryset, singleSearchParam); + + // 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[searchKey] terms. + if (!$scope.querySet) { + $state.go('.', { [searchKey]: queryset }) + .then(() => { + // 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[searchKey].page; + }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }) + .catch(() => revertSearch(origQueryset)); + + $scope.searchTerm = null; + + generateSearchTags(); + }; + + function removeTermsFromQueryset(qset, term, singleSearchParam = null) { + const returnedQueryset = _.cloneDeep(qset); + + const removeSingleTermFromQueryset = (value, key) => { + const space = '%20and%20'; + + if (Array.isArray(returnedQueryset[key])) { + returnedQueryset[key] = returnedQueryset[key].filter(item => item !== value); + if (returnedQueryset[key].length < 1) { + delete returnedQueryset[key]; + } + } else if (singleSearchParam && _.get(returnedQueryset, singleSearchParam, []).includes(space)) { + const searchParamParts = returnedQueryset[singleSearchParam].split(space); + // The value side of each paramPart might have been encoded in + // SmartSearch.splitFilterIntoTerms + _.each(searchParamParts, (paramPart, paramPartIndex) => { + searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); + }); + + const paramPartIndex = searchParamParts.indexOf(value); + + if (paramPartIndex !== -1) { + searchParamParts.splice(paramPartIndex, 1); + } + + returnedQueryset[singleSearchParam] = searchParamParts.join(space); + + } else { + delete returnedQueryset[key]; + } + }; + + const termParts = SmartSearchService.splitTermIntoParts(term); + + let removed; + + if (termParts.length === 1) { + removed = searchWithoutKey(term, singleSearchParam); + } else if (isRelatedField(termParts)) { + removed = qs.encodeParam({ term, singleSearchParam, related: true }); + } else { + removed = qs.encodeParam({ term, singleSearchParam }); + } + + if (!removed) { + removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam); + } + + _.each(removed, removeSingleTermFromQueryset); + + return returnedQueryset; + } + + // remove tag, merge new queryset, $state.go + $scope.removeTerm = index => { + const { singleSearchParam } = $scope; + const [term] = $scope.searchTags.splice(index, 1); + + const modifiedQueryset = removeTermsFromQueryset(queryset, term, singleSearchParam); + + if (!$scope.querySet) { + $state.go('.', { [searchKey]: modifiedQueryset }) + .then(() => { + // 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. + const clearedParams = removeTermsFromQueryset( + $stateParams[searchKey], term, singleSearchParam); + $stateParams[searchKey] = clearedParams; + }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }); + + generateSearchTags(); + }; + + $scope.clearAllTerms = () => { + const cleared = _.cloneDeep(defaults); + + delete cleared.page; + + queryset = cleared; + + if (!$scope.querySet) { + $state.go('.', { [searchKey]: queryset }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }); + + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); + }; +} + +SmartSearchController.$inject = [ + '$scope', + '$state', + '$stateParams', + '$transitions', + 'ConfigService', + 'GetBasePath', + 'i18n', + 'QuerySet', + 'SmartSearchService', ]; + +export default SmartSearchController; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html index 1f31adcf9e..ce52fea759 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -3,11 +3,11 @@
-
+
-
+