From 13162ca33ab877d7b52e5fa08b03bef46b864521 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:29:41 -0500 Subject: [PATCH] move data transformation logic into a service so it can be reused --- .../shared/smart-search/queryset.service.js | 782 +++++++++++------- .../smart-search/smart-search.controller.js | 196 +---- 2 files changed, 492 insertions(+), 486 deletions(-) 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 7d07139c4c..780900d32f 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -1,333 +1,507 @@ -export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', 'SmartSearchService', - function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) { - return { - // kick off building a model for a specific endpoint - // this is usually a list's basePath - // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields - initFieldset(path, name) { - let defer = $q.defer(); - defer.resolve(this.getCommonModelOptions(path, name)); - return defer.promise; - }, +function searchWithoutKey (term, singleSearchParam = null) { + if (singleSearchParam) { + return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; + } + return { search: encodeURIComponent(term) }; +} - getCommonModelOptions(path, name) { - let resolve, base, - defer = $q.defer(); +function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) { + return { + // kick off building a model for a specific endpoint + // this is usually a list's basePath + // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields + initFieldset(path, name) { + let defer = $q.defer(); + defer.resolve(this.getCommonModelOptions(path, name)); + return defer.promise; + }, + getCommonModelOptions(path, name) { + let resolve, base, + defer = $q.defer(); - this.url = path; - resolve = this.options(path) - .then((res) => { - base = res.data.actions.GET; - let relatedSearchFields = res.data.related_search_fields; - defer.resolve({ - models: { - [name]: new DjangoSearchModel(name, base, relatedSearchFields) - }, - options: res - }); + this.url = path; + resolve = this.options(path) + .then((res) => { + base = res.data.actions.GET; + let relatedSearchFields = res.data.related_search_fields; + defer.resolve({ + models: { + [name]: new DjangoSearchModel(name, base, relatedSearchFields) + }, + options: res }); - return defer.promise; - }, + }); + return defer.promise; + }, + replaceDefaultFlags (value) { + value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains"); + value = value.toString().replace(/__search_DEFAULT/g, "__search"); - replaceDefaultFlags (value) { - value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains"); - value = value.toString().replace(/__search_DEFAULT/g, "__search"); + return value; + }, + replaceEncodedTokens(value) { + return decodeURIComponent(value).replace(/"|'/g, ""); + }, + encodeTerms (values, key) { + key = this.replaceDefaultFlags(key); - return value; - }, + if (!Array.isArray(values)) { + values = [values]; + } - replaceEncodedTokens(value) { - return decodeURIComponent(value).replace(/"|'/g, ""); - }, + return values + .map(value => { + value = this.replaceDefaultFlags(value); + value = this.replaceEncodedTokens(value); + return [key, value] + }) - encodeTerms (values, key) { - key = this.replaceDefaultFlags(key); + }, + // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL + encodeQueryset(params) { + if (typeof params !== 'object') { + return ''; + } - if (!Array.isArray(values)) { - values = [values]; + return _.reduce(params, (result, value, key) => { + if (result !== '?') { + result += '&'; } - return values - .map(value => { - value = this.replaceDefaultFlags(value); - value = this.replaceEncodedTokens(value); - return [key, value] - }) + const encodedTermString = this.encodeTerms(value, key) + .map(([key, value]) => `${key}=${value}`) + .join('&'); - }, - // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL - encodeQueryset(params) { - if (typeof params !== 'object') { - return ''; + 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 _.reduce(params, (result, value, key) => { - if (result !== '?') { - result += '&'; - } - - 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({ 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(term); - let keySplit = paramParts[0].split('.'); - let exclude = false; - let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; - if(keySplit[0].match(/^-/g)) { - exclude = true; - keySplit[0] = keySplit[0].replace(/^-/, ''); - } - let paramString = exclude ? "not__" : ""; - let valueString = paramParts[1]; - if(keySplit.length === 1) { - if(searchTerm && !lessThanGreaterThan) { - if(singleSearchParam) { - paramString += keySplit[0] + '__icontains'; - } - else { - paramString += keySplit[0] + '__icontains_DEFAULT'; - } - } - else if(relatedSearchTerm) { - if(singleSearchParam) { - paramString += keySplit[0]; - } - else { - paramString += keySplit[0] + '__search_DEFAULT'; - } + return obj; + }, {}); + }, + // encodes a ui smart-search param to a django-friendly param + // operand:key:comparator:value => {operand__key__comparator: value} + 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(term); + let keySplit = paramParts[0].split('.'); + let exclude = false; + let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; + if(keySplit[0].match(/^-/g)) { + exclude = true; + keySplit[0] = keySplit[0].replace(/^-/, ''); + } + let paramString = exclude ? "not__" : ""; + let valueString = paramParts[1]; + if(keySplit.length === 1) { + if(searchTerm && !lessThanGreaterThan) { + if(singleSearchParam) { + paramString += keySplit[0] + '__icontains'; } else { + paramString += keySplit[0] + '__icontains_DEFAULT'; + } + } + else if(relatedSearchTerm) { + if(singleSearchParam) { paramString += keySplit[0]; } - } - else { - paramString += keySplit.join('__'); - } - - if(lessThanGreaterThan) { - if(paramParts[1].match(/^>=.*$/)) { - paramString += '__gte'; - valueString = valueString.replace(/^(>=)/,""); - } - else if(paramParts[1].match(/^<=.*$/)) { - paramString += '__lte'; - valueString = valueString.replace(/^(<=)/,""); - } - else if(paramParts[1].match(/^<.*$/)) { - paramString += '__lt'; - valueString = valueString.replace(/^(<)/,""); - } - else if(paramParts[1].match(/^>.*$/)) { - paramString += '__gt'; - valueString = valueString.replace(/^(>)/,""); - } - } - - if(singleSearchParam) { - return {[singleSearchParam]: paramString + "=" + valueString}; - } - else { - return {[paramString] : encodeURIComponent(valueString)}; - } - }, - // decodes a django queryset param into a ui smart-search tag or set of tags - decodeParam(value, key){ - - let decodeParamString = function(searchString) { - if(key === 'search') { - // Don't include 'search:' in the search tag - return decodeURIComponent(`${searchString}`); - } else { - key = key.toString().replace(/__icontains_DEFAULT/g, ""); - key = key.toString().replace(/__search_DEFAULT/g, ""); - let split = key.split('__'); - let decodedParam = searchString; - let exclude = false; - if(key.startsWith('not__')) { - exclude = true; - split = split.splice(1, split.length); - } - if(key.endsWith('__gt')) { - decodedParam = '>' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__lt')) { - decodedParam = '<' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__gte')) { - decodedParam = '>=' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__lte')) { - decodedParam = '<=' + decodedParam; - split = split.splice(0, split.length-1); - } - - let uriDecodedParam = decodeURIComponent(decodedParam); - - return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`; + paramString += keySplit[0] + '__search_DEFAULT'; } - }; + } + else { + paramString += keySplit[0]; + } + } + else { + paramString += keySplit.join('__'); + } + if(lessThanGreaterThan) { + if(paramParts[1].match(/^>=.*$/)) { + paramString += '__gte'; + valueString = valueString.replace(/^(>=)/,""); + } + else if(paramParts[1].match(/^<=.*$/)) { + paramString += '__lte'; + valueString = valueString.replace(/^(<=)/,""); + } + else if(paramParts[1].match(/^<.*$/)) { + paramString += '__lt'; + valueString = valueString.replace(/^(<)/,""); + } + else if(paramParts[1].match(/^>.*$/)) { + paramString += '__gt'; + valueString = valueString.replace(/^(>)/,""); + } + } + + if(singleSearchParam) { + return {[singleSearchParam]: paramString + "=" + valueString}; + } + else { + return {[paramString] : encodeURIComponent(valueString)}; + } + }, + // decodes a django queryset param into a ui smart-search tag or set of tags + decodeParam(value, key){ + + let decodeParamString = function(searchString) { + if(key === 'search') { + // Don't include 'search:' in the search tag + return decodeURIComponent(`${searchString}`); + } + else { + key = key.toString().replace(/__icontains_DEFAULT/g, ""); + key = key.toString().replace(/__search_DEFAULT/g, ""); + let split = key.split('__'); + let decodedParam = searchString; + let exclude = false; + if(key.startsWith('not__')) { + exclude = true; + split = split.splice(1, split.length); + } + if(key.endsWith('__gt')) { + decodedParam = '>' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lt')) { + decodedParam = '<' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__gte')) { + decodedParam = '>=' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lte')) { + decodedParam = '<=' + decodedParam; + split = split.splice(0, split.length-1); + } + + let uriDecodedParam = decodeURIComponent(decodedParam); + + return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`; + } + }; + + if (Array.isArray(value)){ + value = _.uniq(_.flattenDeep(value)); + return _.map(value, (item) => { + return decodeParamString(item); + }); + } + else { + return decodeParamString(value); + } + }, + // encodes a django queryset for ui-router's URLMatcherFactory + // {operand__key__comparator: value, } => 'operand:key:comparator:value;...' + // value.isArray expands to: + // {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...' + encodeArr(params) { + let url; + url = _.reduce(params, (result, value, key) => { + return result.concat(encodeUrlString(value, key)); + }, []); + + return url.join(';'); + + // {key:'value'} => 'key:value' + // {key: [value1, value2, ...]} => ['key:value1', 'key:value2'] + function encodeUrlString(value, key){ if (Array.isArray(value)){ value = _.uniq(_.flattenDeep(value)); return _.map(value, (item) => { - return decodeParamString(item); + return `${key}:${item}`; }); } else { - 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: - // {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...' - encodeArr(params) { - let url; - url = _.reduce(params, (result, value, key) => { - return result.concat(encodeUrlString(value, key)); - }, []); - - return url.join(';'); - - // {key:'value'} => 'key:value' - // {key: [value1, value2, ...]} => ['key:value1', 'key:value2'] - function encodeUrlString(value, key){ - if (Array.isArray(value)){ - value = _.uniq(_.flattenDeep(value)); - return _.map(value, (item) => { - return `${key}:${item}`; - }); - } - else { - return `${key}:${value}`; - } - } - }, - - // decodes a django queryset for ui-router's URLMatcherFactory - // 'operand:key:comparator:value,...' => {operand__key__comparator: value, } - decodeArr(arr) { - let params = {}; - _.forEach(arr.split(';'), (item) => { - let key = item.split(':')[0], - value = item.split(':')[1]; - if(!params[key]){ - params[key] = value; - } - else if (Array.isArray(params[key])){ - params[key] = _.uniq(_.flattenDeep(params[key])); - params[key].push(value); - } - else { - params[key] = [params[key], value]; - } - }); - return params; - }, - // REST utilities - options(endpoint) { - Rest.setUrl(endpoint); - return Rest.options(endpoint); - }, - search(endpoint, params) { - Wait('start'); - this.url = `${endpoint}${this.encodeQueryset(params)}`; - Rest.setUrl(this.url); - - return Rest.get() - .then(function(response) { - Wait('stop'); - - if (response - .headers('X-UI-Max-Events') !== null) { - response.data.maxEvents = response. - headers('X-UI-Max-Events'); - } - - return response; - }) - .catch(function(response) { - Wait('stop'); - - this.error(response.data, response.status); - - throw response; - }.bind(this)); - }, - error(data, status) { - if(data && data.detail){ - let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail); - - if(_.isArray(error)){ - data.detail = error[0]; - } - } - ProcessErrors($rootScope, data, status, null, { - hdr: 'Error!', - msg: `Invalid search term entered. GET returned: ${status}` - }); - }, - // Removes state definition defaults and pagination terms - stripDefaultParams(params, defaults) { - if(defaults) { - 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(this.decodeParam).flatten().value(); - } - else { - return _(params).map(this.decodeParam).flatten().value(); + return `${key}:${value}`; } } - }; - } + }, + // decodes a django queryset for ui-router's URLMatcherFactory + // 'operand:key:comparator:value,...' => {operand__key__comparator: value, } + decodeArr(arr) { + let params = {}; + + if (!arr) { + return params; + } + + _.forEach(arr.split(';'), (item) => { + let key = item.split(':')[0], + value = item.split(':')[1]; + if(!params[key]){ + params[key] = value; + } + else if (Array.isArray(params[key])){ + params[key] = _.uniq(_.flattenDeep(params[key])); + params[key].push(value); + } + else { + params[key] = [params[key], value]; + } + }); + return params; + }, + // REST utilities + options(endpoint) { + Rest.setUrl(endpoint); + return Rest.options(endpoint); + }, + search(endpoint, params) { + Wait('start'); + this.url = `${endpoint}${this.encodeQueryset(params)}`; + Rest.setUrl(this.url); + + return Rest.get() + .then(function(response) { + Wait('stop'); + + if (response + .headers('X-UI-Max-Events') !== null) { + response.data.maxEvents = response. + headers('X-UI-Max-Events'); + } + + return response; + }) + .catch(function(response) { + Wait('stop'); + + this.error(response.data, response.status); + + throw response; + }.bind(this)); + }, + error(data, status) { + if(data && data.detail){ + let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail); + + if(_.isArray(error)){ + data.detail = error[0]; + } + } + ProcessErrors($rootScope, data, status, null, { + hdr: 'Error!', + msg: `Invalid search term entered. GET returned: ${status}` + }); + }, + // Removes state definition defaults and pagination terms + stripDefaultParams(params, defaultParams) { + if (!params) { + return []; + } + if(defaultParams) { + 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 defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null; + }); + let strippedCopy = _.cloneDeep(stripped); + if(_.keys(_.pick(defaultParams, _.keys(strippedCopy))).length > 0){ + for (var key in strippedCopy) { + if (strippedCopy.hasOwnProperty(key)) { + let value = strippedCopy[key]; + if(_.isArray(value)){ + let index = _.indexOf(value, defaultParams[key]); + value = value.splice(index, 1)[0]; + } + } + } + stripped = strippedCopy; + } + return _(strippedCopy).map(this.decodeParam).flatten().value(); + } + else { + return _(params).map(this.decodeParam).flatten().value(); + } + }, + mergeQueryset (queryset, additional, singleSearchParam) { + const space = '%20and%20'; + + const merged = _.merge({}, queryset, 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(space); + + if (_.includes(singleSearchParamKeys, sourceValue)) { + return object[key]; + } + + return `${object[key]}${space}${sourceValue}`; + } + + // Start the array of keys + return [object[key], sourceValue]; + }); + + return merged; + }, + getSearchInputQueryset (searchInput, isRelatedField = null, isAnsibleFactField = null, singleSearchParam = null) { + // XXX Should find a better approach than passing in the two 'is...Field' callbacks XXX + const space = '%20and%20'; + let params = {}; + + // Remove leading/trailing whitespace if there is any + const terms = (searchInput) ? searchInput.trim() : ''; + + if (!(terms && terms !== '')) { + return; + } + + let splitTerms; + + if (singleSearchParam === 'host_filter') { + splitTerms = SmartSearchService.splitFilterIntoTerms(terms); + } else { + splitTerms = SmartSearchService.splitSearchIntoTerms(terms); + } + + const combineSameSearches = (a, b) => { + if (!a) { + return undefined; + } + + if (_.isArray(a)) { + return a.concat(b); + } + + if (singleSearchParam) { + return `${a}${space}${b}`; + } + + return [a, b]; + }; + + _.each(splitTerms, term => { + const termParts = SmartSearchService.splitTermIntoParts(term); + let termParams; + + if (termParts.length === 1) { + termParams = searchWithoutKey(term, singleSearchParam); + } else if (isAnsibleFactField && isAnsibleFactField(termParts)) { + termParams = this.encodeParam({ term, singleSearchParam }); + } else if (isRelatedField && isRelatedField(termParts)) { + termParams = this.encodeParam({ term, singleSearchParam, related: true }); + } else { + termParams = this.encodeParam({ term, singleSearchParam }); + } + + params = _.merge(params, termParams, combineSameSearches); + }); + + return params; + }, + removeTermsFromQueryset(queryset, term, isRelatedField = null, singleSearchParam = null) { + const modifiedQueryset = _.cloneDeep(queryset); + + const removeSingleTermFromQueryset = (value, key) => { + const space = '%20and%20'; + + if (Array.isArray(modifiedQueryset[key])) { + modifiedQueryset[key] = modifiedQueryset[key].filter(item => item !== value); + if (modifiedQueryset[key].length < 1) { + delete modifiedQueryset[key]; + } + } else if (singleSearchParam && _.get(modifiedQueryset, singleSearchParam, []).includes(space)) { + const searchParamParts = modifiedQueryset[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); + } + + modifiedQueryset[singleSearchParam] = searchParamParts.join(space); + + } else { + delete modifiedQueryset[key]; + } + }; + + const termParts = SmartSearchService.splitTermIntoParts(term); + + let removed; + + if (termParts.length === 1) { + removed = searchWithoutKey(term, singleSearchParam); + } else if (isRelatedField && isRelatedField(termParts)) { + removed = this.encodeParam({ term, singleSearchParam, related: true }); + } else { + removed = this.encodeParam({ term, singleSearchParam }); + } + + if (!removed) { + removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam); + } + + _.each(removed, removeSingleTermFromQueryset); + + return modifiedQueryset; + }, + createSearchTagsFromQueryset(queryset, defaultParams = null, singleSearchParam = null) { + const space = '%20and%20'; + const modifiedQueryset = angular.copy(queryset); + + let searchTags = []; + + if (singleSearchParam && modifiedQueryset[singleSearchParam]) { + const searchParam = modifiedQueryset[singleSearchParam].split(space); + delete modifiedQueryset[singleSearchParam]; + + $.each(searchParam, (index, param) => { + const paramParts = decodeURIComponent(param).split(/=(.+)/); + const reconstructedSearchString = this.decodeParam(paramParts[1], paramParts[0]); + + searchTags.push(reconstructedSearchString); + }); + } + + return searchTags.concat(this.stripDefaultParams(modifiedQueryset, defaultParams)); + } + }; +} + +QuerysetService.$inject = [ + '$q', + 'Rest', + 'ProcessErrors', + '$rootScope', + 'Wait', + 'DjangoSearchModel', + 'SmartSearchService', ]; + +export default QuerysetService; 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 5a59bbde81..414a0763db 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 @@ -7,7 +7,6 @@ function SmartSearchController ( GetBasePath, i18n, qs, - SmartSearchService ) { const searchKey = `${$scope.iterator}_search`; const optionsKey = `${$scope.list.iterator}_options`; @@ -58,7 +57,7 @@ function SmartSearchController ( } }); - function compareParams(a, b) { + function compareParams (a, b) { for (let key in a) { if (!(key in b) || a[key].toString() !== b[key].toString()) { return false; @@ -106,22 +105,8 @@ function SmartSearchController ( } 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); - }); - } - - $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + const { singleSearchParam } = $scope; + $scope.searchTags = qs.createSearchTagsFromQueryset(queryset, defaults, singleSearchParam); } function revertSearch (queryToBeRestored) { @@ -149,14 +134,7 @@ function SmartSearchController ( $scope.showKeyPane = !$scope.showKeyPane; }; - function searchWithoutKey (term, singleSearchParam = null) { - if (singleSearchParam) { - return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; - } - return { search: encodeURIComponent(term) }; - } - - function isAnsibleFactSearchTerm (termParts) { + function isAnsibleFactField (termParts) { const rootField = termParts[0].split('.')[0].replace(/^-/, ''); return rootField === 'ansible_facts'; } @@ -172,111 +150,21 @@ function SmartSearchController ( 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); - } - - const combineSameSearches = (a, b) => { - if (!a) { - return undefined; - } - - if (_.isArray(a)) { - return a.concat(b); - } - - if (singleSearchParam) { - return `${a}%20and%20${b}`; - } - - return [a, b]; - }; - - _.each(splitTerms, term => { - const termParts = SmartSearchService.splitTermIntoParts(term); - let termParams; - - if (termParts.length === 1) { - 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 { - termParams = qs.encodeParam({ term, singleSearchParam }); - } - - params = _.merge(params, termParams, combineSameSearches); - }); - - 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); + const unmodifiedQueryset = _.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); + const searchInputQueryset = qs.getSearchInputQueryset(terms, isRelatedField, isAnsibleFactField, singleSearchParam); + const modifiedQueryset = qs.mergeQueryset(queryset, searchInputQueryset, singleSearchParam); // Go back to the first page after a new search - delete queryset.page; + delete modifiedQueryset.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 }) + $state.go('.', { [searchKey]: modifiedQueryset }) .then(() => { // same as above in $scope.remove. For some reason deleting the page // from the queryset works for all lists except lists in modals. @@ -284,80 +172,26 @@ function SmartSearchController ( }); } - qs.search(path, queryset) + qs.search(path, modifiedQueryset) .then(({ data }) => { if ($scope.querySet) { - $scope.querySet = queryset; + $scope.querySet = modifiedQueryset; } $scope.dataset = data; $scope.collection = data.results; }) - .catch(() => revertSearch(origQueryset)); + .catch(() => revertSearch(unmodifiedQueryset)); $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); + const modifiedQueryset = qs.removeTermsFromQueryset(queryset, term, isRelatedField, singleSearchParam); if (!$scope.querySet) { $state.go('.', { [searchKey]: modifiedQueryset }) @@ -365,8 +199,7 @@ function SmartSearchController ( // 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); + const clearedParams = qs.removeTermsFromQueryset($stateParams[searchKey], term, isRelatedField, singleSearchParam); $stateParams[searchKey] = clearedParams; }); } @@ -416,7 +249,6 @@ SmartSearchController.$inject = [ 'GetBasePath', 'i18n', 'QuerySet', - 'SmartSearchService', ]; export default SmartSearchController;